feat: implement daemon connectivity updates and relay protocol versioning

This commit is contained in:
Mohamed Boudra
2026-02-20 19:27:56 +07:00
parent 6dad3212da
commit 0350f00840
49 changed files with 4146 additions and 1351 deletions
@@ -76,9 +76,10 @@ import {
derivePendingPermissionKey,
normalizeAgentSnapshot,
} from "@/utils/agent-snapshots";
import { resolveProjectPlacement } from "@/utils/project-placement";
import { mergePendingCreateImages } from "@/utils/pending-create-images";
import { shouldClearAgentAttentionOnView } from "@/utils/agent-attention";
import type { DaemonClient, FetchAgentsEntry } from "@server/client/daemon-client";
import type { DaemonClient } from "@server/client/daemon-client";
import { useExplorerOpenGesture } from "@/hooks/use-explorer-open-gesture";
import {
DropdownMenu,
@@ -128,7 +129,7 @@ export function AgentReadyScreen({
const isUnknownDaemon = Boolean(connectionServerId && !connection);
const connectionStatus: HostRuntimeConnectionStatus =
runtimeSnapshot?.connectionStatus ??
(isUnknownDaemon ? "offline" : "idle");
(isUnknownDaemon ? "offline" : "connecting");
const lastConnectionError = runtimeSnapshot?.lastError ?? null;
const isRuntimeSessionAvailable = Boolean(resolvedServerId && runtimeClient);
@@ -278,7 +279,6 @@ function AgentScreenContent({
const checkout = checkoutStatusQuery.status;
const resolveCachedCheckoutIsGit = useCallback(
(params: {
agentId?: string | null;
cwd?: string | null;
projectPlacementIsGit?: boolean;
checkoutStatusIsGit?: boolean;
@@ -287,19 +287,6 @@ function AgentScreenContent({
return params.projectPlacementIsGit;
}
const agentId = params.agentId?.trim();
if (agentId) {
const sidebarAgents = queryClient.getQueryData<{
entries: FetchAgentsEntry[];
}>(["sidebarAgentsList", serverId]);
const sidebarIsGit = sidebarAgents?.entries.find(
(entry) => entry.agent.id === agentId
)?.project?.checkout?.isGit;
if (typeof sidebarIsGit === "boolean") {
return sidebarIsGit;
}
}
const cwd = params.cwd?.trim();
if (!cwd) {
return null;
@@ -327,7 +314,6 @@ function AgentScreenContent({
?.agents?.get(resolvedAgentId);
const cwd = currentAgent?.cwd?.trim();
const isGit = resolveCachedCheckoutIsGit({
agentId: resolvedAgentId,
cwd,
projectPlacementIsGit: currentAgent?.projectPlacement?.checkout?.isGit,
checkoutStatusIsGit: checkout?.isGit,
@@ -394,7 +380,6 @@ function AgentScreenContent({
const activeExplorerCheckout = useMemo<ExplorerCheckoutContext | null>(() => {
const cwd = agent?.cwd?.trim();
const isGit = resolveCachedCheckoutIsGit({
agentId: resolvedAgentId,
cwd,
projectPlacementIsGit: agent?.projectPlacement?.checkout?.isGit,
checkoutStatusIsGit: checkout?.isGit,
@@ -458,6 +443,7 @@ function AgentScreenContent({
? state.sessions[serverId]?.agentHistorySyncGeneration?.get(resolvedAgentId) ?? -1
: -1
);
const hasHydratedHistoryBefore = agentHistorySyncGeneration >= 0;
// Select raw pending permissions - filter with useMemo to avoid new Map on every render
const allPendingPermissions = useSessionStore(
@@ -517,6 +503,38 @@ function AgentScreenContent({
};
});
const handleHistorySyncFailure = useCallback(
({ origin, error }: { origin: "focus" | "entry"; error: unknown }) => {
if (resolvedAgentId) {
console.warn("[AgentScreen] history sync failed", {
origin,
agentId: resolvedAgentId,
error,
});
}
const message = toErrorMessage(error);
setMissingAgentState((prev) => {
if (prev.kind === "error" && prev.message === message) {
return prev;
}
return { kind: "error", message };
});
},
[resolvedAgentId]
);
const ensureInitializedWithSyncErrorHandling = useCallback(
(origin: "focus" | "entry") => {
if (!resolvedAgentId) {
return;
}
ensureAgentIsInitialized(resolvedAgentId).catch((error) => {
handleHistorySyncFailure({ origin, error });
});
},
[ensureAgentIsInitialized, handleHistorySyncFailure, resolvedAgentId]
);
useEffect(() => {
if (connectionStatus === "online") {
reconnectToastArmedRef.current = false;
@@ -534,25 +552,18 @@ function AgentScreenContent({
}
}, [connectionStatus, toast]);
useEffect(() => {
if (missingAgentState.kind !== "error") {
return;
}
toast.error("Failed to refresh agent. Retrying in background.");
}, [missingAgentState.kind, toast]);
useFocusEffect(
useCallback(() => {
if (!resolvedAgentId || !isConnected || !hasSession) {
return;
}
ensureAgentIsInitialized(resolvedAgentId).catch((error) => {
console.warn("[AgentScreen] focus sync failed", {
agentId: resolvedAgentId,
error,
});
});
}, [ensureAgentIsInitialized, hasSession, isConnected, resolvedAgentId])
ensureInitializedWithSyncErrorHandling("focus");
}, [
ensureInitializedWithSyncErrorHandling,
hasSession,
isConnected,
resolvedAgentId,
])
);
const isGitCheckout = activeExplorerCheckout?.isGit ?? false;
@@ -688,6 +699,7 @@ function AgentScreenContent({
isHistorySyncing,
needsAuthoritativeSync,
shouldUseOptimisticStream,
hasHydratedHistoryBefore,
},
});
@@ -769,15 +781,10 @@ function AgentScreenContent({
return;
}
ensureAgentIsInitialized(resolvedAgentId).catch((error) => {
console.warn("[AgentScreen] Agent initialization failed", {
agentId: resolvedAgentId,
error,
});
});
ensureInitializedWithSyncErrorHandling("entry");
}, [
resolvedAgentId,
ensureAgentIsInitialized,
ensureInitializedWithSyncErrorHandling,
hasSession,
isConnected,
needsAuthoritativeSync,
@@ -831,21 +838,28 @@ function AgentScreenContent({
return;
}
const normalized = normalizeAgentSnapshot(snapshot, serverId);
const hydrated = {
...normalized,
projectPlacement: resolveProjectPlacement({
projectPlacement: null,
cwd: normalized.cwd,
}),
};
setAgents(serverId, (prev) => {
const next = new Map(prev);
next.set(normalized.id, normalized);
next.set(hydrated.id, hydrated);
return next;
});
setPendingPermissions(serverId, (prev) => {
const next = new Map(prev);
for (const [key, pending] of next.entries()) {
if (pending.agentId === normalized.id) {
if (pending.agentId === hydrated.id) {
next.delete(key);
}
}
for (const request of normalized.pendingPermissions) {
const key = derivePendingPermissionKey(normalized.id, request);
next.set(key, { key, agentId: normalized.id, request });
for (const request of hydrated.pendingPermissions) {
const key = derivePendingPermissionKey(hydrated.id, request);
next.set(key, { key, agentId: hydrated.id, request });
}
return next;
});
@@ -940,14 +954,31 @@ function AgentScreenContent({
[theme.colors.primary, toast]
);
const syncBannerMessage =
viewState.tag !== "ready"
? null
: viewState.syncStatus === "catching_up"
? "Refreshing agent history..."
: viewState.syncStatus === "sync_error"
? "Refresh delayed. Retrying in background."
: null;
const shouldEmitHistoryRefreshToast =
viewState.tag === "ready" &&
viewState.sync.status === "catching_up" &&
viewState.sync.shouldEmitHistoryRefreshToast;
const shouldEmitSyncErrorToast =
viewState.tag === "ready" &&
viewState.sync.status === "sync_error" &&
viewState.sync.shouldEmitSyncErrorToast;
useEffect(() => {
if (!shouldEmitHistoryRefreshToast) {
return;
}
toast.show("Refreshing agent history...", {
durationMs: 2200,
testID: "agent-history-refresh-toast",
});
}, [shouldEmitHistoryRefreshToast, toast]);
useEffect(() => {
if (!shouldEmitSyncErrorToast) {
return;
}
toast.error("Failed to refresh agent. Retrying in background.");
}, [shouldEmitSyncErrorToast, toast]);
if (viewState.tag === "not_found") {
return (
@@ -1188,12 +1219,6 @@ function AgentScreenContent({
}
/>
{syncBannerMessage ? (
<View style={styles.syncBanner} testID="agent-sync-banner">
<Text style={styles.syncBannerText}>{syncBannerMessage}</Text>
</View>
) : null}
{/* Content Area with Keyboard Animation */}
<View style={styles.contentContainer}>
<ReanimatedAnimated.View
@@ -1221,6 +1246,14 @@ function AgentScreenContent({
/>
)}
{viewState.tag === "ready" &&
viewState.sync.status === "catching_up" &&
viewState.sync.ui === "overlay" ? (
<View style={styles.historySyncOverlay} testID="agent-history-overlay">
<ActivityIndicator size="large" color={theme.colors.foregroundMuted} />
</View>
) : null}
</View>
</FileDropZone>
@@ -1357,24 +1390,20 @@ const styles = StyleSheet.create((theme) => ({
flex: 1,
overflow: "hidden",
},
syncBanner: {
marginHorizontal: theme.spacing[4],
marginTop: theme.spacing[1],
marginBottom: theme.spacing[2],
borderRadius: theme.borderRadius.full,
paddingVertical: theme.spacing[2],
paddingHorizontal: theme.spacing[3],
borderWidth: theme.borderWidth[1],
borderColor: theme.colors.border,
backgroundColor: theme.colors.surface1,
},
syncBannerText: {
fontSize: theme.fontSize.sm,
color: theme.colors.foregroundMuted,
},
content: {
flex: 1,
},
historySyncOverlay: {
position: "absolute",
top: 0,
right: 0,
bottom: 0,
left: 0,
backgroundColor: theme.colors.surface0,
alignItems: "center",
justifyContent: "center",
zIndex: 40,
},
archivingOverlay: {
position: "absolute",
top: 0,