mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
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:
@@ -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}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user