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}>
<Text style={styles.title}>Agent not found</Text>
<Text style={styles.subtitle}>
We could not find {agentId} on any connected daemon. Make sure the daemon is online
or reopen the agent from the Home screen after it reconnects.
We could not find {agentId} on any connected host. Make sure the host is online or
reopen the agent from the Home screen after it reconnects.
</Text>
<Pressable style={styles.primaryButton} onPress={handleGoHome}>
<Text style={styles.primaryButtonText}>Go Home</Text>
@@ -116,9 +116,9 @@ export default function LegacyAgentRedirectScreen() {
} else if (matches.length > 1) {
body = (
<View style={styles.centerState}>
<Text style={styles.title}>Pick a daemon</Text>
<Text style={styles.title}>Pick a host</Text>
<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>
<View style={styles.matchList}>
{matches.map((match) => (
@@ -653,13 +653,13 @@ function AgentSessionUnavailableState({
</>
) : (
<>
<Text style={styles.errorText}>
Cannot open this agent while {serverLabel} is {connectionStatusLabel.toLowerCase()}.
<Text style={styles.offlineTitle}>
{serverLabel} is currently {connectionStatusLabel.toLowerCase()}.
</Text>
<Text style={styles.statusText}>
Connect this host or switch to another one to continue.
<Text style={styles.offlineDescription}>
We'll reconnect automatically and show this agent as soon as the host comes back online.
</Text>
{lastError ? <Text style={styles.errorDetails}>{lastError}</Text> : null}
{lastError ? <Text style={styles.offlineDetails}>{lastError}</Text> : null}
</>
)}
</View>
@@ -718,6 +718,22 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.xs,
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: {
padding: theme.spacing[3],
borderRadius: theme.borderRadius.lg,
+23 -5
View File
@@ -688,15 +688,17 @@ function FileExplorerSessionUnavailable({
<>
<ActivityIndicator size="small" />
<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}>
Cannot open files while {serverLabel} is {connectionStatusLabel.toLowerCase()}.
<Text style={styles.offlineTitle}>
{serverLabel} is currently {connectionStatusLabel.toLowerCase()}.
</Text>
<Text style={styles.statusText}>Connect this daemon and try again.</Text>
{lastError ? <Text style={styles.errorDetails}>{lastError}</Text> : null}
<Text style={styles.offlineDescription}>
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>
@@ -966,6 +968,22 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.xs,
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: {
color: theme.colors.mutedForeground,
fontSize: theme.fontSize.base,
+43 -13
View File
@@ -81,19 +81,23 @@ export default function GitDiffScreen() {
);
}
const routeServerId = resolvedServerId ?? session.serverId;
return <GitDiffContent session={session} agentId={agentId} routeServerId={routeServerId} />;
return (
<GitDiffContent
session={session}
agentId={agentId}
serverLabel={serverLabel}
/>
);
}
function GitDiffContent({
session,
agentId,
routeServerId,
serverLabel,
}: {
session: SessionContextValue;
agentId?: string;
routeServerId: string;
serverLabel: string;
}) {
const { agents, gitDiffs, requestGitDiff } = session;
const [isLoading, setIsLoading] = useState(true);
@@ -127,7 +131,7 @@ function GitDiffContent({
return (
<View style={styles.container}>
<BackHeader title="Changes" />
<Text style={styles.metaText}>Server: {routeServerId}</Text>
<Text style={styles.metaText}>Host: {serverLabel}</Text>
<View style={styles.errorContainer}>
<Text style={styles.errorText}>Agent not found</Text>
</View>
@@ -142,7 +146,7 @@ function GitDiffContent({
return (
<View style={styles.container}>
<BackHeader title="Changes" />
<Text style={styles.metaText}>Server: {routeServerId}</Text>
<Text style={styles.metaText}>Host: {serverLabel}</Text>
<ScrollView style={styles.scrollView} contentContainerStyle={styles.contentContainer}>
{isLoading ? (
@@ -216,8 +220,8 @@ function SessionUnavailableState({
return (
<View style={styles.container}>
<BackHeader title="Changes" />
<Text style={styles.metaText}>Server: {serverLabel}</Text>
<View style={styles.errorContainer}>
<Text style={styles.metaText}>Host: {serverLabel}</Text>
<View style={styles.sessionStateContainer}>
{isConnecting ? (
<>
<ActivityIndicator size="large" />
@@ -226,11 +230,13 @@ function SessionUnavailableState({
</>
) : (
<>
<Text style={styles.errorText}>
Cannot load changes while {serverLabel} is {connectionStatusLabel.toLowerCase()}.
<Text style={styles.offlineTitle}>
{serverLabel} is currently {connectionStatusLabel.toLowerCase()}.
</Text>
<Text style={styles.statusText}>Connect this daemon or switch to another one to continue.</Text>
{lastError ? <Text style={styles.errorDetails}>{lastError}</Text> : null}
<Text style={styles.offlineDescription}>
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>
@@ -243,6 +249,14 @@ const styles = StyleSheet.create((theme) => ({
flex: 1,
backgroundColor: theme.colors.background,
},
sessionStateContainer: {
flex: 1,
alignItems: "center",
justifyContent: "center",
paddingTop: theme.spacing[16],
paddingHorizontal: theme.spacing[6],
gap: theme.spacing[3],
},
metaText: {
paddingHorizontal: theme.spacing[6],
marginBottom: theme.spacing[2],
@@ -291,6 +305,22 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.xs,
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: {
flex: 1,
alignItems: "center",
+4 -1
View File
@@ -849,7 +849,10 @@ function DaemonCard({
}
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;
}
+1 -1
View File
@@ -146,7 +146,7 @@ export function AgentList({ agentGroups }: AgentListProps) {
</Text>
<Text style={styles.sheetSubtitle}>
{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."}
</Text>
<Pressable
@@ -336,7 +336,7 @@ function AgentFlowModal({
const daemonAvailabilityError = !hasSelectedDaemon
? "Select a host before creating or importing agents."
: 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}` : ""
}`
: null;
@@ -963,7 +963,7 @@ function AgentFlowModal({
logOfflineDaemonAction("dictation");
setErrorMessage(
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;
}
@@ -1035,8 +1035,8 @@ function AgentFlowModal({
setIsImportLoading(false);
logOfflineDaemonAction("import_list");
setImportError(
daemonAvailabilityError ??
"Connect to the selected host to load import candidates."
daemonAvailabilityError ??
"Import candidates load automatically once the selected host is back online."
);
return;
}
@@ -1288,7 +1288,8 @@ function AgentFlowModal({
const trimmedWorkingDir = workingDir.trim();
const shouldInspectRepo = isCreateFlow && isVisible && trimmedWorkingDir.length > 0;
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;
const repoInfoStatus: "idle" | "loading" | "ready" | "error" = !shouldInspectRepo
? "idle"
@@ -1367,7 +1368,7 @@ function AgentFlowModal({
logOfflineDaemonAction("create");
setErrorMessage(
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;
}
@@ -1463,8 +1464,8 @@ function AgentFlowModal({
if (!resumeAgent || !isTargetDaemonReady) {
logOfflineDaemonAction("resume");
setImportError(
daemonAvailabilityError ??
"Connect to the selected host before importing an agent."
daemonAvailabilityError ??
"Importing agents will resume automatically once the selected host is online."
);
return;
}
+13 -2
View File
@@ -57,10 +57,21 @@ export function GlobalFooter() {
const realtimeEligibleHosts = useMemo(() => {
return Array.from(connectionStates.values()).filter((entry) => entry.status === "online");
}, [connectionStates]);
const hasAnyConfiguredHosts = connectionStates.size > 0;
const handleStartRealtime = useCallback(() => {
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;
}
if (realtimeEligibleHosts.length === 1) {
@@ -71,7 +82,7 @@ export function GlobalFooter() {
return;
}
setShowRealtimeHostPicker(true);
}, [realtimeEligibleHosts, startRealtime]);
}, [hasAnyConfiguredHosts, realtimeEligibleHosts, startRealtime]);
const handleSelectRealtimeHost = useCallback(
(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
- [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`.
- [ ] 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.
### 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.
### 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.
- [ ] Each agent row displays its host name as metadata (badge, subtitle, etc.).
- [ ] Sort agents by recent activity or alphabetically (not by host).
### 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`).