fix(app): stabilize hook order in agent archive flows

This commit is contained in:
Mohamed Boudra
2026-02-22 12:37:26 +07:00
parent 74a9a3c33b
commit 55b450c8b9
4 changed files with 116 additions and 96 deletions
+78 -62
View File
@@ -9,7 +9,7 @@ import {
type SectionListRenderItem,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useCallback, useMemo, useRef, useState, type ReactElement } from "react";
import { useCallback, useMemo, useState, type ReactElement } from "react";
import { router, usePathname } from "expo-router";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { useQueryClient } from "@tanstack/react-query";
@@ -81,6 +81,71 @@ function deriveDateSectionLabel(lastActivityAt: Date): string {
return "Older";
}
interface AgentListRowProps {
agent: AggregatedAgent;
selectedAgentId?: string;
showCheckoutInfo: boolean;
onPress: (serverId: string, agentId: string) => void;
onLongPress: (agent: AggregatedAgent) => void;
}
function AgentListRow({
agent,
selectedAgentId,
showCheckoutInfo,
onPress,
onLongPress,
}: AgentListRowProps) {
const timeAgo = formatTimeAgo(agent.lastActivityAt);
const agentKey = `${agent.serverId}:${agent.id}`;
const isSelected = selectedAgentId === agentKey;
const checkoutQuery = useCheckoutStatusCacheOnly({
serverId: agent.serverId,
cwd: agent.cwd,
});
const checkout = checkoutQuery.data ?? null;
const projectPath = showCheckoutInfo
? deriveProjectPath(agent.cwd, checkout)
: agent.cwd;
const branchLabel = showCheckoutInfo ? deriveBranchLabel(checkout) : null;
return (
<Pressable
style={({ pressed, hovered }) => [
styles.agentItem,
isSelected && styles.agentItemSelected,
hovered && styles.agentItemHovered,
pressed && styles.agentItemPressed,
]}
onPress={() => onPress(agent.serverId, agent.id)}
onLongPress={() => onLongPress(agent)}
testID={`agent-row-${agent.serverId}-${agent.id}`}
>
{({ hovered }) => (
<View style={styles.agentContent}>
<View style={styles.row}>
<AgentStatusDot status={agent.status} requiresAttention={agent.requiresAttention} />
<Text
style={[
styles.agentTitle,
(isSelected || hovered) && styles.agentTitleHighlighted,
]}
numberOfLines={1}
>
{agent.title || "New agent"}
</Text>
</View>
<Text style={styles.secondaryRow} numberOfLines={1}>
{shortenPath(projectPath)}
{branchLabel ? ` · ${branchLabel}` : ""} · {timeAgo}
</Text>
</View>
)}
</Pressable>
);
}
export function AgentList({
agents,
showCheckoutInfo = true,
@@ -187,66 +252,6 @@ export function AgentList({
[queryClient, showCheckoutInfo]
);
const AgentListRow = useCallback(
({ agent }: { agent: AggregatedAgent }) => {
const timeAgo = formatTimeAgo(agent.lastActivityAt);
const agentKey = `${agent.serverId}:${agent.id}`;
const isSelected = selectedAgentId === agentKey;
const checkoutQuery = useCheckoutStatusCacheOnly({
serverId: agent.serverId,
cwd: agent.cwd,
});
const checkout = checkoutQuery.data ?? null;
const projectPath = showCheckoutInfo
? deriveProjectPath(agent.cwd, checkout)
: agent.cwd;
const branchLabel = showCheckoutInfo ? deriveBranchLabel(checkout) : null;
return (
<Pressable
style={({ pressed, hovered }) => [
styles.agentItem,
isSelected && styles.agentItemSelected,
hovered && styles.agentItemHovered,
pressed && styles.agentItemPressed,
]}
onPress={() => handleAgentPress(agent.serverId, agent.id)}
onLongPress={() => handleAgentLongPress(agent)}
testID={`agent-row-${agent.serverId}-${agent.id}`}
>
{({ hovered }) => (
<View style={styles.agentContent}>
<View style={styles.row}>
<AgentStatusDot status={agent.status} requiresAttention={agent.requiresAttention} />
<Text
style={[
styles.agentTitle,
(isSelected || hovered) && styles.agentTitleHighlighted,
]}
numberOfLines={1}
>
{agent.title || "New agent"}
</Text>
</View>
<Text style={styles.secondaryRow} numberOfLines={1}>
{shortenPath(projectPath)}
{branchLabel ? ` · ${branchLabel}` : ""} · {timeAgo}
</Text>
</View>
)}
</Pressable>
);
},
[
handleAgentLongPress,
handleAgentPress,
selectedAgentId,
showCheckoutInfo,
]
);
const sections = useMemo((): AgentListSection[] => {
const order = ["Today", "Yesterday", "This week", "This month", "Older"] as const;
const buckets = new Map<string, AggregatedAgent[]>();
@@ -269,7 +274,18 @@ export function AgentList({
}, [agents]);
const renderAgentItem: SectionListRenderItem<AggregatedAgent, AgentListSection> =
useCallback(({ item: agent }) => <AgentListRow agent={agent} />, [AgentListRow]);
useCallback(
({ item: agent }) => (
<AgentListRow
agent={agent}
selectedAgentId={selectedAgentId}
showCheckoutInfo={showCheckoutInfo}
onPress={handleAgentPress}
onLongPress={handleAgentLongPress}
/>
),
[handleAgentLongPress, handleAgentPress, selectedAgentId, showCheckoutInfo]
);
const renderSectionHeader = useCallback(
({ section }: { section: AgentListSection }) => (
@@ -38,17 +38,19 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
const client = useSessionStore((state) => state.sessions[serverId]?.client ?? null);
if (!agent) {
return null;
}
const canFetchModels = Boolean(client) && Boolean(agent.provider) && (IS_WEB || prefsOpen);
const canFetchModels =
Boolean(client) && Boolean(agent?.provider) && (IS_WEB || prefsOpen);
const modelsQuery = useQuery({
queryKey: ["providerModels", serverId, agent.provider, agent.cwd],
queryKey: [
"providerModels",
serverId,
agent?.provider ?? "__missing_provider__",
agent?.cwd ?? "__missing_cwd__",
],
enabled: canFetchModels,
staleTime: 5 * 60 * 1000,
queryFn: async () => {
if (!client) {
if (!client || !agent) {
throw new Error("Daemon client unavailable");
}
const payload = await client.listProviderModels(agent.provider, { cwd: agent.cwd });
@@ -61,7 +63,7 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
const models = modelsQuery.data ?? null;
function handleModeChange(modeId: string) {
if (!client) {
if (!client || !agent) {
return;
}
void client.setAgentMode(agentId, modeId).catch((error) => {
@@ -69,8 +71,8 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
});
}
const normalizedRuntimeModelId = normalizeModelId(agent.runtimeInfo?.model);
const normalizedConfiguredModelId = normalizeModelId(agent.model);
const normalizedRuntimeModelId = normalizeModelId(agent?.runtimeInfo?.model);
const normalizedConfiguredModelId = normalizeModelId(agent?.model);
const preferredModelId = normalizedRuntimeModelId ?? normalizedConfiguredModelId;
const selectedModel = useMemo(() => {
if (!models || !preferredModelId) return null;
@@ -82,7 +84,7 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
const thinkingOptions = selectedModel?.thinkingOptions ?? null;
const explicitThinkingId =
agent.thinkingOptionId && agent.thinkingOptionId !== "default"
agent?.thinkingOptionId && agent.thinkingOptionId !== "default"
? agent.thinkingOptionId
: null;
const selectedThinkingId =
@@ -93,10 +95,14 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
(selectedThinkingId === "default" ? "Model default" : selectedThinkingId ?? "auto");
const displayMode =
agent.availableModes?.find((m) => m.id === agent.currentModeId)?.label ||
agent.currentModeId ||
agent?.availableModes?.find((m) => m.id === agent.currentModeId)?.label ||
agent?.currentModeId ||
"default";
if (!agent) {
return null;
}
return (
<View style={[styles.container, IS_WEB && { marginBottom: -theme.spacing[1] }]}>
{/* Agent Mode Badge (desktop only — on mobile, mode is in the preferences sheet) */}
@@ -597,14 +597,6 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
});
}, [agentId, errorRecoveryPath, requestDirectoryListing, selectExplorerEntry]);
if (!agentExists) {
return (
<View style={styles.centerState}>
<Text style={styles.errorText}>Agent not found</Text>
</View>
);
}
const handleTreeListScroll = useCallback(
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
if (showDesktopWebScrollbar) {
@@ -623,6 +615,14 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
[showDesktopWebScrollbar, treeScrollbarMetrics]
);
if (!agentExists) {
return (
<View style={styles.centerState}>
<Text style={styles.errorText}>Agent not found</Text>
</View>
);
}
return (
<View
style={styles.container}
+11 -13
View File
@@ -24,19 +24,17 @@ export function useSessionForServer<T>(
selector?: SessionSelector<T>,
equalityFn?: EqualityFn<T>
): SessionState | null | T {
const baseSelector = useCallback(
(state: ReturnType<typeof useSessionStore.getState>) =>
(serverId ? state.sessions[serverId] ?? null : null),
[serverId]
const storeSelector = useCallback(
(state: ReturnType<typeof useSessionStore.getState>): SessionState | null | T => {
const session = serverId ? state.sessions[serverId] ?? null : null;
return selector ? selector(session) : session;
},
[selector, serverId]
);
if (selector) {
const derivedSelector = useCallback(
(state: ReturnType<typeof useSessionStore.getState>) => selector(baseSelector(state)),
[baseSelector, selector]
);
return useStoreWithEqualityFn(useSessionStore, derivedSelector, equalityFn);
}
return useSessionStore(baseSelector);
return useStoreWithEqualityFn(
useSessionStore,
storeSelector,
equalityFn as EqualityFn<SessionState | null | T>
);
}