Clarify automatic host reconnection
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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`).
|
||||
|
||||
Reference in New Issue
Block a user