From e970f0baaf543266da535df749384c6f0e4d3f3a Mon Sep 17 00:00:00 2001 From: Wes Date: Thu, 16 Apr 2026 18:21:34 -0600 Subject: [PATCH] chore: fix deprecation warnings and decompose AgentsView (#347) Co-authored-by: Claude Opus 4.6 (1M context) --- desktop/src-tauri/src/huddle/audio_output.rs | 14 +- desktop/src-tauri/src/huddle/stt.rs | 7 - desktop/src/features/agents/ui/AgentsView.tsx | 700 ++++-------------- .../agents/ui/useManagedAgentActions.ts | 271 +++++++ .../features/agents/ui/usePersonaActions.ts | 257 +++++++ 5 files changed, 674 insertions(+), 575 deletions(-) create mode 100644 desktop/src/features/agents/ui/useManagedAgentActions.ts create mode 100644 desktop/src/features/agents/ui/usePersonaActions.ts diff --git a/desktop/src-tauri/src/huddle/audio_output.rs b/desktop/src-tauri/src/huddle/audio_output.rs index 7e5519ce21..7af9256a75 100644 --- a/desktop/src-tauri/src/huddle/audio_output.rs +++ b/desktop/src-tauri/src/huddle/audio_output.rs @@ -11,14 +11,16 @@ pub fn list_audio_output_devices() -> Result, String> { use rodio::DeviceTrait; let host = rodio::cpal::default_host(); - let default_name = host.default_output_device().and_then(|d| d.name().ok()); + let default_name = host + .default_output_device() + .and_then(|d| d.description().ok().map(|desc| desc.name().to_string())); let devices = host .output_devices() .map_err(|e| format!("enumerate output devices: {e}"))?; let mut result = Vec::new(); for device in devices { - if let Ok(name) = device.name() { + if let Ok(name) = device.description().map(|d| d.name().to_string()) { let is_default = default_name.as_deref() == Some(&name); result.push(AudioOutputDevice { name, is_default }); } @@ -65,7 +67,13 @@ pub(crate) fn open_output_sink_by_name( let host = rodio::cpal::default_host(); if let Ok(devices) = host.output_devices() { for device in devices { - if device.name().ok().as_deref() == Some(name) { + if device + .description() + .ok() + .map(|d| d.name().to_string()) + .as_deref() + == Some(name) + { if let Ok(sink) = rodio::DeviceSinkBuilder::from_device(device) { return sink .open_stream() diff --git a/desktop/src-tauri/src/huddle/stt.rs b/desktop/src-tauri/src/huddle/stt.rs index 6566ab553d..a67e8e68ad 100644 --- a/desktop/src-tauri/src/huddle/stt.rs +++ b/desktop/src-tauri/src/huddle/stt.rs @@ -56,12 +56,6 @@ pub struct SttPipeline { shutdown: Arc, /// Worker thread handle — taken on drop to join cleanly. thread: Option>, - /// TTS cancel flag shared with the TTS pipeline. - /// When VAD detects speech onset during TTS playback, the STT worker sets - /// this flag to trigger barge-in (stops TTS immediately). - /// Stored here so it lives as long as the pipeline; the worker holds a clone. - #[allow(dead_code)] - pub tts_cancel: Option>, } impl SttPipeline { @@ -121,7 +115,6 @@ impl SttPipeline { audio_tx, shutdown, thread: Some(handle), - tts_cancel, }; Ok((pipeline, text_rx)) } diff --git a/desktop/src/features/agents/ui/AgentsView.tsx b/desktop/src/features/agents/ui/AgentsView.tsx index d525c50dd6..4ae2dfe687 100644 --- a/desktop/src/features/agents/ui/AgentsView.tsx +++ b/desktop/src/features/agents/ui/AgentsView.tsx @@ -1,42 +1,3 @@ -import * as React from "react"; -import { useQueryClient } from "@tanstack/react-query"; - -import { - type AttachManagedAgentToChannelResult, - personasQueryKey, - useAcpProvidersQuery, - useCreatePersonaMutation, - useDeletePersonaMutation, - useDeleteManagedAgentMutation, - useExportPersonaJsonMutation, - useManagedAgentLogQuery, - useManagedAgentsQuery, - useMintManagedAgentTokenMutation, - usePersonasQuery, - useRelayAgentsQuery, - useSetManagedAgentStartOnAppLaunchMutation, - useSetPersonaActiveMutation, - useStartManagedAgentMutation, - useStopManagedAgentMutation, - useUpdatePersonaMutation, -} from "@/features/agents/hooks"; -import { getPersonaLibraryState } from "@/features/agents/lib/catalog"; -import { useChannelsQuery } from "@/features/channels/hooks"; -import { usePresenceQuery } from "@/features/presence/hooks"; -import { - parsePersonaFiles, - type ParsePersonaFilesResult, -} from "@/shared/api/tauriPersonas"; -import { isSingleItemFile } from "@/shared/lib/fileMagic"; - -import type { - AgentPersona, - Channel, - CreatePersonaInput, - CreateManagedAgentResponse, - ManagedAgent, - UpdatePersonaInput, -} from "@/shared/api/types"; import { AddAgentToChannelDialog } from "./AddAgentToChannelDialog"; import { AddTeamToChannelDialog } from "./AddTeamToChannelDialog"; import { BatchImportDialog } from "./BatchImportDialog"; @@ -55,386 +16,27 @@ import { TeamImportDialog } from "./TeamImportDialog"; import { TeamImportUpdateDialog } from "./TeamImportUpdateDialog"; import { TeamsSection } from "./TeamsSection"; import { TokenRevealDialog } from "./TokenRevealDialog"; -import { - createPersonaDialogState, - duplicatePersonaDialogState, - editPersonaDialogState, - importPersonaDialogState, - type PersonaDialogState, -} from "./personaDialogState"; -import { usePersonaImportActions } from "./usePersonaImportActions"; +import { useManagedAgentActions } from "./useManagedAgentActions"; +import { usePersonaActions } from "./usePersonaActions"; import { useTeamActions } from "./useTeamActions"; -import { - deleteManagedAgentWithRules, - startManagedAgentWithRules, - stopManagedAgentWithRules, -} from "../lib/managedAgentControlActions"; - -type PersonaFeedbackSurface = "catalog" | "library"; export function AgentsView() { - const queryClient = useQueryClient(); - const relayAgentsQuery = useRelayAgentsQuery(); - const managedAgentsQuery = useManagedAgentsQuery(); - const channelsQuery = useChannelsQuery(); - const personasQuery = usePersonasQuery(); - const acpProvidersQuery = useAcpProvidersQuery(); - const startMutation = useStartManagedAgentMutation(); - const stopMutation = useStopManagedAgentMutation(); - const startOnLaunchMutation = useSetManagedAgentStartOnAppLaunchMutation(); - const deleteMutation = useDeleteManagedAgentMutation(); - const mintTokenMutation = useMintManagedAgentTokenMutation(); - const createPersonaMutation = useCreatePersonaMutation(); - const updatePersonaMutation = useUpdatePersonaMutation(); - const deletePersonaMutation = useDeletePersonaMutation(); - const setPersonaActiveMutation = useSetPersonaActiveMutation(); - const exportPersonaJsonMutation = useExportPersonaJsonMutation(); - const [isCreateOpen, setIsCreateOpen] = React.useState(false); - const [personaDialogState, setPersonaDialogState] = - React.useState(null); - const [personaToDelete, setPersonaToDelete] = - React.useState(null); - const [agentToAddToChannel, setAgentToAddToChannel] = - React.useState(null); - const [createdAgent, setCreatedAgent] = - React.useState(null); - const [revealedToken, setRevealedToken] = React.useState<{ - name: string; - token: string; - } | null>(null); - const [actionNoticeMessage, setActionNoticeMessage] = React.useState< - string | null - >(null); - const [actionErrorMessage, setActionErrorMessage] = React.useState< - string | null - >(null); - const [personaNoticeMessage, setPersonaNoticeMessage] = React.useState< - string | null - >(null); - const [personaErrorMessage, setPersonaErrorMessage] = React.useState< - string | null - >(null); - const [personaFeedbackSurface, setPersonaFeedbackSurface] = - React.useState("library"); - + const agents = useManagedAgentActions(); + const personas = usePersonaActions(); const teamActions = useTeamActions( - { setActionNoticeMessage, setActionErrorMessage }, { - refetchManagedAgents: () => void managedAgentsQuery.refetch(), - refetchRelayAgents: () => void relayAgentsQuery.refetch(), + setActionNoticeMessage: agents.setActionNoticeMessage, + setActionErrorMessage: agents.setActionErrorMessage, + }, + { + refetchManagedAgents: agents.refetchManagedAgents, + refetchRelayAgents: agents.refetchRelayAgents, }, ); - const personaImportActions = usePersonaImportActions( - personasQuery.data ?? [], - { - clearPersonaFeedback: () => clearPersonaFeedback("library"), - setPersonaNoticeMessage, - setPersonaErrorMessage, - setPersonaDialogState, - }, - ); - const [batchImportResult, setBatchImportResult] = - React.useState(null); - const [batchImportFileName, setBatchImportFileName] = React.useState(""); - const [isCatalogDialogOpen, setIsCatalogDialogOpen] = React.useState(false); - const managedAgents = React.useMemo( - () => - [...(managedAgentsQuery.data ?? [])].sort((left, right) => { - // Active agents (running or deployed) sort before inactive ones. - // Both "running" and "deployed" are equivalent for sorting purposes. - const activeScore = (s: string) => - s === "running" || s === "deployed" ? 1 : 0; - const diff = activeScore(right.status) - activeScore(left.status); - if (diff !== 0) return diff; - - return left.name.localeCompare(right.name); - }), - [managedAgentsQuery.data], - ); - const personas = personasQuery.data ?? []; - const { catalogPersonas, libraryPersonas, personaLabelsById } = React.useMemo( - () => getPersonaLibraryState(personas), - [personas], - ); - const [logAgentPubkey, setLogAgentPubkey] = React.useState( - null, - ); - const managedAgentLogQuery = useManagedAgentLogQuery(logAgentPubkey); - const managedPubkeys = React.useMemo( - () => new Set(managedAgents.map((agent) => agent.pubkey)), - [managedAgents], - ); - const managedPubkeyList = React.useMemo( - () => managedAgents.map((agent) => agent.pubkey), - [managedAgents], - ); - const managedPresenceQuery = usePresenceQuery(managedPubkeyList); - const channelsByPubkey = React.useMemo(() => { - const map: Record = {}; - for (const ra of relayAgentsQuery.data ?? []) { - if (ra.channels.length > 0) { - map[ra.pubkey] = ra.channels; - } - } - return map; - }, [relayAgentsQuery.data]); - - // Clear log selection if the agent was removed - React.useEffect(() => { - if ( - logAgentPubkey && - !managedAgents.some((agent) => agent.pubkey === logAgentPubkey) - ) { - setLogAgentPubkey(null); - } - }, [managedAgents, logAgentPubkey]); - - function clearActionFeedback() { - setActionNoticeMessage(null); - setActionErrorMessage(null); - } - - function clearPersonaFeedback( - surface: PersonaFeedbackSurface = personaFeedbackSurface, - ) { - setPersonaFeedbackSurface(surface); - setPersonaNoticeMessage(null); - setPersonaErrorMessage(null); - } - - async function handleStart(pubkey: string) { - clearActionFeedback(); - - try { - const agent = managedAgents.find( - (candidate) => candidate.pubkey === pubkey, - ); - if (!agent) return; - - await startManagedAgentWithRules({ - agent, - startManagedAgent: startMutation.mutateAsync, - }); - } catch (error) { - setActionErrorMessage( - error instanceof Error ? error.message : "Failed to start agent.", - ); - } - } - - async function handleStop(pubkey: string) { - clearActionFeedback(); - - try { - const agent = managedAgents.find((a) => a.pubkey === pubkey); - if (!agent) return; - - const result = await stopManagedAgentWithRules({ - agent, - channels: channelsQuery.data ?? [], - relayAgents: relayAgentsQuery.data ?? [], - stopManagedAgent: stopMutation.mutateAsync, - }); - if (result.noticeMessage) { - setActionNoticeMessage(result.noticeMessage); - } - } catch (error) { - setActionErrorMessage( - error instanceof Error ? error.message : "Failed to stop agent.", - ); - } - } - - async function handleDelete(pubkey: string) { - clearActionFeedback(); - - try { - const agent = managedAgents.find((a) => a.pubkey === pubkey); - if (!agent) return; - - const result = await deleteManagedAgentWithRules({ - agent, - channels: channelsQuery.data ?? [], - deleteManagedAgent: deleteMutation.mutateAsync, - presenceLookup: managedPresenceQuery.data, - relayAgents: relayAgentsQuery.data ?? [], - }); - if (result.cancelled) return; - - if (logAgentPubkey === pubkey) { - setLogAgentPubkey(null); - } - } catch (error) { - setActionErrorMessage( - error instanceof Error ? error.message : "Failed to delete agent.", - ); - } - } - - async function handleToggleStartOnAppLaunch( - pubkey: string, - startOnAppLaunch: boolean, - ) { - clearActionFeedback(); - - try { - const updated = await startOnLaunchMutation.mutateAsync({ - pubkey, - startOnAppLaunch, - }); - setActionNoticeMessage( - updated.startOnAppLaunch - ? `Will start ${updated.name} automatically when the desktop app opens.` - : `${updated.name} will stay manual-start only.`, - ); - } catch (error) { - setActionErrorMessage( - error instanceof Error - ? error.message - : "Failed to update startup preference.", - ); - } - } - - async function handleMintToken(pubkey: string, name: string) { - clearActionFeedback(); - - try { - const result = await mintTokenMutation.mutateAsync({ - pubkey, - tokenName: `${name}-token`, - }); - setRevealedToken({ - name, - token: result.token, - }); - } catch (error) { - setActionErrorMessage( - error instanceof Error ? error.message : "Failed to mint token.", - ); - } - } - - async function handlePersonaSubmit( - input: CreatePersonaInput | UpdatePersonaInput, - ) { - clearPersonaFeedback("library"); - - try { - if ("id" in input) { - await updatePersonaMutation.mutateAsync(input); - setPersonaNoticeMessage(`Updated ${input.displayName}.`); - } else { - await createPersonaMutation.mutateAsync(input); - setPersonaNoticeMessage(`Created ${input.displayName}.`); - } - setPersonaDialogState(null); - } catch (error) { - setPersonaErrorMessage( - error instanceof Error ? error.message : "Failed to save persona.", - ); - } - } - - async function handleDeletePersona(persona: AgentPersona) { - clearPersonaFeedback("library"); - - try { - await deletePersonaMutation.mutateAsync(persona.id); - setPersonaNoticeMessage(`Deleted ${persona.displayName}.`); - setPersonaToDelete(null); - } catch (error) { - setPersonaErrorMessage( - error instanceof Error ? error.message : "Failed to delete persona.", - ); - } - } - - async function handleSetPersonaActive( - persona: AgentPersona, - active: boolean, - surface: PersonaFeedbackSurface, - ) { - clearPersonaFeedback(surface); - - try { - await setPersonaActiveMutation.mutateAsync({ - id: persona.id, - active, - }); - setPersonaNoticeMessage( - active - ? `Selected ${persona.displayName} for My Agents.` - : `Deselected ${persona.displayName} from My Agents.`, - ); - } catch (error) { - setPersonaErrorMessage( - error instanceof Error - ? error.message - : active - ? "Failed to select persona for My Agents." - : "Failed to deselect persona from My Agents.", - ); - } - } - - function handleAddedToChannel( - channel: Channel, - result: AttachManagedAgentToChannelResult, - ) { - setActionErrorMessage(null); - setActionNoticeMessage(() => { - if (result.restarted) { - return `Added ${result.agent.name} to ${channel.name} and restarted it so the new channel subscription is live.`; - } - - if (result.started) { - return `Added ${result.agent.name} to ${channel.name} and spawned it.`; - } - - if (result.membershipAdded) { - return `Added ${result.agent.name} to ${channel.name}.`; - } - - return `${result.agent.name} is already in ${channel.name}.`; - }); - void managedAgentsQuery.refetch(); - void relayAgentsQuery.refetch(); - } - - async function handlePersonaImportFile( - fileBytes: number[], - fileName: string, - ) { - clearPersonaFeedback("library"); - try { - const result = await parsePersonaFiles(fileBytes, fileName); - if (isSingleItemFile(fileBytes) && result.personas.length === 1) { - setPersonaDialogState(importPersonaDialogState(result.personas[0])); - } else if (result.personas.length > 0) { - setBatchImportResult(result); - setBatchImportFileName(fileName); - } else { - setPersonaErrorMessage("No valid personas found in file."); - } - } catch (err) { - setPersonaErrorMessage( - err instanceof Error ? err.message : "Failed to parse persona file.", - ); - } - } const isActionPending = - startMutation.isPending || - stopMutation.isPending || - startOnLaunchMutation.isPending || - deleteMutation.isPending || - mintTokenMutation.isPending || - createPersonaMutation.isPending || - updatePersonaMutation.isPending || - deletePersonaMutation.isPending || - setPersonaActiveMutation.isPending || - exportPersonaJsonMutation.isPending || + agents.isPending || + personas.isPending || teamActions.exportTeamJsonMutation.isPending || teamActions.createTeamMutation.isPending || teamActions.updateTeamMutation.isPending || @@ -446,76 +48,37 @@ export function AgentsView() {
0} + canChooseCatalog={personas.catalogPersonas.length > 0} error={ - personasQuery.error instanceof Error - ? personasQuery.error + personas.personasQuery.error instanceof Error + ? personas.personasQuery.error : null } feedbackErrorMessage={ - personaFeedbackSurface === "library" - ? personaErrorMessage + personas.personaFeedbackSurface === "library" + ? personas.personaErrorMessage : null } feedbackNoticeMessage={ - personaFeedbackSurface === "library" - ? personaNoticeMessage + personas.personaFeedbackSurface === "library" + ? personas.personaNoticeMessage : null } - isLoading={personasQuery.isLoading} - isPending={ - createPersonaMutation.isPending || - updatePersonaMutation.isPending || - deletePersonaMutation.isPending || - setPersonaActiveMutation.isPending || - exportPersonaJsonMutation.isPending - } - onChooseCatalog={() => { - clearPersonaFeedback("catalog"); - setIsCatalogDialogOpen(true); - }} - onCreate={() => { - clearPersonaFeedback("library"); - setPersonaDialogState(createPersonaDialogState()); - }} - onDelete={(persona) => { - clearPersonaFeedback("library"); - setPersonaToDelete(persona); - }} + isLoading={personas.personasQuery.isLoading} + isPending={personas.isPending} + onChooseCatalog={personas.openCatalog} + onCreate={personas.openCreate} + onDelete={personas.openDelete} onDeactivate={(persona) => { - void handleSetPersonaActive(persona, false, "library"); - }} - onDuplicate={(persona) => { - clearPersonaFeedback("library"); - setPersonaDialogState(duplicatePersonaDialogState(persona)); - }} - onEdit={(persona) => { - clearPersonaFeedback("library"); - setPersonaDialogState(editPersonaDialogState(persona)); + void personas.handleSetActive(persona, false, "library"); }} + onDuplicate={personas.openDuplicate} + onEdit={personas.openEdit} onImportFile={(fileBytes, fileName) => { - void handlePersonaImportFile(fileBytes, fileName); + void personas.handleImportFile(fileBytes, fileName); }} - onExport={(persona) => { - clearPersonaFeedback("library"); - exportPersonaJsonMutation.mutate(persona.id, { - onSuccess: (saved) => { - if (saved) { - setPersonaNoticeMessage( - `Exported ${persona.displayName}.`, - ); - } - }, - onError: (error) => { - setPersonaErrorMessage( - error instanceof Error - ? error.message - : "Failed to export persona.", - ); - }, - }); - }} - personas={libraryPersonas} + onExport={personas.handleExport} + personas={personas.libraryPersonas} /> { - setActionNoticeMessage(null); - setActionErrorMessage(null); - setAgentToAddToChannel(agent); + agents.setActionNoticeMessage(null); + agents.setActionErrorMessage(null); + agents.setAgentToAddToChannel(agent); }} onCreate={() => { - setIsCreateOpen(true); + agents.setIsCreateOpen(true); }} onDelete={(pubkey) => { - void handleDelete(pubkey); + void agents.handleDelete(pubkey); }} onMintToken={(pubkey, name) => { - void handleMintToken(pubkey, name); + void agents.handleMintToken(pubkey, name); }} - onSelectLogAgent={setLogAgentPubkey} + onSelectLogAgent={agents.setLogAgentPubkey} onStart={(pubkey) => { - void handleStart(pubkey); + void agents.handleStart(pubkey); }} onStop={(pubkey) => { - void handleStop(pubkey); + void agents.handleStop(pubkey); }} onToggleStartOnAppLaunch={(pubkey, startOnAppLaunch) => { - void handleToggleStartOnAppLaunch(pubkey, startOnAppLaunch); + void agents.handleToggleStartOnAppLaunch( + pubkey, + startOnAppLaunch, + ); }} - selectedLogAgentPubkey={logAgentPubkey} + selectedLogAgentPubkey={agents.logAgentPubkey} />
@@ -605,101 +171,110 @@ export function AgentsView() { { - setLogAgentPubkey(result.agent.pubkey); - setCreatedAgent(result); + agents.setLogAgentPubkey(result.agent.pubkey); + agents.setCreatedAgent(result); }} - onOpenChange={setIsCreateOpen} - open={isCreateOpen} + onOpenChange={agents.setIsCreateOpen} + open={agents.isCreateOpen} /> { if (!open) { - setAgentToAddToChannel(null); + agents.setAgentToAddToChannel(null); } }} - open={agentToAddToChannel !== null} + open={agents.agentToAddToChannel !== null} /> { if (!open) { - setCreatedAgent(null); + agents.setCreatedAgent(null); } }} /> { if (!open) { - setRevealedToken(null); + agents.setRevealedToken(null); } }} - token={revealedToken?.token ?? null} + token={agents.revealedToken?.token ?? null} /> { if (!open) { - setPersonaDialogState(null); + personas.setPersonaDialogState(null); } }} - onSubmit={handlePersonaSubmit} - open={personaDialogState !== null} - submitLabel={personaDialogState?.submitLabel ?? "Save"} - title={personaDialogState?.title ?? "Persona"} + onSubmit={personas.handleSubmit} + open={personas.personaDialogState !== null} + submitLabel={personas.personaDialogState?.submitLabel ?? "Save"} + title={personas.personaDialogState?.title ?? "Persona"} /> { - void handleDeletePersona(persona); + void personas.handleDelete(persona); }} onOpenChange={(open) => { if (!open) { - setPersonaToDelete(null); + personas.setPersonaToDelete(null); } }} - open={personaToDelete !== null} - persona={personaToDelete} + open={personas.personaToDelete !== null} + persona={personas.personaToDelete} /> { - clearPersonaFeedback("catalog"); + personas.clearFeedback("catalog"); }} - onOpenChange={setIsCatalogDialogOpen} + onOpenChange={personas.setIsCatalogDialogOpen} onSelectPersona={(persona, active) => { - void handleSetPersonaActive(persona, active, "catalog"); + void personas.handleSetActive(persona, active, "catalog"); }} - open={isCatalogDialogOpen} - personas={catalogPersonas} + open={personas.isCatalogDialogOpen} + personas={personas.catalogPersonas} /> @@ -749,26 +324,19 @@ export function AgentsView() { } }} open={teamActions.teamToAddToChannel !== null} - personas={libraryPersonas} + personas={personas.libraryPersonas} team={teamActions.teamToAddToChannel} /> { - clearPersonaFeedback("library"); - setBatchImportResult(null); - setPersonaNoticeMessage( - `Imported ${count} persona${count !== 1 ? "s" : ""}.`, - ); - void queryClient.invalidateQueries({ queryKey: personasQueryKey }); - }} + fileName={personas.batchImportFileName} + onComplete={personas.handleBatchImportComplete} onOpenChange={(open) => { if (!open) { - setBatchImportResult(null); + personas.setBatchImportResult(null); } }} - open={batchImportResult !== null} - result={batchImportResult} + open={personas.batchImportResult !== null} + result={personas.batchImportResult} /> { if (!open) { - personaImportActions.closeImportUpdateDialog(); + personas.personaImportActions.closeImportUpdateDialog(); } }} - open={personaImportActions.personaImportTarget !== null} - persona={personaImportActions.personaImportTarget} + open={personas.personaImportActions.personaImportTarget !== null} + persona={personas.personaImportActions.personaImportTarget} preview={ - personaImportActions.personaImportTargetPreview?.preview ?? null + personas.personaImportActions.personaImportTargetPreview?.preview ?? + null } /> diff --git a/desktop/src/features/agents/ui/useManagedAgentActions.ts b/desktop/src/features/agents/ui/useManagedAgentActions.ts new file mode 100644 index 0000000000..062318f2ef --- /dev/null +++ b/desktop/src/features/agents/ui/useManagedAgentActions.ts @@ -0,0 +1,271 @@ +import * as React from "react"; + +import { + type AttachManagedAgentToChannelResult, + useManagedAgentLogQuery, + useManagedAgentsQuery, + useMintManagedAgentTokenMutation, + useRelayAgentsQuery, + useSetManagedAgentStartOnAppLaunchMutation, + useStartManagedAgentMutation, + useStopManagedAgentMutation, + useDeleteManagedAgentMutation, +} from "@/features/agents/hooks"; +import { useChannelsQuery } from "@/features/channels/hooks"; +import { usePresenceQuery } from "@/features/presence/hooks"; +import type { + Channel, + CreateManagedAgentResponse, + ManagedAgent, +} from "@/shared/api/types"; +import { + deleteManagedAgentWithRules, + startManagedAgentWithRules, + stopManagedAgentWithRules, +} from "../lib/managedAgentControlActions"; + +export function useManagedAgentActions() { + const relayAgentsQuery = useRelayAgentsQuery(); + const managedAgentsQuery = useManagedAgentsQuery(); + const channelsQuery = useChannelsQuery(); + const startMutation = useStartManagedAgentMutation(); + const stopMutation = useStopManagedAgentMutation(); + const deleteMutation = useDeleteManagedAgentMutation(); + const startOnLaunchMutation = useSetManagedAgentStartOnAppLaunchMutation(); + const mintTokenMutation = useMintManagedAgentTokenMutation(); + + const [isCreateOpen, setIsCreateOpen] = React.useState(false); + const [agentToAddToChannel, setAgentToAddToChannel] = + React.useState(null); + const [createdAgent, setCreatedAgent] = + React.useState(null); + const [revealedToken, setRevealedToken] = React.useState<{ + name: string; + token: string; + } | null>(null); + const [logAgentPubkey, setLogAgentPubkey] = React.useState( + null, + ); + const [actionNoticeMessage, setActionNoticeMessage] = React.useState< + string | null + >(null); + const [actionErrorMessage, setActionErrorMessage] = React.useState< + string | null + >(null); + + const managedAgentLogQuery = useManagedAgentLogQuery(logAgentPubkey); + + const managedAgents = React.useMemo( + () => + [...(managedAgentsQuery.data ?? [])].sort((left, right) => { + const activeScore = (s: string) => + s === "running" || s === "deployed" ? 1 : 0; + const diff = activeScore(right.status) - activeScore(left.status); + if (diff !== 0) return diff; + return left.name.localeCompare(right.name); + }), + [managedAgentsQuery.data], + ); + + const managedPubkeys = React.useMemo( + () => new Set(managedAgents.map((agent) => agent.pubkey)), + [managedAgents], + ); + + const managedPubkeyList = React.useMemo( + () => managedAgents.map((agent) => agent.pubkey), + [managedAgents], + ); + + const managedPresenceQuery = usePresenceQuery(managedPubkeyList); + + const channelsByPubkey = React.useMemo(() => { + const map: Record = {}; + for (const ra of relayAgentsQuery.data ?? []) { + if (ra.channels.length > 0) { + map[ra.pubkey] = ra.channels; + } + } + return map; + }, [relayAgentsQuery.data]); + + // Clear log selection if the agent was removed + React.useEffect(() => { + if ( + logAgentPubkey && + !managedAgents.some((agent) => agent.pubkey === logAgentPubkey) + ) { + setLogAgentPubkey(null); + } + }, [managedAgents, logAgentPubkey]); + + function clearFeedback() { + setActionNoticeMessage(null); + setActionErrorMessage(null); + } + + async function handleStart(pubkey: string) { + clearFeedback(); + try { + const agent = managedAgents.find((c) => c.pubkey === pubkey); + if (!agent) return; + await startManagedAgentWithRules({ + agent, + startManagedAgent: startMutation.mutateAsync, + }); + } catch (error) { + setActionErrorMessage( + error instanceof Error ? error.message : "Failed to start agent.", + ); + } + } + + async function handleStop(pubkey: string) { + clearFeedback(); + try { + const agent = managedAgents.find((a) => a.pubkey === pubkey); + if (!agent) return; + const result = await stopManagedAgentWithRules({ + agent, + channels: channelsQuery.data ?? [], + relayAgents: relayAgentsQuery.data ?? [], + stopManagedAgent: stopMutation.mutateAsync, + }); + if (result.noticeMessage) { + setActionNoticeMessage(result.noticeMessage); + } + } catch (error) { + setActionErrorMessage( + error instanceof Error ? error.message : "Failed to stop agent.", + ); + } + } + + async function handleDelete(pubkey: string) { + clearFeedback(); + try { + const agent = managedAgents.find((a) => a.pubkey === pubkey); + if (!agent) return; + const result = await deleteManagedAgentWithRules({ + agent, + channels: channelsQuery.data ?? [], + deleteManagedAgent: deleteMutation.mutateAsync, + presenceLookup: managedPresenceQuery.data, + relayAgents: relayAgentsQuery.data ?? [], + }); + if (result.cancelled) return; + if (logAgentPubkey === pubkey) { + setLogAgentPubkey(null); + } + } catch (error) { + setActionErrorMessage( + error instanceof Error ? error.message : "Failed to delete agent.", + ); + } + } + + async function handleToggleStartOnAppLaunch( + pubkey: string, + startOnAppLaunch: boolean, + ) { + clearFeedback(); + try { + const updated = await startOnLaunchMutation.mutateAsync({ + pubkey, + startOnAppLaunch, + }); + setActionNoticeMessage( + updated.startOnAppLaunch + ? `Will start ${updated.name} automatically when the desktop app opens.` + : `${updated.name} will stay manual-start only.`, + ); + } catch (error) { + setActionErrorMessage( + error instanceof Error + ? error.message + : "Failed to update startup preference.", + ); + } + } + + async function handleMintToken(pubkey: string, name: string) { + clearFeedback(); + try { + const result = await mintTokenMutation.mutateAsync({ + pubkey, + tokenName: `${name}-token`, + }); + setRevealedToken({ name, token: result.token }); + } catch (error) { + setActionErrorMessage( + error instanceof Error ? error.message : "Failed to mint token.", + ); + } + } + + function handleAddedToChannel( + channel: Channel, + result: AttachManagedAgentToChannelResult, + ) { + setActionErrorMessage(null); + setActionNoticeMessage(() => { + if (result.restarted) { + return `Added ${result.agent.name} to ${channel.name} and restarted it so the new channel subscription is live.`; + } + if (result.started) { + return `Added ${result.agent.name} to ${channel.name} and spawned it.`; + } + if (result.membershipAdded) { + return `Added ${result.agent.name} to ${channel.name}.`; + } + return `${result.agent.name} is already in ${channel.name}.`; + }); + void managedAgentsQuery.refetch(); + void relayAgentsQuery.refetch(); + } + + const isPending = + startMutation.isPending || + stopMutation.isPending || + startOnLaunchMutation.isPending || + deleteMutation.isPending || + mintTokenMutation.isPending; + + return { + // Queries + relayAgentsQuery, + managedAgentsQuery, + managedAgentLogQuery, + managedPresenceQuery, + // Derived state + managedAgents, + managedPubkeys, + channelsByPubkey, + isPending, + // UI state + isCreateOpen, + setIsCreateOpen, + agentToAddToChannel, + setAgentToAddToChannel, + createdAgent, + setCreatedAgent, + revealedToken, + setRevealedToken, + logAgentPubkey, + setLogAgentPubkey, + actionNoticeMessage, + setActionNoticeMessage, + actionErrorMessage, + setActionErrorMessage, + // Handlers + handleStart, + handleStop, + handleDelete, + handleToggleStartOnAppLaunch, + handleMintToken, + handleAddedToChannel, + // Refetch helpers (for cross-domain use) + refetchManagedAgents: () => void managedAgentsQuery.refetch(), + refetchRelayAgents: () => void relayAgentsQuery.refetch(), + }; +} diff --git a/desktop/src/features/agents/ui/usePersonaActions.ts b/desktop/src/features/agents/ui/usePersonaActions.ts new file mode 100644 index 0000000000..6d01f6c1f2 --- /dev/null +++ b/desktop/src/features/agents/ui/usePersonaActions.ts @@ -0,0 +1,257 @@ +import * as React from "react"; +import { useQueryClient } from "@tanstack/react-query"; + +import { + personasQueryKey, + useAcpProvidersQuery, + useCreatePersonaMutation, + useDeletePersonaMutation, + useExportPersonaJsonMutation, + usePersonasQuery, + useSetPersonaActiveMutation, + useUpdatePersonaMutation, +} from "@/features/agents/hooks"; +import { getPersonaLibraryState } from "@/features/agents/lib/catalog"; +import { + parsePersonaFiles, + type ParsePersonaFilesResult, +} from "@/shared/api/tauriPersonas"; +import { isSingleItemFile } from "@/shared/lib/fileMagic"; +import type { + AgentPersona, + CreatePersonaInput, + UpdatePersonaInput, +} from "@/shared/api/types"; +import { + createPersonaDialogState, + duplicatePersonaDialogState, + editPersonaDialogState, + importPersonaDialogState, + type PersonaDialogState, +} from "./personaDialogState"; +import { usePersonaImportActions } from "./usePersonaImportActions"; + +type PersonaFeedbackSurface = "catalog" | "library"; + +export function usePersonaActions() { + const queryClient = useQueryClient(); + const personasQuery = usePersonasQuery(); + const acpProvidersQuery = useAcpProvidersQuery(); + const createPersonaMutation = useCreatePersonaMutation(); + const updatePersonaMutation = useUpdatePersonaMutation(); + const deletePersonaMutation = useDeletePersonaMutation(); + const setPersonaActiveMutation = useSetPersonaActiveMutation(); + const exportPersonaJsonMutation = useExportPersonaJsonMutation(); + + const [personaDialogState, setPersonaDialogState] = + React.useState(null); + const [personaToDelete, setPersonaToDelete] = + React.useState(null); + const [isCatalogDialogOpen, setIsCatalogDialogOpen] = React.useState(false); + const [batchImportResult, setBatchImportResult] = + React.useState(null); + const [batchImportFileName, setBatchImportFileName] = React.useState(""); + const [personaNoticeMessage, setPersonaNoticeMessage] = React.useState< + string | null + >(null); + const [personaErrorMessage, setPersonaErrorMessage] = React.useState< + string | null + >(null); + const [personaFeedbackSurface, setPersonaFeedbackSurface] = + React.useState("library"); + + const personas = personasQuery.data ?? []; + const { catalogPersonas, libraryPersonas, personaLabelsById } = React.useMemo( + () => getPersonaLibraryState(personas), + [personas], + ); + + const personaImportActions = usePersonaImportActions(personas, { + clearPersonaFeedback: () => clearFeedback("library"), + setPersonaNoticeMessage, + setPersonaErrorMessage, + setPersonaDialogState, + }); + + function clearFeedback( + surface: PersonaFeedbackSurface = personaFeedbackSurface, + ) { + setPersonaFeedbackSurface(surface); + setPersonaNoticeMessage(null); + setPersonaErrorMessage(null); + } + + async function handleSubmit(input: CreatePersonaInput | UpdatePersonaInput) { + clearFeedback("library"); + try { + if ("id" in input) { + await updatePersonaMutation.mutateAsync(input); + setPersonaNoticeMessage(`Updated ${input.displayName}.`); + } else { + await createPersonaMutation.mutateAsync(input); + setPersonaNoticeMessage(`Created ${input.displayName}.`); + } + setPersonaDialogState(null); + } catch (error) { + setPersonaErrorMessage( + error instanceof Error ? error.message : "Failed to save persona.", + ); + } + } + + async function handleDelete(persona: AgentPersona) { + clearFeedback("library"); + try { + await deletePersonaMutation.mutateAsync(persona.id); + setPersonaNoticeMessage(`Deleted ${persona.displayName}.`); + setPersonaToDelete(null); + } catch (error) { + setPersonaErrorMessage( + error instanceof Error ? error.message : "Failed to delete persona.", + ); + } + } + + async function handleSetActive( + persona: AgentPersona, + active: boolean, + surface: PersonaFeedbackSurface, + ) { + clearFeedback(surface); + try { + await setPersonaActiveMutation.mutateAsync({ id: persona.id, active }); + setPersonaNoticeMessage( + active + ? `Selected ${persona.displayName} for My Agents.` + : `Deselected ${persona.displayName} from My Agents.`, + ); + } catch (error) { + setPersonaErrorMessage( + error instanceof Error + ? error.message + : active + ? "Failed to select persona for My Agents." + : "Failed to deselect persona from My Agents.", + ); + } + } + + async function handleImportFile(fileBytes: number[], fileName: string) { + clearFeedback("library"); + try { + const result = await parsePersonaFiles(fileBytes, fileName); + if (isSingleItemFile(fileBytes) && result.personas.length === 1) { + setPersonaDialogState(importPersonaDialogState(result.personas[0])); + } else if (result.personas.length > 0) { + setBatchImportResult(result); + setBatchImportFileName(fileName); + } else { + setPersonaErrorMessage("No valid personas found in file."); + } + } catch (err) { + setPersonaErrorMessage( + err instanceof Error ? err.message : "Failed to parse persona file.", + ); + } + } + + function handleExport(persona: AgentPersona) { + clearFeedback("library"); + exportPersonaJsonMutation.mutate(persona.id, { + onSuccess: (saved) => { + if (saved) { + setPersonaNoticeMessage(`Exported ${persona.displayName}.`); + } + }, + onError: (error) => { + setPersonaErrorMessage( + error instanceof Error ? error.message : "Failed to export persona.", + ); + }, + }); + } + + function handleBatchImportComplete(count: number) { + clearFeedback("library"); + setBatchImportResult(null); + setPersonaNoticeMessage( + `Imported ${count} persona${count !== 1 ? "s" : ""}.`, + ); + void queryClient.invalidateQueries({ queryKey: personasQueryKey }); + } + + function openCreate() { + clearFeedback("library"); + setPersonaDialogState(createPersonaDialogState()); + } + + function openEdit(persona: AgentPersona) { + clearFeedback("library"); + setPersonaDialogState(editPersonaDialogState(persona)); + } + + function openDuplicate(persona: AgentPersona) { + clearFeedback("library"); + setPersonaDialogState(duplicatePersonaDialogState(persona)); + } + + function openCatalog() { + clearFeedback("catalog"); + setIsCatalogDialogOpen(true); + } + + function openDelete(persona: AgentPersona) { + clearFeedback("library"); + setPersonaToDelete(persona); + } + + const isPending = + createPersonaMutation.isPending || + updatePersonaMutation.isPending || + deletePersonaMutation.isPending || + setPersonaActiveMutation.isPending || + exportPersonaJsonMutation.isPending; + + return { + // Queries + personasQuery, + acpProvidersQuery, + // Mutations (for error/pending access) + createPersonaMutation, + updatePersonaMutation, + setPersonaActiveMutation, + // Derived state + catalogPersonas, + libraryPersonas, + personaLabelsById, + isPending, + // UI state + personaDialogState, + setPersonaDialogState, + personaToDelete, + setPersonaToDelete, + isCatalogDialogOpen, + setIsCatalogDialogOpen, + batchImportResult, + setBatchImportResult, + batchImportFileName, + personaNoticeMessage, + personaErrorMessage, + personaFeedbackSurface, + // Import actions (composed) + personaImportActions, + // Handlers + handleSubmit, + handleDelete, + handleSetActive, + handleImportFile, + handleExport, + handleBatchImportComplete, + openCreate, + openEdit, + openDuplicate, + openCatalog, + openDelete, + clearFeedback, + }; +}