diff --git a/desktop/src/features/messages/ui/TypingIndicatorRow.tsx b/desktop/src/features/messages/ui/TypingIndicatorRow.tsx index d7a2f88d67..aaaaf9afc2 100644 --- a/desktop/src/features/messages/ui/TypingIndicatorRow.tsx +++ b/desktop/src/features/messages/ui/TypingIndicatorRow.tsx @@ -1,11 +1,16 @@ import * as React from "react"; +import { + useManagedAgentsQuery, + useStopManagedAgentMutation, +} from "@/features/agents/hooks"; import { resolveUserLabel, type UserProfileLookup, } from "@/features/profile/lib/identity"; import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar"; -import type { Channel } from "@/shared/api/types"; +import type { Channel, ManagedAgent } from "@/shared/api/types"; +import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover"; type TypingIndicatorRowProps = { channel: Channel | null; @@ -46,12 +51,91 @@ function formatTypingLabel(names: string[]) { return `${names[0]}, ${names[1]}, and ${names.length - 2} others are typing...`; } +function formatElapsed(startIso: string): string { + const startMs = new Date(startIso).getTime(); + const nowMs = Date.now(); + const totalSeconds = Math.max(0, Math.floor((nowMs - startMs) / 1000)); + + if (totalSeconds >= 3600) { + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + return `${hours}h ${minutes}m`; + } + + if (totalSeconds >= 60) { + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + return `${minutes}m ${seconds}s`; + } + + return `${totalSeconds}s`; +} + +type BotTypingPopoverContentProps = { + botAgents: ManagedAgent[]; +}; + +function BotTypingPopoverContent({ botAgents }: BotTypingPopoverContentProps) { + const [, setTick] = React.useState(0); + const mutation = useStopManagedAgentMutation(); + + React.useEffect(() => { + const interval = setInterval(() => { + setTick((prev) => prev + 1); + }, 1000); + return () => clearInterval(interval); + }, []); + + function handleInterrupt() { + for (const agent of botAgents) { + mutation.mutate(agent.pubkey); + } + } + + return ( +
+ {botAgents.map((agent) => ( +
+
{agent.name}
+ {agent.model && ( +
{agent.model}
+ )} +
+ {agent.lastStartedAt ? formatElapsed(agent.lastStartedAt) : "—"} +
+
+ ))} + +
+ ); +} + export function TypingIndicatorRow({ channel, currentPubkey, profiles, typingPubkeys, }: TypingIndicatorRowProps) { + const { data: managedAgents } = useManagedAgentsQuery(); + + const managedAgentMap = React.useMemo(() => { + const map = new Map(); + if (managedAgents) { + for (const agent of managedAgents) { + map.set(agent.pubkey.toLowerCase(), agent); + } + } + return map; + }, [managedAgents]); + const labels = React.useMemo( () => typingPubkeys.map((pubkey) => @@ -66,10 +150,22 @@ export function TypingIndicatorRow({ [channel, currentPubkey, profiles, typingPubkeys], ); + const botAgents = React.useMemo( + () => + typingPubkeys + .map((pubkey) => managedAgentMap.get(pubkey.toLowerCase())) + .filter((agent): agent is ManagedAgent => agent !== undefined), + [typingPubkeys, managedAgentMap], + ); + + const hasBotTypers = botAgents.length > 0; + if (labels.length === 0) { return null; } + const typingText = formatTypingLabel(labels); + return (
-

- {formatTypingLabel(labels)} -

+ {hasBotTypers ? ( + + + + + + + + + ) : ( +

+ {typingText} +

+ )}
);