fix(app): stabilize hook order in agent archive flows
This commit is contained in:
@@ -9,7 +9,7 @@ import {
|
|||||||
type SectionListRenderItem,
|
type SectionListRenderItem,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
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 { router, usePathname } from "expo-router";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
@@ -81,6 +81,71 @@ function deriveDateSectionLabel(lastActivityAt: Date): string {
|
|||||||
return "Older";
|
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({
|
export function AgentList({
|
||||||
agents,
|
agents,
|
||||||
showCheckoutInfo = true,
|
showCheckoutInfo = true,
|
||||||
@@ -187,66 +252,6 @@ export function AgentList({
|
|||||||
[queryClient, showCheckoutInfo]
|
[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 sections = useMemo((): AgentListSection[] => {
|
||||||
const order = ["Today", "Yesterday", "This week", "This month", "Older"] as const;
|
const order = ["Today", "Yesterday", "This week", "This month", "Older"] as const;
|
||||||
const buckets = new Map<string, AggregatedAgent[]>();
|
const buckets = new Map<string, AggregatedAgent[]>();
|
||||||
@@ -269,7 +274,18 @@ export function AgentList({
|
|||||||
}, [agents]);
|
}, [agents]);
|
||||||
|
|
||||||
const renderAgentItem: SectionListRenderItem<AggregatedAgent, AgentListSection> =
|
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(
|
const renderSectionHeader = useCallback(
|
||||||
({ section }: { section: AgentListSection }) => (
|
({ section }: { section: AgentListSection }) => (
|
||||||
|
|||||||
@@ -38,17 +38,19 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
|
|||||||
|
|
||||||
const client = useSessionStore((state) => state.sessions[serverId]?.client ?? null);
|
const client = useSessionStore((state) => state.sessions[serverId]?.client ?? null);
|
||||||
|
|
||||||
if (!agent) {
|
const canFetchModels =
|
||||||
return null;
|
Boolean(client) && Boolean(agent?.provider) && (IS_WEB || prefsOpen);
|
||||||
}
|
|
||||||
|
|
||||||
const canFetchModels = Boolean(client) && Boolean(agent.provider) && (IS_WEB || prefsOpen);
|
|
||||||
const modelsQuery = useQuery({
|
const modelsQuery = useQuery({
|
||||||
queryKey: ["providerModels", serverId, agent.provider, agent.cwd],
|
queryKey: [
|
||||||
|
"providerModels",
|
||||||
|
serverId,
|
||||||
|
agent?.provider ?? "__missing_provider__",
|
||||||
|
agent?.cwd ?? "__missing_cwd__",
|
||||||
|
],
|
||||||
enabled: canFetchModels,
|
enabled: canFetchModels,
|
||||||
staleTime: 5 * 60 * 1000,
|
staleTime: 5 * 60 * 1000,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!client) {
|
if (!client || !agent) {
|
||||||
throw new Error("Daemon client unavailable");
|
throw new Error("Daemon client unavailable");
|
||||||
}
|
}
|
||||||
const payload = await client.listProviderModels(agent.provider, { cwd: agent.cwd });
|
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;
|
const models = modelsQuery.data ?? null;
|
||||||
|
|
||||||
function handleModeChange(modeId: string) {
|
function handleModeChange(modeId: string) {
|
||||||
if (!client) {
|
if (!client || !agent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
void client.setAgentMode(agentId, modeId).catch((error) => {
|
void client.setAgentMode(agentId, modeId).catch((error) => {
|
||||||
@@ -69,8 +71,8 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedRuntimeModelId = normalizeModelId(agent.runtimeInfo?.model);
|
const normalizedRuntimeModelId = normalizeModelId(agent?.runtimeInfo?.model);
|
||||||
const normalizedConfiguredModelId = normalizeModelId(agent.model);
|
const normalizedConfiguredModelId = normalizeModelId(agent?.model);
|
||||||
const preferredModelId = normalizedRuntimeModelId ?? normalizedConfiguredModelId;
|
const preferredModelId = normalizedRuntimeModelId ?? normalizedConfiguredModelId;
|
||||||
const selectedModel = useMemo(() => {
|
const selectedModel = useMemo(() => {
|
||||||
if (!models || !preferredModelId) return null;
|
if (!models || !preferredModelId) return null;
|
||||||
@@ -82,7 +84,7 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
|
|||||||
|
|
||||||
const thinkingOptions = selectedModel?.thinkingOptions ?? null;
|
const thinkingOptions = selectedModel?.thinkingOptions ?? null;
|
||||||
const explicitThinkingId =
|
const explicitThinkingId =
|
||||||
agent.thinkingOptionId && agent.thinkingOptionId !== "default"
|
agent?.thinkingOptionId && agent.thinkingOptionId !== "default"
|
||||||
? agent.thinkingOptionId
|
? agent.thinkingOptionId
|
||||||
: null;
|
: null;
|
||||||
const selectedThinkingId =
|
const selectedThinkingId =
|
||||||
@@ -93,10 +95,14 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
|
|||||||
(selectedThinkingId === "default" ? "Model default" : selectedThinkingId ?? "auto");
|
(selectedThinkingId === "default" ? "Model default" : selectedThinkingId ?? "auto");
|
||||||
|
|
||||||
const displayMode =
|
const displayMode =
|
||||||
agent.availableModes?.find((m) => m.id === agent.currentModeId)?.label ||
|
agent?.availableModes?.find((m) => m.id === agent.currentModeId)?.label ||
|
||||||
agent.currentModeId ||
|
agent?.currentModeId ||
|
||||||
"default";
|
"default";
|
||||||
|
|
||||||
|
if (!agent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, IS_WEB && { marginBottom: -theme.spacing[1] }]}>
|
<View style={[styles.container, IS_WEB && { marginBottom: -theme.spacing[1] }]}>
|
||||||
{/* Agent Mode Badge (desktop only — on mobile, mode is in the preferences sheet) */}
|
{/* 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]);
|
}, [agentId, errorRecoveryPath, requestDirectoryListing, selectExplorerEntry]);
|
||||||
|
|
||||||
if (!agentExists) {
|
|
||||||
return (
|
|
||||||
<View style={styles.centerState}>
|
|
||||||
<Text style={styles.errorText}>Agent not found</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleTreeListScroll = useCallback(
|
const handleTreeListScroll = useCallback(
|
||||||
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||||
if (showDesktopWebScrollbar) {
|
if (showDesktopWebScrollbar) {
|
||||||
@@ -623,6 +615,14 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
|
|||||||
[showDesktopWebScrollbar, treeScrollbarMetrics]
|
[showDesktopWebScrollbar, treeScrollbarMetrics]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!agentExists) {
|
||||||
|
return (
|
||||||
|
<View style={styles.centerState}>
|
||||||
|
<Text style={styles.errorText}>Agent not found</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={styles.container}
|
style={styles.container}
|
||||||
|
|||||||
@@ -24,19 +24,17 @@ export function useSessionForServer<T>(
|
|||||||
selector?: SessionSelector<T>,
|
selector?: SessionSelector<T>,
|
||||||
equalityFn?: EqualityFn<T>
|
equalityFn?: EqualityFn<T>
|
||||||
): SessionState | null | T {
|
): SessionState | null | T {
|
||||||
const baseSelector = useCallback(
|
const storeSelector = useCallback(
|
||||||
(state: ReturnType<typeof useSessionStore.getState>) =>
|
(state: ReturnType<typeof useSessionStore.getState>): SessionState | null | T => {
|
||||||
(serverId ? state.sessions[serverId] ?? null : null),
|
const session = serverId ? state.sessions[serverId] ?? null : null;
|
||||||
[serverId]
|
return selector ? selector(session) : session;
|
||||||
|
},
|
||||||
|
[selector, serverId]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (selector) {
|
return useStoreWithEqualityFn(
|
||||||
const derivedSelector = useCallback(
|
useSessionStore,
|
||||||
(state: ReturnType<typeof useSessionStore.getState>) => selector(baseSelector(state)),
|
storeSelector,
|
||||||
[baseSelector, selector]
|
equalityFn as EqualityFn<SessionState | null | T>
|
||||||
);
|
);
|
||||||
return useStoreWithEqualityFn(useSessionStore, derivedSelector, equalityFn);
|
|
||||||
}
|
|
||||||
|
|
||||||
return useSessionStore(baseSelector);
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user