From 0db0e1ad23ed32159f006ef659a921ca750efb4e Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 26 Nov 2025 19:33:43 +0100 Subject: [PATCH] Clarify automatic host reconnection --- packages/app/src/app/agent/[id].tsx | 8 +-- .../src/app/agent/[serverId]/[agentId].tsx | 26 +++++++-- packages/app/src/app/file-explorer.tsx | 28 ++++++++-- packages/app/src/app/git-diff.tsx | 56 ++++++++++++++----- packages/app/src/app/settings.tsx | 5 +- packages/app/src/components/agent-list.tsx | 2 +- .../app/src/components/create-agent-modal.tsx | 17 +++--- packages/app/src/components/global-footer.tsx | 15 ++++- plan.md | 7 ++- 9 files changed, 123 insertions(+), 41 deletions(-) diff --git a/packages/app/src/app/agent/[id].tsx b/packages/app/src/app/agent/[id].tsx index 31b53e49d..cc07a6706 100644 --- a/packages/app/src/app/agent/[id].tsx +++ b/packages/app/src/app/agent/[id].tsx @@ -105,8 +105,8 @@ export default function LegacyAgentRedirectScreen() { Agent not found - 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. Go Home @@ -116,9 +116,9 @@ export default function LegacyAgentRedirectScreen() { } else if (matches.length > 1) { body = ( - Pick a daemon + Pick a host - 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. {matches.map((match) => ( diff --git a/packages/app/src/app/agent/[serverId]/[agentId].tsx b/packages/app/src/app/agent/[serverId]/[agentId].tsx index 05b8fd10a..edd0ba7ec 100644 --- a/packages/app/src/app/agent/[serverId]/[agentId].tsx +++ b/packages/app/src/app/agent/[serverId]/[agentId].tsx @@ -653,13 +653,13 @@ function AgentSessionUnavailableState({ ) : ( <> - - Cannot open this agent while {serverLabel} is {connectionStatusLabel.toLowerCase()}. + + {serverLabel} is currently {connectionStatusLabel.toLowerCase()}. - - 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. - {lastError ? {lastError} : null} + {lastError ? {lastError} : null} )} @@ -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, diff --git a/packages/app/src/app/file-explorer.tsx b/packages/app/src/app/file-explorer.tsx index f14b685c7..0a6c6cde0 100644 --- a/packages/app/src/app/file-explorer.tsx +++ b/packages/app/src/app/file-explorer.tsx @@ -688,15 +688,17 @@ function FileExplorerSessionUnavailable({ <> Connecting to {serverLabel}... - We will load files once this daemon is online. + We will load files once this host is online. ) : ( <> - - Cannot open files while {serverLabel} is {connectionStatusLabel.toLowerCase()}. + + {serverLabel} is currently {connectionStatusLabel.toLowerCase()}. - Connect this daemon and try again. - {lastError ? {lastError} : null} + + We'll reconnect automatically and load files once the host is back online. No action needed. + + {lastError ? {lastError} : null} )} @@ -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, diff --git a/packages/app/src/app/git-diff.tsx b/packages/app/src/app/git-diff.tsx index 5dc4484b2..06cec3c88 100644 --- a/packages/app/src/app/git-diff.tsx +++ b/packages/app/src/app/git-diff.tsx @@ -81,19 +81,23 @@ export default function GitDiffScreen() { ); } - const routeServerId = resolvedServerId ?? session.serverId; - - return ; + return ( + + ); } 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 ( - Server: {routeServerId} + Host: {serverLabel} Agent not found @@ -142,7 +146,7 @@ function GitDiffContent({ return ( - Server: {routeServerId} + Host: {serverLabel} {isLoading ? ( @@ -216,8 +220,8 @@ function SessionUnavailableState({ return ( - Server: {serverLabel} - + Host: {serverLabel} + {isConnecting ? ( <> @@ -226,11 +230,13 @@ function SessionUnavailableState({ ) : ( <> - - Cannot load changes while {serverLabel} is {connectionStatusLabel.toLowerCase()}. + + {serverLabel} is currently {connectionStatusLabel.toLowerCase()}. - Connect this daemon or switch to another one to continue. - {lastError ? {lastError} : null} + + We'll reconnect automatically and show changes once the host is back online. No action needed. + + {lastError ? {lastError} : null} )} @@ -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", diff --git a/packages/app/src/app/settings.tsx b/packages/app/src/app/settings.tsx index 9ae4dea1b..f1ede72ee 100644 --- a/packages/app/src/app/settings.tsx +++ b/packages/app/src/app/settings.tsx @@ -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; } diff --git a/packages/app/src/components/agent-list.tsx b/packages/app/src/components/agent-list.tsx index a4adc75b8..e10853daa 100644 --- a/packages/app/src/components/agent-list.tsx +++ b/packages/app/src/components/agent-list.tsx @@ -146,7 +146,7 @@ export function AgentList({ agentGroups }: AgentListProps) { {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."} 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; } diff --git a/packages/app/src/components/global-footer.tsx b/packages/app/src/components/global-footer.tsx index 61ec0221e..0f1b66102 100644 --- a/packages/app/src/components/global-footer.tsx +++ b/packages/app/src/components/global-footer.tsx @@ -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) => { diff --git a/plan.md b/plan.md index 9dfee3e6f..99c1abbdf 100644 --- a/plan.md +++ b/plan.md @@ -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`).