diff --git a/desktop/src/features/agents/lib/useLastRuntimeProvider.ts b/desktop/src/features/agents/lib/useLastRuntimeProvider.ts new file mode 100644 index 000000000..085e1a505 --- /dev/null +++ b/desktop/src/features/agents/lib/useLastRuntimeProvider.ts @@ -0,0 +1,29 @@ +import * as React from "react"; + +const STORAGE_KEY = "sprout:last-runtime-provider"; + +export function useLastRuntimeProvider(): { + lastProviderId: string | null; + setLastProvider: (id: string) => void; +} { + const [lastProviderId, setLastProviderId] = React.useState( + () => { + try { + return localStorage.getItem(STORAGE_KEY); + } catch { + return null; + } + }, + ); + + const setLastProvider = React.useCallback((id: string) => { + setLastProviderId(id); + try { + localStorage.setItem(STORAGE_KEY, id); + } catch { + // localStorage full — ignore + } + }, []); + + return { lastProviderId, setLastProvider }; +} diff --git a/desktop/src/features/agents/ui/CreateAgentDialog.tsx b/desktop/src/features/agents/ui/CreateAgentDialog.tsx index 7c54d8c5a..c296f171a 100644 --- a/desktop/src/features/agents/ui/CreateAgentDialog.tsx +++ b/desktop/src/features/agents/ui/CreateAgentDialog.tsx @@ -34,6 +34,7 @@ import { coerceConfigValues, ProviderConfigFields, } from "./ProviderConfigFields"; +import { useLastRuntimeProvider } from "@/features/agents/lib/useLastRuntimeProvider"; // ── Dialog ──────────────────────────────────────────────────────────────────── @@ -49,6 +50,7 @@ export function CreateAgentDialog({ const createMutation = useCreateManagedAgentMutation(); const providersQuery = useAcpProvidersQuery(); const backendProvidersQuery = useBackendProvidersQuery(); + const { lastProviderId, setLastProvider } = useLastRuntimeProvider(); const [acpCommand, setAcpCommand] = React.useState("sprout-acp"); const [agentCommand, setAgentCommand] = React.useState("goose"); const [agentArgs, setAgentArgs] = React.useState("acp"); @@ -111,15 +113,26 @@ export function CreateAgentDialog({ return; } - const matchingProvider = - providers.find((provider) => provider.command === agentCommand) ?? null; - if (matchingProvider) { - setSelectedProviderId(matchingProvider.id); + // Prefer last-used provider from localStorage + const remembered = lastProviderId + ? providers.find((provider) => provider.id === lastProviderId) + : null; + if (remembered) { + setSelectedProviderId(remembered.id); + setAgentCommand(remembered.command); + setAgentArgs(remembered.defaultArgs.join(",")); + } else { + const matchingProvider = + providers.find((provider) => provider.command === agentCommand) ?? null; + if (matchingProvider) { + setSelectedProviderId(matchingProvider.id); + } } setHasSyncedProviderSelection(true); }, [ agentCommand, hasSyncedProviderSelection, + lastProviderId, providers, providersQuery.isLoading, ]); @@ -269,6 +282,7 @@ export function CreateAgentDialog({ return; } + setLastProvider(nextProviderId); setAgentCommand(provider.command); setAgentArgs(provider.defaultArgs.join(",")); } diff --git a/desktop/src/features/channels/ui/AddChannelBotDialog.tsx b/desktop/src/features/channels/ui/AddChannelBotDialog.tsx index 492463b7d..c9a50c807 100644 --- a/desktop/src/features/channels/ui/AddChannelBotDialog.tsx +++ b/desktop/src/features/channels/ui/AddChannelBotDialog.tsx @@ -38,6 +38,7 @@ import { } from "@/features/agents/lib/resolvePersonaProvider"; import { getActivePersonas } from "@/features/agents/lib/catalog"; import { getUsableTeams } from "@/features/agents/lib/teamPersonas"; +import { useLastRuntimeProvider } from "@/features/agents/lib/useLastRuntimeProvider"; type AddChannelBotDialogProps = { backendProviders?: BackendProviderCandidate[]; @@ -98,6 +99,7 @@ export function AddChannelBotDialog({ onAdded, onOpenChange, }: AddChannelBotDialogProps) { + const { lastProviderId, setLastProvider } = useLastRuntimeProvider(); const personasQuery = usePersonasQuery(); const teamsQuery = useTeamsQuery(); const inChannelPersonaIds = useInChannelPersonaIds( @@ -183,10 +185,13 @@ export function AddChannelBotDialog({ return; } - if (!selectedProviderId && providers[0]) { - setSelectedProviderId(providers[0].id); + if (!selectedProviderId && providers.length > 0) { + const remembered = lastProviderId + ? providers.find((p) => p.id === lastProviderId) + : null; + setSelectedProviderId(remembered ? remembered.id : providers[0].id); } - }, [open, providers, selectedProviderId]); + }, [open, providers, selectedProviderId, lastProviderId]); React.useEffect(() => { if (!selectedProvider || hasEditedCustomName) { @@ -247,7 +252,7 @@ export function AddChannelBotDialog({ }, [isProviderMode, selectedBackendProvider]); function reset() { - setSelectedProviderId(providers[0]?.id ?? ""); + setSelectedProviderId(""); setSelectedPersonaIds([]); setIncludeGeneric(false); setCustomName(providers[0] ? defaultBotName(providers[0]) : ""); @@ -503,7 +508,10 @@ export function AddChannelBotDialog({ onCloseAutoFocus={(event) => event.preventDefault()} > { + setSelectedProviderId(id); + setLastProvider(id); + }} value={selectedProvider?.id ?? ""} > {providers.map((provider) => ( diff --git a/desktop/src/features/profile/ui/UserProfilePopover.tsx b/desktop/src/features/profile/ui/UserProfilePopover.tsx index 33a675338..c04b9c706 100644 --- a/desktop/src/features/profile/ui/UserProfilePopover.tsx +++ b/desktop/src/features/profile/ui/UserProfilePopover.tsx @@ -4,6 +4,10 @@ import { useUserNotesQuery, useUserProfileQuery, } from "@/features/profile/hooks"; +import { + useRelayAgentsQuery, + useManagedAgentsQuery, +} from "@/features/agents/hooks"; import { usePresenceQuery } from "@/features/presence/hooks"; import { PresenceBadge } from "@/features/presence/ui/PresenceBadge"; import { formatRelativeTime } from "@/features/forum/lib/time"; @@ -21,6 +25,25 @@ type UserProfilePopoverProps = { botIdenticonValue?: string; }; +const RUNTIME_LABELS: Record = { + goose: "Goose", + "claude-code": "Claude Code", + "codex-acp": "Codex", + aider: "Aider", +}; + +function runtimeLabel(command: string): string { + return RUNTIME_LABELS[command] ?? command; +} + +function InfoBadge({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} + function truncatePubkey(pubkey: string) { if (pubkey.length <= 16) { return pubkey; @@ -41,10 +64,20 @@ export function UserProfilePopover({ const notesQuery = useUserNotesQuery(open ? pubkey : undefined, { limit: showAllNotes ? 20 : 3, }); + const relayAgentsQuery = useRelayAgentsQuery({ + enabled: open && role === "bot", + }); + const managedAgentsQuery = useManagedAgentsQuery({ + enabled: open && role === "bot", + }); const presenceQuery = usePresenceQuery(open ? [pubkey] : [], { enabled: open, }); + const relayAgent = relayAgentsQuery.data?.find((a) => a.pubkey === pubkey); + const managedAgent = managedAgentsQuery.data?.find( + (a) => a.pubkey === pubkey, + ); const profile = profileQuery.data; const notes = notesQuery.data?.notes ?? []; const presenceStatus = presenceQuery.data?.[pubkey.toLowerCase()]; @@ -93,6 +126,22 @@ export function UserProfilePopover({ {presenceStatus ? : null} + {role === "bot" && (managedAgent || relayAgent) ? ( +
+ {managedAgent?.agentCommand ? ( + {runtimeLabel(managedAgent.agentCommand)} + ) : relayAgent?.agentType ? ( + {runtimeLabel(relayAgent.agentType)} + ) : null} + {managedAgent?.model ? ( + {managedAgent.model} + ) : null} + {managedAgent?.acpCommand ? ( + ACP: {managedAgent.acpCommand} + ) : null} +
+ ) : null} + {profile?.about ? (

{profile.about} diff --git a/desktop/src/features/sidebar/ui/AppSidebar.tsx b/desktop/src/features/sidebar/ui/AppSidebar.tsx index 6b1f30205..e4dbe8dd6 100644 --- a/desktop/src/features/sidebar/ui/AppSidebar.tsx +++ b/desktop/src/features/sidebar/ui/AppSidebar.tsx @@ -114,7 +114,7 @@ function SectionHeaderActions({ onCreateClick: () => void; }) { return ( -

+