feat: add live model and thinking option preferences UI (#8)
* feat: add live model and thinking option preferences UI * refactor: remove agent variant support and clean up thinking options * feat: live model + thinking presets (RPC)
This commit is contained in:
@@ -27,6 +27,7 @@ import {
|
||||
import { Theme } from "@/styles/theme";
|
||||
import { CommandAutocomplete } from "./command-autocomplete";
|
||||
import { useAgentCommandsQuery } from "@/hooks/use-agent-commands-query";
|
||||
import { encodeImages } from "@/utils/encode-images";
|
||||
|
||||
type QueuedMessage = {
|
||||
id: string;
|
||||
@@ -85,11 +86,8 @@ export function AgentInputArea({
|
||||
);
|
||||
const queuedMessages = queuedMessagesRaw ?? EMPTY_ARRAY;
|
||||
|
||||
const methods = useSessionStore((state) => state.sessions[serverId]?.methods);
|
||||
const sendAgentMessage = methods?.sendAgentMessage;
|
||||
const cancelAgentRun = methods?.cancelAgentRun;
|
||||
|
||||
const setQueuedMessages = useSessionStore((state) => state.setQueuedMessages);
|
||||
const setAgentStreamTail = useSessionStore((state) => state.setAgentStreamTail);
|
||||
|
||||
const { isVoiceMode, isMuted: isVoiceMuted, toggleMute: toggleVoiceMute } = useVoice();
|
||||
|
||||
@@ -129,7 +127,9 @@ export function AgentInputArea({
|
||||
|
||||
const { pickImages } = useImageAttachmentPicker();
|
||||
const agentIdRef = useRef(agentId);
|
||||
const sendAgentMessageRef = useRef(sendAgentMessage);
|
||||
const sendAgentMessageRef = useRef<
|
||||
((agentId: string, text: string, images?: ImageAttachment[]) => Promise<void>) | null
|
||||
>(null);
|
||||
const onSubmitMessageRef = useRef(onSubmitMessage);
|
||||
const messageInputRef = useRef<MessageInputRef>(null);
|
||||
|
||||
@@ -148,7 +148,10 @@ export function AgentInputArea({
|
||||
await onSubmitMessageRef.current({ text, images });
|
||||
return;
|
||||
}
|
||||
await sendAgentMessageRef.current?.(agentIdRef.current, text, images);
|
||||
if (!sendAgentMessageRef.current) {
|
||||
throw new Error("Host is not connected");
|
||||
}
|
||||
await sendAgentMessageRef.current(agentIdRef.current, text, images);
|
||||
},
|
||||
[]
|
||||
);
|
||||
@@ -158,8 +161,36 @@ export function AgentInputArea({
|
||||
}, [agentId]);
|
||||
|
||||
useEffect(() => {
|
||||
sendAgentMessageRef.current = sendAgentMessage;
|
||||
}, [sendAgentMessage]);
|
||||
sendAgentMessageRef.current = async (
|
||||
agentId: string,
|
||||
text: string,
|
||||
images?: ImageAttachment[]
|
||||
) => {
|
||||
if (!client) {
|
||||
throw new Error("Host is not connected");
|
||||
}
|
||||
|
||||
const messageId = generateMessageId();
|
||||
setAgentStreamTail(serverId, (prev) => {
|
||||
const currentStream = prev.get(agentId) || [];
|
||||
const nextItem: any = {
|
||||
kind: "user_message",
|
||||
id: messageId,
|
||||
text,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
const updated = new Map(prev);
|
||||
updated.set(agentId, [...currentStream, nextItem]);
|
||||
return updated;
|
||||
});
|
||||
|
||||
const imagesData = await encodeImages(images);
|
||||
await client.sendAgentMessage(agentId, text, {
|
||||
messageId,
|
||||
...(imagesData && imagesData.length > 0 ? { images: imagesData } : {}),
|
||||
});
|
||||
};
|
||||
}, [client, serverId, setAgentStreamTail]);
|
||||
|
||||
useEffect(() => {
|
||||
onSubmitMessageRef.current = onSubmitMessage;
|
||||
@@ -213,7 +244,7 @@ export function AgentInputArea({
|
||||
// decide what to do even if the socket is currently disconnected (so we
|
||||
// don't no-op and lose deterministic error handling in the UI/tests).
|
||||
if (!onSubmitMessageRef.current && !socketConnected) return;
|
||||
if (!sendAgentMessage && !onSubmitMessageRef.current) return;
|
||||
if (!sendAgentMessageRef.current && !onSubmitMessageRef.current) return;
|
||||
|
||||
if (agent?.status === "running" && !forceSend) {
|
||||
queueMessage(trimmedMessage, imageAttachments);
|
||||
@@ -334,11 +365,11 @@ export function AgentInputArea({
|
||||
if (!agent || agent.status !== "running" || isCancellingAgent) {
|
||||
return;
|
||||
}
|
||||
if (!isConnected || !cancelAgentRun) {
|
||||
if (!isConnected || !client) {
|
||||
return;
|
||||
}
|
||||
setIsCancellingAgent(true);
|
||||
cancelAgentRun(agentIdRef.current);
|
||||
void client.cancelAgent(agentIdRef.current);
|
||||
messageInputRef.current?.focus();
|
||||
}
|
||||
|
||||
@@ -354,7 +385,7 @@ export function AgentInputArea({
|
||||
async function handleSendQueuedNow(id: string) {
|
||||
const item = queuedMessages.find((q) => q.id === id);
|
||||
if (!item || !isConnected) return;
|
||||
if (!sendAgentMessage && !onSubmitMessageRef.current) return;
|
||||
if (!sendAgentMessageRef.current && !onSubmitMessageRef.current) return;
|
||||
|
||||
updateQueue((current) => current.filter((q) => q.id !== id));
|
||||
|
||||
|
||||
@@ -85,16 +85,12 @@ export function AgentList({
|
||||
const insets = useSafeAreaInsets();
|
||||
const [actionAgent, setActionAgent] = useState<AggregatedAgent | null>(null);
|
||||
|
||||
// Get the methods for the specific server
|
||||
const methods = useSessionStore((state) =>
|
||||
actionAgent?.serverId
|
||||
? state.sessions[actionAgent.serverId]?.methods
|
||||
: undefined
|
||||
const actionClient = useSessionStore((state) =>
|
||||
actionAgent?.serverId ? state.sessions[actionAgent.serverId]?.client ?? null : null
|
||||
);
|
||||
const archiveAgent = methods?.archiveAgent;
|
||||
|
||||
const isActionSheetVisible = actionAgent !== null;
|
||||
const isActionDaemonUnavailable = Boolean(actionAgent?.serverId && !methods);
|
||||
const isActionDaemonUnavailable = Boolean(actionAgent?.serverId && !actionClient);
|
||||
|
||||
const handleAgentPress = useCallback(
|
||||
(serverId: string, agentId: string) => {
|
||||
@@ -134,12 +130,12 @@ export function AgentList({
|
||||
}, []);
|
||||
|
||||
const handleArchiveAgent = useCallback(() => {
|
||||
if (!actionAgent || !archiveAgent) {
|
||||
if (!actionAgent || !actionClient) {
|
||||
return;
|
||||
}
|
||||
archiveAgent(actionAgent.id);
|
||||
void actionClient.archiveAgent(actionAgent.id);
|
||||
setActionAgent(null);
|
||||
}, [actionAgent, archiveAgent]);
|
||||
}, [actionAgent, actionClient]);
|
||||
|
||||
const viewabilityConfig = useMemo(
|
||||
() => ({ itemVisiblePercentThreshold: 30 }),
|
||||
@@ -347,7 +343,7 @@ export function AgentList({
|
||||
<Text style={styles.sheetCancelText}>Cancel</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
disabled={!archiveAgent || isActionDaemonUnavailable}
|
||||
disabled={isActionDaemonUnavailable}
|
||||
style={[styles.sheetButton, styles.sheetArchiveButton]}
|
||||
onPress={handleArchiveAgent}
|
||||
testID="agent-action-archive"
|
||||
@@ -355,8 +351,7 @@ export function AgentList({
|
||||
<Text
|
||||
style={[
|
||||
styles.sheetArchiveText,
|
||||
(!archiveAgent || isActionDaemonUnavailable) &&
|
||||
styles.sheetArchiveTextDisabled,
|
||||
isActionDaemonUnavailable && styles.sheetArchiveTextDisabled,
|
||||
]}
|
||||
>
|
||||
Archive
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { View, Text } from "react-native";
|
||||
import { View, Text, Platform, Pressable } from "react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { ChevronDown } from "lucide-react-native";
|
||||
import { ChevronDown, SlidersHorizontal } from "lucide-react-native";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -9,6 +9,9 @@ import {
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
interface AgentStatusBarProps {
|
||||
agentId: string;
|
||||
@@ -17,27 +20,62 @@ interface AgentStatusBarProps {
|
||||
|
||||
export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const IS_WEB = Platform.OS === "web";
|
||||
const [prefsOpen, setPrefsOpen] = useState(false);
|
||||
|
||||
// Select only the specific agent (not all agents)
|
||||
const agent = useSessionStore((state) =>
|
||||
state.sessions[serverId]?.agents?.get(agentId)
|
||||
);
|
||||
|
||||
// Get the setAgentMode action (actions are stable, won't cause rerenders)
|
||||
const setAgentMode = useSessionStore(
|
||||
(state) => state.sessions[serverId]?.methods?.setAgentMode
|
||||
);
|
||||
const client = useSessionStore((state) => state.sessions[serverId]?.client ?? null);
|
||||
|
||||
if (!agent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const canFetchModels = Boolean(client) && Boolean(agent.provider) && (IS_WEB || prefsOpen);
|
||||
const modelsQuery = useQuery({
|
||||
queryKey: ["providerModels", serverId, agent.provider, agent.cwd],
|
||||
enabled: canFetchModels,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
queryFn: async () => {
|
||||
if (!client) {
|
||||
throw new Error("Daemon client unavailable");
|
||||
}
|
||||
const payload = await client.listProviderModels(agent.provider, { cwd: agent.cwd });
|
||||
if (payload.error) {
|
||||
throw new Error(payload.error);
|
||||
}
|
||||
return payload.models ?? [];
|
||||
},
|
||||
});
|
||||
const models = modelsQuery.data ?? null;
|
||||
|
||||
function handleModeChange(modeId: string) {
|
||||
if (setAgentMode) {
|
||||
setAgentMode(agentId, modeId);
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
void client.setAgentMode(agentId, modeId).catch((error) => {
|
||||
console.warn("[AgentStatusBar] setAgentMode failed", error);
|
||||
});
|
||||
}
|
||||
|
||||
const selectedModel = useMemo(() => {
|
||||
if (!models || !agent.model) return null;
|
||||
return models.find((m) => m.id === agent.model) ?? null;
|
||||
}, [models, agent.model]);
|
||||
|
||||
const displayModel = selectedModel?.label ?? agent.model ?? "default";
|
||||
|
||||
const thinkingOptions = selectedModel?.thinkingOptions ?? null;
|
||||
const selectedThinkingId =
|
||||
agent.thinkingOptionId ??
|
||||
selectedModel?.defaultThinkingOptionId ??
|
||||
"default";
|
||||
const selectedThinking = thinkingOptions?.find((o) => o.id === selectedThinkingId) ?? null;
|
||||
const displayThinking = selectedThinking?.label ?? selectedThinkingId ?? "default";
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{/* Agent Mode Badge */}
|
||||
@@ -83,6 +121,208 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
{/* Desktop: inline dropdowns for model/thinking */}
|
||||
{IS_WEB && (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
style={({ pressed }) => [
|
||||
styles.modeBadge,
|
||||
pressed && styles.modeBadgePressed,
|
||||
]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Select agent model"
|
||||
testID="agent-model-selector"
|
||||
>
|
||||
<Text style={styles.modeBadgeText}>{displayModel}</Text>
|
||||
<ChevronDown size={14} color={theme.colors.foregroundMuted} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="top" align="start" testID="agent-model-menu">
|
||||
<DropdownMenuLabel>Model</DropdownMenuLabel>
|
||||
{models?.map((model) => {
|
||||
const isActive = model.id === agent.model;
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={model.id}
|
||||
selected={isActive}
|
||||
selectedVariant="accent"
|
||||
description={model.description}
|
||||
onSelect={() => {
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
void client.setAgentModel(agentId, model.id).catch((error) => {
|
||||
console.warn("[AgentStatusBar] setAgentModel failed", error);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{model.label}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{thinkingOptions && thinkingOptions.length > 1 && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
style={({ pressed }) => [
|
||||
styles.modeBadge,
|
||||
pressed && styles.modeBadgePressed,
|
||||
]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Select thinking option"
|
||||
testID="agent-thinking-selector"
|
||||
>
|
||||
<Text style={styles.modeBadgeText}>{displayThinking}</Text>
|
||||
<ChevronDown size={14} color={theme.colors.foregroundMuted} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="top" align="start" testID="agent-thinking-menu">
|
||||
<DropdownMenuLabel>Thinking</DropdownMenuLabel>
|
||||
{thinkingOptions.map((opt) => {
|
||||
const isActive = opt.id === selectedThinkingId;
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={opt.id}
|
||||
selected={isActive}
|
||||
selectedVariant="accent"
|
||||
description={opt.description}
|
||||
onSelect={() => {
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
void client
|
||||
.setAgentThinkingOption(agentId, opt.id === "default" ? null : opt.id)
|
||||
.catch((error) => {
|
||||
console.warn("[AgentStatusBar] setAgentThinkingOption failed", error);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{opt.label}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Mobile: preferences button opens a bottom sheet */}
|
||||
{!IS_WEB && (
|
||||
<>
|
||||
<Pressable
|
||||
onPress={() => setPrefsOpen(true)}
|
||||
style={({ pressed }) => [
|
||||
styles.prefsButton,
|
||||
pressed && styles.prefsButtonPressed,
|
||||
]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Agent preferences"
|
||||
testID="agent-preferences-button"
|
||||
>
|
||||
<SlidersHorizontal size={16} color={theme.colors.foregroundMuted} />
|
||||
</Pressable>
|
||||
|
||||
<AdaptiveModalSheet
|
||||
title="Preferences"
|
||||
visible={prefsOpen}
|
||||
onClose={() => setPrefsOpen(false)}
|
||||
testID="agent-preferences-sheet"
|
||||
>
|
||||
<View style={styles.sheetSection}>
|
||||
<Text style={styles.sheetLabel}>Model</Text>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
style={({ pressed }) => [
|
||||
styles.sheetSelect,
|
||||
pressed && styles.sheetSelectPressed,
|
||||
]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Select agent model"
|
||||
testID="agent-preferences-model"
|
||||
>
|
||||
<Text style={styles.sheetSelectText}>{displayModel}</Text>
|
||||
<ChevronDown size={16} color={theme.colors.foregroundMuted} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="top" align="start">
|
||||
<DropdownMenuLabel>Model</DropdownMenuLabel>
|
||||
{models?.map((model) => {
|
||||
const isActive = model.id === agent.model;
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={model.id}
|
||||
selected={isActive}
|
||||
selectedVariant="accent"
|
||||
description={model.description}
|
||||
onSelect={() => {
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
void client.setAgentModel(agentId, model.id).catch((error) => {
|
||||
console.warn("[AgentStatusBar] setAgentModel failed", error);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{model.label}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</View>
|
||||
|
||||
{thinkingOptions && thinkingOptions.length > 1 && (
|
||||
<View style={styles.sheetSection}>
|
||||
<Text style={styles.sheetLabel}>Thinking</Text>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
style={({ pressed }) => [
|
||||
styles.sheetSelect,
|
||||
pressed && styles.sheetSelectPressed,
|
||||
]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Select thinking option"
|
||||
testID="agent-preferences-thinking"
|
||||
>
|
||||
<Text style={styles.sheetSelectText}>{displayThinking}</Text>
|
||||
<ChevronDown size={16} color={theme.colors.foregroundMuted} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="top" align="start">
|
||||
<DropdownMenuLabel>Thinking</DropdownMenuLabel>
|
||||
{thinkingOptions.map((opt) => {
|
||||
const isActive = opt.id === selectedThinkingId;
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={opt.id}
|
||||
selected={isActive}
|
||||
selectedVariant="accent"
|
||||
description={opt.description}
|
||||
onSelect={() => {
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
void client
|
||||
.setAgentThinkingOption(agentId, opt.id === "default" ? null : opt.id)
|
||||
.catch((error) => {
|
||||
console.warn("[AgentStatusBar] setAgentThinkingOption failed", error);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{opt.label}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</View>
|
||||
)}
|
||||
|
||||
</AdaptiveModalSheet>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -111,4 +351,44 @@ const styles = StyleSheet.create((theme) => ({
|
||||
fontWeight: theme.fontWeight.semibold,
|
||||
textTransform: "capitalize",
|
||||
},
|
||||
prefsButton: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: theme.borderRadius["2xl"],
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
prefsButtonPressed: {
|
||||
backgroundColor: theme.colors.surface0,
|
||||
},
|
||||
sheetSection: {
|
||||
gap: theme.spacing[2],
|
||||
},
|
||||
sheetLabel: {
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontSize: theme.fontSize.sm,
|
||||
fontWeight: theme.fontWeight.semibold,
|
||||
},
|
||||
sheetSelect: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: theme.spacing[3],
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
paddingVertical: theme.spacing[3],
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.colors.surface2,
|
||||
backgroundColor: theme.colors.surface0,
|
||||
},
|
||||
sheetSelectPressed: {
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
sheetSelectText: {
|
||||
flex: 1,
|
||||
color: theme.colors.foreground,
|
||||
fontSize: theme.fontSize.sm,
|
||||
fontWeight: theme.fontWeight.semibold,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -44,6 +44,7 @@ import type { PendingPermission } from "@/types/shared";
|
||||
import type { AgentPermissionResponse } from "@server/server/agent/agent-sdk-types";
|
||||
import type { Agent } from "@/contexts/session-context";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import { useFileExplorerActions } from "@/hooks/use-file-explorer-actions";
|
||||
import type { DaemonClientV2 } from "@server/client/daemon-client-v2";
|
||||
import { parseToolCallDisplay } from "@/utils/tool-call-parsers";
|
||||
import { ToolCallDetailsContent } from "./tool-call-details";
|
||||
@@ -94,15 +95,7 @@ export function AgentStreamView({
|
||||
state.sessions[resolvedServerId]?.agentStreamHead?.get(agentId)
|
||||
);
|
||||
|
||||
// Get methods for file operations
|
||||
const methods = useSessionStore(
|
||||
(state) => state.sessions[resolvedServerId]?.methods
|
||||
);
|
||||
const requestDirectoryListing = methods?.requestDirectoryListing;
|
||||
const requestFilePreview = methods?.requestFilePreview;
|
||||
|
||||
const requestDirectoryListingOrInert = requestDirectoryListing ?? (() => {});
|
||||
const requestFilePreviewOrInert = requestFilePreview ?? (() => {});
|
||||
const { requestDirectoryListing, requestFilePreview } = useFileExplorerActions(resolvedServerId);
|
||||
// Keep entry/exit animations off on Android due to RN dispatchDraw crashes
|
||||
// tracked in react-native-reanimated#8422.
|
||||
const shouldDisableEntryExitAnimations = Platform.OS === "android";
|
||||
@@ -130,9 +123,9 @@ export function AgentStreamView({
|
||||
return;
|
||||
}
|
||||
|
||||
requestDirectoryListingOrInert(agentId, normalized.directory);
|
||||
requestDirectoryListing(agentId, normalized.directory);
|
||||
if (normalized.file) {
|
||||
requestFilePreviewOrInert(agentId, normalized.file);
|
||||
requestFilePreview(agentId, normalized.file);
|
||||
}
|
||||
|
||||
setExplorerTab("files");
|
||||
@@ -141,8 +134,8 @@ export function AgentStreamView({
|
||||
[
|
||||
agent.cwd,
|
||||
agentId,
|
||||
requestDirectoryListingOrInert,
|
||||
requestFilePreviewOrInert,
|
||||
requestDirectoryListing,
|
||||
requestFilePreview,
|
||||
setExplorerTab,
|
||||
openFileExplorer,
|
||||
]
|
||||
|
||||
@@ -39,6 +39,7 @@ import type { ExplorerEntry } from "@/stores/session-store";
|
||||
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import { useDownloadStore } from "@/stores/download-store";
|
||||
import { useFileExplorerActions } from "@/hooks/use-file-explorer-actions";
|
||||
import {
|
||||
usePanelStore,
|
||||
type SortOption,
|
||||
@@ -81,11 +82,12 @@ export function FileExplorerPane({
|
||||
: undefined
|
||||
);
|
||||
|
||||
const methods = useSessionStore((state) => state.sessions[serverId]?.methods);
|
||||
const requestDirectoryListing = methods?.requestDirectoryListing;
|
||||
const requestFilePreview = methods?.requestFilePreview;
|
||||
const requestFileDownloadToken = methods?.requestFileDownloadToken;
|
||||
const navigateExplorerBack = methods?.navigateExplorerBack;
|
||||
const {
|
||||
requestDirectoryListing,
|
||||
requestFilePreview,
|
||||
requestFileDownloadToken,
|
||||
navigateExplorerBack,
|
||||
} = useFileExplorerActions(serverId);
|
||||
const viewMode = usePanelStore((state) => state.explorerViewMode);
|
||||
const sortOption = usePanelStore((state) => state.explorerSortOption);
|
||||
const setSortOption = usePanelStore((state) => state.setExplorerSortOption);
|
||||
|
||||
@@ -213,16 +213,12 @@ export function GroupedAgentList({
|
||||
new Set()
|
||||
);
|
||||
|
||||
// Get the methods for the specific server
|
||||
const methods = useSessionStore((state) =>
|
||||
actionAgent?.serverId
|
||||
? state.sessions[actionAgent.serverId]?.methods
|
||||
: undefined
|
||||
const actionClient = useSessionStore((state) =>
|
||||
actionAgent?.serverId ? state.sessions[actionAgent.serverId]?.client ?? null : null
|
||||
);
|
||||
const archiveAgent = methods?.archiveAgent;
|
||||
|
||||
const isActionSheetVisible = actionAgent !== null;
|
||||
const isActionDaemonUnavailable = Boolean(actionAgent?.serverId && !methods);
|
||||
const isActionDaemonUnavailable = Boolean(actionAgent?.serverId && !actionClient);
|
||||
|
||||
const handleAgentPress = useCallback(
|
||||
(serverId: string, agentId: string) => {
|
||||
@@ -262,12 +258,12 @@ export function GroupedAgentList({
|
||||
}, []);
|
||||
|
||||
const handleArchiveFromSheet = useCallback(() => {
|
||||
if (!actionAgent || !archiveAgent) {
|
||||
if (!actionAgent || !actionClient) {
|
||||
return;
|
||||
}
|
||||
archiveAgent(actionAgent.id);
|
||||
void actionClient.archiveAgent(actionAgent.id);
|
||||
setActionAgent(null);
|
||||
}, [actionAgent, archiveAgent]);
|
||||
}, [actionAgent, actionClient]);
|
||||
|
||||
const toggleSection = useCallback((sectionKey: string) => {
|
||||
setCollapsedSections((prev) => {
|
||||
@@ -413,9 +409,9 @@ export function GroupedAgentList({
|
||||
(e: { stopPropagation: () => void }, agent: AggregatedAgent) => {
|
||||
e.stopPropagation();
|
||||
const session = useSessionStore.getState().sessions[agent.serverId];
|
||||
const archiveMethod = session?.methods?.archiveAgent;
|
||||
if (archiveMethod) {
|
||||
archiveMethod(agent.id);
|
||||
const client = session?.client ?? null;
|
||||
if (client) {
|
||||
void client.archiveAgent(agent.id);
|
||||
}
|
||||
},
|
||||
[]
|
||||
@@ -595,7 +591,7 @@ export function GroupedAgentList({
|
||||
<Text style={styles.sheetCancelText}>Cancel</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
disabled={!archiveAgent || isActionDaemonUnavailable}
|
||||
disabled={isActionDaemonUnavailable}
|
||||
style={[styles.sheetButton, styles.sheetArchiveButton]}
|
||||
onPress={handleArchiveFromSheet}
|
||||
testID="agent-action-archive"
|
||||
@@ -603,8 +599,7 @@ export function GroupedAgentList({
|
||||
<Text
|
||||
style={[
|
||||
styles.sheetArchiveText,
|
||||
(!archiveAgent || isActionDaemonUnavailable) &&
|
||||
styles.sheetArchiveTextDisabled,
|
||||
isActionDaemonUnavailable && styles.sheetArchiveTextDisabled,
|
||||
]}
|
||||
>
|
||||
Archive
|
||||
|
||||
Reference in New Issue
Block a user