fix(desktop): sidebar button z-index, remember runtime, show agent ACP info (#388)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wes
2026-04-22 19:09:37 -07:00
committed by GitHub
co-authored by Claude Opus 4.6
parent f5b2e4f8b1
commit bc6de693f8
6 changed files with 112 additions and 12 deletions
@@ -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<string | null>(
() => {
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 };
}
@@ -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(","));
}
@@ -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()}
>
<DropdownMenuRadioGroup
onValueChange={setSelectedProviderId}
onValueChange={(id) => {
setSelectedProviderId(id);
setLastProvider(id);
}}
value={selectedProvider?.id ?? ""}
>
{providers.map((provider) => (
@@ -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<string, string> = {
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 (
<span className="inline-flex items-center rounded-full bg-muted/50 px-2 py-0.5 text-xs text-muted-foreground">
{children}
</span>
);
}
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 ? <PresenceBadge status={presenceStatus} /> : null}
</div>
{role === "bot" && (managedAgent || relayAgent) ? (
<div className="flex flex-wrap gap-1.5">
{managedAgent?.agentCommand ? (
<InfoBadge>{runtimeLabel(managedAgent.agentCommand)}</InfoBadge>
) : relayAgent?.agentType ? (
<InfoBadge>{runtimeLabel(relayAgent.agentType)}</InfoBadge>
) : null}
{managedAgent?.model ? (
<InfoBadge>{managedAgent.model}</InfoBadge>
) : null}
{managedAgent?.acpCommand ? (
<InfoBadge>ACP: {managedAgent.acpCommand}</InfoBadge>
) : null}
</div>
) : null}
{profile?.about ? (
<p className="text-xs leading-relaxed text-muted-foreground">
{profile.about}
@@ -114,7 +114,7 @@ function SectionHeaderActions({
onCreateClick: () => void;
}) {
return (
<div className="absolute right-1 top-3 flex items-center gap-0.5">
<div className="absolute right-1 top-3 z-10 flex items-center gap-0.5">
<button
aria-label={browseAriaLabel}
className={SECTION_ICON_BUTTON_CLASS}
+2 -2
View File
@@ -412,7 +412,7 @@ const SidebarContent = React.forwardRef<
ref={ref}
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto [scrollbar-gutter:stable] group-data-[collapsible=icon]:overflow-hidden",
className,
)}
{...props}
@@ -468,7 +468,7 @@ const SidebarGroupAction = React.forwardRef<
ref={ref}
data-sidebar="group-action"
className={cn(
"absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"absolute right-3 top-3.5 z-10 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"group-data-[collapsible=icon]:hidden",