Clarify automatic host reconnection

This commit is contained in:
Mohamed Boudra
2025-11-26 19:33:43 +01:00
parent 393aa44e68
commit 0db0e1ad23
9 changed files with 123 additions and 41 deletions
+4 -4
View File
@@ -105,8 +105,8 @@ export default function LegacyAgentRedirectScreen() {
<View style={styles.centerState}> <View style={styles.centerState}>
<Text style={styles.title}>Agent not found</Text> <Text style={styles.title}>Agent not found</Text>
<Text style={styles.subtitle}> <Text style={styles.subtitle}>
We could not find {agentId} on any connected daemon. Make sure the daemon is online We could not find {agentId} on any connected host. Make sure the host is online or
or reopen the agent from the Home screen after it reconnects. reopen the agent from the Home screen after it reconnects.
</Text> </Text>
<Pressable style={styles.primaryButton} onPress={handleGoHome}> <Pressable style={styles.primaryButton} onPress={handleGoHome}>
<Text style={styles.primaryButtonText}>Go Home</Text> <Text style={styles.primaryButtonText}>Go Home</Text>
@@ -116,9 +116,9 @@ export default function LegacyAgentRedirectScreen() {
} else if (matches.length > 1) { } else if (matches.length > 1) {
body = ( body = (
<View style={styles.centerState}> <View style={styles.centerState}>
<Text style={styles.title}>Pick a daemon</Text> <Text style={styles.title}>Pick a host</Text>
<Text style={styles.subtitle}> <Text style={styles.subtitle}>
Multiple daemons have an agent with this id. Choose the one you intended to open. Multiple hosts have an agent with this id. Choose the one you intended to open.
</Text> </Text>
<View style={styles.matchList}> <View style={styles.matchList}>
{matches.map((match) => ( {matches.map((match) => (
@@ -653,13 +653,13 @@ function AgentSessionUnavailableState({
</> </>
) : ( ) : (
<> <>
<Text style={styles.errorText}> <Text style={styles.offlineTitle}>
Cannot open this agent while {serverLabel} is {connectionStatusLabel.toLowerCase()}. {serverLabel} is currently {connectionStatusLabel.toLowerCase()}.
</Text> </Text>
<Text style={styles.statusText}> <Text style={styles.offlineDescription}>
Connect this host or switch to another one to continue. We'll reconnect automatically and show this agent as soon as the host comes back online.
</Text> </Text>
{lastError ? <Text style={styles.errorDetails}>{lastError}</Text> : null} {lastError ? <Text style={styles.offlineDetails}>{lastError}</Text> : null}
</> </>
)} )}
</View> </View>
@@ -718,6 +718,22 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.xs,
color: theme.colors.mutedForeground, color: theme.colors.mutedForeground,
}, },
offlineTitle: {
fontSize: theme.fontSize.base,
fontWeight: theme.fontWeight.semibold,
color: theme.colors.foreground,
textAlign: "center",
},
offlineDescription: {
fontSize: theme.fontSize.sm,
color: theme.colors.mutedForeground,
textAlign: "center",
},
offlineDetails: {
fontSize: theme.fontSize.xs,
color: theme.colors.mutedForeground,
textAlign: "center",
},
menuButton: { menuButton: {
padding: theme.spacing[3], padding: theme.spacing[3],
borderRadius: theme.borderRadius.lg, borderRadius: theme.borderRadius.lg,
+23 -5
View File
@@ -688,15 +688,17 @@ function FileExplorerSessionUnavailable({
<> <>
<ActivityIndicator size="small" /> <ActivityIndicator size="small" />
<Text style={styles.loadingText}>Connecting to {serverLabel}...</Text> <Text style={styles.loadingText}>Connecting to {serverLabel}...</Text>
<Text style={styles.statusText}>We will load files once this daemon is online.</Text> <Text style={styles.statusText}>We will load files once this host is online.</Text>
</> </>
) : ( ) : (
<> <>
<Text style={styles.errorText}> <Text style={styles.offlineTitle}>
Cannot open files while {serverLabel} is {connectionStatusLabel.toLowerCase()}. {serverLabel} is currently {connectionStatusLabel.toLowerCase()}.
</Text> </Text>
<Text style={styles.statusText}>Connect this daemon and try again.</Text> <Text style={styles.offlineDescription}>
{lastError ? <Text style={styles.errorDetails}>{lastError}</Text> : null} We'll reconnect automatically and load files once the host is back online. No action needed.
</Text>
{lastError ? <Text style={styles.offlineDetails}>{lastError}</Text> : null}
</> </>
)} )}
</View> </View>
@@ -966,6 +968,22 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.xs,
textAlign: "center", textAlign: "center",
}, },
offlineTitle: {
color: theme.colors.foreground,
fontSize: theme.fontSize.base,
fontWeight: theme.fontWeight.semibold,
textAlign: "center",
},
offlineDescription: {
color: theme.colors.mutedForeground,
fontSize: theme.fontSize.sm,
textAlign: "center",
},
offlineDetails: {
color: theme.colors.mutedForeground,
fontSize: theme.fontSize.xs,
textAlign: "center",
},
emptyText: { emptyText: {
color: theme.colors.mutedForeground, color: theme.colors.mutedForeground,
fontSize: theme.fontSize.base, fontSize: theme.fontSize.base,
+43 -13
View File
@@ -81,19 +81,23 @@ export default function GitDiffScreen() {
); );
} }
const routeServerId = resolvedServerId ?? session.serverId; return (
<GitDiffContent
return <GitDiffContent session={session} agentId={agentId} routeServerId={routeServerId} />; session={session}
agentId={agentId}
serverLabel={serverLabel}
/>
);
} }
function GitDiffContent({ function GitDiffContent({
session, session,
agentId, agentId,
routeServerId, serverLabel,
}: { }: {
session: SessionContextValue; session: SessionContextValue;
agentId?: string; agentId?: string;
routeServerId: string; serverLabel: string;
}) { }) {
const { agents, gitDiffs, requestGitDiff } = session; const { agents, gitDiffs, requestGitDiff } = session;
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
@@ -127,7 +131,7 @@ function GitDiffContent({
return ( return (
<View style={styles.container}> <View style={styles.container}>
<BackHeader title="Changes" /> <BackHeader title="Changes" />
<Text style={styles.metaText}>Server: {routeServerId}</Text> <Text style={styles.metaText}>Host: {serverLabel}</Text>
<View style={styles.errorContainer}> <View style={styles.errorContainer}>
<Text style={styles.errorText}>Agent not found</Text> <Text style={styles.errorText}>Agent not found</Text>
</View> </View>
@@ -142,7 +146,7 @@ function GitDiffContent({
return ( return (
<View style={styles.container}> <View style={styles.container}>
<BackHeader title="Changes" /> <BackHeader title="Changes" />
<Text style={styles.metaText}>Server: {routeServerId}</Text> <Text style={styles.metaText}>Host: {serverLabel}</Text>
<ScrollView style={styles.scrollView} contentContainerStyle={styles.contentContainer}> <ScrollView style={styles.scrollView} contentContainerStyle={styles.contentContainer}>
{isLoading ? ( {isLoading ? (
@@ -216,8 +220,8 @@ function SessionUnavailableState({
return ( return (
<View style={styles.container}> <View style={styles.container}>
<BackHeader title="Changes" /> <BackHeader title="Changes" />
<Text style={styles.metaText}>Server: {serverLabel}</Text> <Text style={styles.metaText}>Host: {serverLabel}</Text>
<View style={styles.errorContainer}> <View style={styles.sessionStateContainer}>
{isConnecting ? ( {isConnecting ? (
<> <>
<ActivityIndicator size="large" /> <ActivityIndicator size="large" />
@@ -226,11 +230,13 @@ function SessionUnavailableState({
</> </>
) : ( ) : (
<> <>
<Text style={styles.errorText}> <Text style={styles.offlineTitle}>
Cannot load changes while {serverLabel} is {connectionStatusLabel.toLowerCase()}. {serverLabel} is currently {connectionStatusLabel.toLowerCase()}.
</Text> </Text>
<Text style={styles.statusText}>Connect this daemon or switch to another one to continue.</Text> <Text style={styles.offlineDescription}>
{lastError ? <Text style={styles.errorDetails}>{lastError}</Text> : null} We'll reconnect automatically and show changes once the host is back online. No action needed.
</Text>
{lastError ? <Text style={styles.offlineDetails}>{lastError}</Text> : null}
</> </>
)} )}
</View> </View>
@@ -243,6 +249,14 @@ const styles = StyleSheet.create((theme) => ({
flex: 1, flex: 1,
backgroundColor: theme.colors.background, backgroundColor: theme.colors.background,
}, },
sessionStateContainer: {
flex: 1,
alignItems: "center",
justifyContent: "center",
paddingTop: theme.spacing[16],
paddingHorizontal: theme.spacing[6],
gap: theme.spacing[3],
},
metaText: { metaText: {
paddingHorizontal: theme.spacing[6], paddingHorizontal: theme.spacing[6],
marginBottom: theme.spacing[2], marginBottom: theme.spacing[2],
@@ -291,6 +305,22 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.xs,
color: theme.colors.mutedForeground, color: theme.colors.mutedForeground,
}, },
offlineTitle: {
fontSize: theme.fontSize.base,
fontWeight: theme.fontWeight.semibold,
color: theme.colors.foreground,
textAlign: "center",
},
offlineDescription: {
fontSize: theme.fontSize.sm,
color: theme.colors.mutedForeground,
textAlign: "center",
},
offlineDetails: {
fontSize: theme.fontSize.xs,
color: theme.colors.mutedForeground,
textAlign: "center",
},
emptyContainer: { emptyContainer: {
flex: 1, flex: 1,
alignItems: "center", alignItems: "center",
+4 -1
View File
@@ -849,7 +849,10 @@ function DaemonCard({
} }
if (!wsIsConnectedRef.current) { if (!wsIsConnectedRef.current) {
Alert.alert("Not Connected", "Connect to the server before attempting a restart."); Alert.alert(
"Host offline",
"This host is offline. Paseo reconnects automatically—wait until it's back online before restarting."
);
return; return;
} }
+1 -1
View File
@@ -146,7 +146,7 @@ export function AgentList({ agentGroups }: AgentListProps) {
</Text> </Text>
<Text style={styles.sheetSubtitle}> <Text style={styles.sheetSubtitle}>
{isActionDaemonUnavailable {isActionDaemonUnavailable
? "Connect this host before managing its agents." ? "This host is offline—actions will be available once it reconnects automatically."
: "Removing this agent only deletes it from Paseo. Claude/Codex keeps the original project."} : "Removing this agent only deletes it from Paseo. Claude/Codex keeps the original project."}
</Text> </Text>
<Pressable <Pressable
@@ -336,7 +336,7 @@ function AgentFlowModal({
const daemonAvailabilityError = !hasSelectedDaemon const daemonAvailabilityError = !hasSelectedDaemon
? "Select a host before creating or importing agents." ? "Select a host before creating or importing agents."
: selectedDaemonIsUnavailable : selectedDaemonIsUnavailable
? `${selectedDaemonLabel} is ${selectedDaemonStatusLabel}. Connect to it before creating or importing agents.${ ? `${selectedDaemonLabel} is ${selectedDaemonStatusLabel}. We'll reconnect automatically and enable actions once it's online.${
selectedDaemonLastError ? ` ${selectedDaemonLastError}` : "" selectedDaemonLastError ? ` ${selectedDaemonLastError}` : ""
}` }`
: null; : null;
@@ -963,7 +963,7 @@ function AgentFlowModal({
logOfflineDaemonAction("dictation"); logOfflineDaemonAction("dictation");
setErrorMessage( setErrorMessage(
daemonAvailabilityError ?? daemonAvailabilityError ??
"Connect to the selected host before using dictation." "Dictation is unavailable until the selected host is online. Paseo reconnects automatically—try again once it comes back."
); );
return; return;
} }
@@ -1035,8 +1035,8 @@ function AgentFlowModal({
setIsImportLoading(false); setIsImportLoading(false);
logOfflineDaemonAction("import_list"); logOfflineDaemonAction("import_list");
setImportError( setImportError(
daemonAvailabilityError ?? daemonAvailabilityError ??
"Connect to the selected host to load import candidates." "Import candidates load automatically once the selected host is back online."
); );
return; return;
} }
@@ -1288,7 +1288,8 @@ function AgentFlowModal({
const trimmedWorkingDir = workingDir.trim(); const trimmedWorkingDir = workingDir.trim();
const shouldInspectRepo = isCreateFlow && isVisible && trimmedWorkingDir.length > 0; const shouldInspectRepo = isCreateFlow && isVisible && trimmedWorkingDir.length > 0;
const repoAvailabilityError = shouldInspectRepo && (!isTargetDaemonReady || !isWsConnected) const repoAvailabilityError = shouldInspectRepo && (!isTargetDaemonReady || !isWsConnected)
? daemonAvailabilityError ?? "Connect to the selected host to inspect the repository." ? daemonAvailabilityError ??
"Repository details will load automatically once the selected host is back online."
: null; : null;
const repoInfoStatus: "idle" | "loading" | "ready" | "error" = !shouldInspectRepo const repoInfoStatus: "idle" | "loading" | "ready" | "error" = !shouldInspectRepo
? "idle" ? "idle"
@@ -1367,7 +1368,7 @@ function AgentFlowModal({
logOfflineDaemonAction("create"); logOfflineDaemonAction("create");
setErrorMessage( setErrorMessage(
daemonAvailabilityError ?? daemonAvailabilityError ??
"Connect to the selected host before creating an agent." "Creating agents is temporarily unavailable while the selected host is offline. Paseo reconnects automatically—try again once it's online."
); );
return; return;
} }
@@ -1463,8 +1464,8 @@ function AgentFlowModal({
if (!resumeAgent || !isTargetDaemonReady) { if (!resumeAgent || !isTargetDaemonReady) {
logOfflineDaemonAction("resume"); logOfflineDaemonAction("resume");
setImportError( setImportError(
daemonAvailabilityError ?? daemonAvailabilityError ??
"Connect to the selected host before importing an agent." "Importing agents will resume automatically once the selected host is online."
); );
return; return;
} }
+13 -2
View File
@@ -57,10 +57,21 @@ export function GlobalFooter() {
const realtimeEligibleHosts = useMemo(() => { const realtimeEligibleHosts = useMemo(() => {
return Array.from(connectionStates.values()).filter((entry) => entry.status === "online"); return Array.from(connectionStates.values()).filter((entry) => entry.status === "online");
}, [connectionStates]); }, [connectionStates]);
const hasAnyConfiguredHosts = connectionStates.size > 0;
const handleStartRealtime = useCallback(() => { const handleStartRealtime = useCallback(() => {
if (realtimeEligibleHosts.length === 0) { if (realtimeEligibleHosts.length === 0) {
Alert.alert("No connected hosts", "Connect a host before starting realtime mode."); if (!hasAnyConfiguredHosts) {
Alert.alert(
"No hosts available",
"Add a host in Settings before starting realtime mode."
);
return;
}
Alert.alert(
"Hosts reconnecting",
"Every host is offline right now. Paseo reconnects automatically—try realtime again once one comes online."
);
return; return;
} }
if (realtimeEligibleHosts.length === 1) { if (realtimeEligibleHosts.length === 1) {
@@ -71,7 +82,7 @@ export function GlobalFooter() {
return; return;
} }
setShowRealtimeHostPicker(true); setShowRealtimeHostPicker(true);
}, [realtimeEligibleHosts, startRealtime]); }, [hasAnyConfiguredHosts, realtimeEligibleHosts, startRealtime]);
const handleSelectRealtimeHost = useCallback( const handleSelectRealtimeHost = useCallback(
(daemonId: string) => { (daemonId: string) => {
+5 -2
View File
@@ -47,7 +47,8 @@ The multi-daemon infrastructure is in place: session directory with daemon-scope
### 4. Transparent Connection Management ### 4. Transparent Connection Management
- [x] When a host is added, the app auto-connects and keeps the connection alive. - [x] When a host is added, the app auto-connects and keeps the connection alive.
- Added an AppState-aware reconnect path in `useWebSocket` so every background session automatically reconnects when the app becomes active again, keeping newly added hosts online without manual intervention; verified with `npm run typecheck --workspace=@paseo/app`. - Added an AppState-aware reconnect path in `useWebSocket` so every background session automatically reconnects when the app becomes active again, keeping newly added hosts online without manual intervention; verified with `npm run typecheck --workspace=@paseo/app`.
- [ ] Users should never need to manually "connect" to a host—the app handles it. - [x] Users should never need to manually "connect" to a host—the app handles it.
- Reworded realtime, settings restart, and agent creation/import flows so they explain hosts reconnect automatically instead of asking users to connect manually; verified with `npm run typecheck --workspace=@paseo/app`.
- [ ] Remove any UI that asks users to "connect" before performing actions. - [ ] Remove any UI that asks users to "connect" before performing actions.
### 5. Remove Host Status from Home Screen ### 5. Remove Host Status from Home Screen
@@ -70,9 +71,11 @@ The multi-daemon infrastructure is in place: session directory with daemon-scope
- [ ] Fix the contradictory error message "Daemon is online, connect to it before creating"—this should never appear. - [ ] Fix the contradictory error message "Daemon is online, connect to it before creating"—this should never appear.
### 7. Home Screen Agent List ### 7. Home Screen Agent List
- [ ] Show a loading indicator while hosts are connecting instead of the empty state.
- [ ] Remove grouping of agents by host—show a single flat list. - [ ] Remove grouping of agents by host—show a single flat list.
- [ ] Each agent row displays its host name as metadata (badge, subtitle, etc.). - [ ] Each agent row displays its host name as metadata (badge, subtitle, etc.).
- [ ] Sort agents by recent activity or alphabetically (not by host). - [ ] Sort agents by recent activity or alphabetically (not by host).
### Review: Git Diff Metadata Cleanup ### Review: Git Diff Metadata Cleanup
- [ ] Remove the now-unused `routeServerId` prop from `GitDiffContent` (`packages/app/src/app/git-diff.tsx:84-104`) so we aren't plumbing dead state through the component after switching to `serverLabel`. - [x] Remove the now-unused `routeServerId` prop from `GitDiffContent` (`packages/app/src/app/git-diff.tsx:84-104`) so we aren't plumbing dead state through the component after switching to `serverLabel`.
- Context: Deleted the redundant prop/const and updated `GitDiffContent` to rely solely on `serverLabel`, removing the final host-id plumbing that was no longer used anywhere in the component (`packages/app/src/app/git-diff.tsx`).