From 1a4f7082a3aabf3541fa599642498dc840bf3bb1 Mon Sep 17 00:00:00 2001 From: Finesssee <90105158+Finesssee@users.noreply.github.com> Date: Sun, 10 May 2026 19:04:57 +0700 Subject: [PATCH] Polish ACP agent selector scrolling --- .../components/selectors/agent-selector.tsx | 101 ++++++++++++++++-- 1 file changed, 93 insertions(+), 8 deletions(-) diff --git a/src/features/ai/components/selectors/agent-selector.tsx b/src/features/ai/components/selectors/agent-selector.tsx index d1592b926..18395234b 100644 --- a/src/features/ai/components/selectors/agent-selector.tsx +++ b/src/features/ai/components/selectors/agent-selector.tsx @@ -6,7 +6,14 @@ import { SlidersHorizontal as Settings2, SpinnerGap, } from "@phosphor-icons/react"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, + type WheelEvent as ReactWheelEvent, +} from "react"; import { ProviderIcon } from "@/features/ai/components/icons/provider-icons"; import { AcpStreamHandler } from "@/features/ai/services/acp-stream-handler"; import { useAIChatStore } from "@/features/ai/store/store"; @@ -23,9 +30,52 @@ const ATHAS_AGENT_OPTION = { id: "custom", name: "Athas Agent", description: "Use Athas chat settings and provider configuration", + icon: null, isAcp: false, }; +function AgentIcon({ + agentId, + icon, + size, + className, +}: { + agentId: string; + icon?: string | null; + size: number; + className?: string; +}) { + const [didFail, setDidFail] = useState(false); + + if (icon && !didFail) { + return ( + setDidFail(true)} + className={cn("shrink-0 object-contain", className)} + style={{ + filter: `brightness(0) saturate(100%) invert(82%) sepia(82%) saturate(1180%) hue-rotate(${agentIconHue(agentId)}deg) brightness(101%) contrast(96%)`, + }} + /> + ); + } + + return ; +} + +function agentIconHue(agentId: string) { + let hash = 0; + for (let index = 0; index < agentId.length; index++) { + hash = (hash * 31 + agentId.charCodeAt(index)) >>> 0; + } + return hash % 360; +} + interface AgentSelectorProps { variant?: "header" | "input"; onOpenSettings?: () => void; @@ -60,6 +110,8 @@ export function AgentSelector({ const triggerRef = useRef(null); const inputRef = useRef(null); + const listRef = useRef(null); + const itemRefs = useRef>([]); const previousOpenSignalRef = useRef(openSignal); const currentAgentId = selectedAgentId ?? getCurrentAgentId(); @@ -93,6 +145,7 @@ export function AgentSelector({ id: string; name: string; description: string; + icon?: string | null; isInstalled?: boolean; isCurrent?: boolean; canInstall?: boolean; @@ -120,6 +173,7 @@ export function AgentSelector({ id: agent.id, name: agent.name, description: agentConfig?.description ?? agent.description ?? "ACP-compatible coding agent", + icon: agentConfig?.icon ?? agent.icon, isInstalled, isCurrent: agent.id === currentAgentId, canInstall: agent.id === "custom" ? false : (agentConfig?.canInstall ?? true), @@ -156,6 +210,20 @@ export function AgentSelector({ } }, [isOpen]); + useEffect(() => { + if (!isOpen) return; + itemRefs.current[selectedIndex]?.scrollIntoView({ block: "nearest" }); + }, [isOpen, selectedIndex]); + + const handleAgentListWheel = useCallback((event: ReactWheelEvent) => { + const list = event.currentTarget; + if (list.scrollHeight <= list.clientHeight || event.deltaY === 0) return; + + event.preventDefault(); + event.stopPropagation(); + list.scrollTop += event.deltaY; + }, []); + const handleAgentChange = useCallback( async (agentId: AgentType) => { if (onSelectAgent) { @@ -284,7 +352,12 @@ export function AgentSelector({ compact className="ui-font flex h-8 max-w-[min(220px,100%)] items-center gap-1.5 rounded-full border border-border bg-secondary-bg/80 px-3 text-xs transition-colors hover:bg-hover" > - + {currentAgent?.name || "Agent"} setIsOpen(false)} portalContainer={portalContainer} - className="flex w-[min(280px,calc(100vw-16px))] max-w-[calc(100vw-16px)] flex-col overflow-hidden rounded-xl p-0" - style={{ maxHeight: "240px" }} + className="flex w-[min(340px,calc(100vw-16px))] max-w-[calc(100vw-16px)] flex-col overflow-hidden rounded-xl p-0" + style={{ maxHeight: "min(560px, calc(100vh - 24px))" }} >
-
+
{filteredItems.length === 0 ? (
No results found
) : ( @@ -329,6 +406,9 @@ export function AgentSelector({ return (
{ + itemRefs.current[itemIndex] = element; + }} role="button" tabIndex={-1} onMouseEnter={() => setSelectedIndex(itemIndex)} @@ -342,16 +422,21 @@ export function AgentSelector({ } }} className={cn( - "group flex min-h-7 cursor-pointer items-center gap-2 rounded-md px-2 py-1 text-xs transition-colors", + "group mb-1 flex min-h-10 cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-xs transition-colors last:mb-2", isSelected ? "bg-hover/90" : "bg-transparent", item.isCurrent && "bg-selected/90 ring-1 ring-accent/10", !item.isInstalled && item.id !== "custom" && "text-text-lighter", )} >
- +
-
+
{item.name}
{!item.isInstalled && item.id !== "custom" ? (