Files
openchamber/packages/ui/src/components/sections/commands/AgentSelector.tsx
T
Bohdan Triapitsyn 01b4f11e56 fix(agent-selector): read agents from active-directory config store
AgentSelector used useAgentsStore which is only warm after a chat session
loads, so the todo send dialog opened from the project context panel showed
an empty agent list. Prefer useConfigStore.agents (the same source ModelControls
uses in chat) with useAgentsStore as fallback, and load both on mount when
neither is populated.

Also set the primary-mode filter on the scheduled task editor agent selector
to keep its options consistent with the other flows.
2026-04-18 13:47:24 +03:00

175 lines
7.7 KiB
TypeScript

import React from 'react';
import type { Agent } from '@opencode-ai/sdk/v2';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { useAgentsStore, filterVisibleAgents } from '@/stores/useAgentsStore';
import { useConfigStore } from '@/stores/useConfigStore';
import { useUIStore } from '@/stores/useUIStore';
import { useDeviceInfo } from '@/lib/device';
import { RiArrowDownSLine, RiRobot2Line } from '@remixicon/react';
import { cn } from '@/lib/utils';
import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
interface AgentSelectorProps {
agentName: string;
onChange: (agentName: string) => void;
className?: string;
filter?: (agent: Agent) => boolean;
}
export const AgentSelector: React.FC<AgentSelectorProps> = ({
agentName,
onChange,
className,
filter,
}) => {
const configAgents = useConfigStore((state) => state.agents);
const agentsStoreAgents = useAgentsStore((state) => state.agents);
const loadAgentsStore = useAgentsStore((state) => state.loadAgents);
const loadConfigAgents = useConfigStore((state) => state.loadAgents);
const rawAgents = React.useMemo(() => {
if (Array.isArray(configAgents) && configAgents.length > 0) return configAgents;
return Array.isArray(agentsStoreAgents) ? agentsStoreAgents : [];
}, [configAgents, agentsStoreAgents]);
const agents = React.useMemo(() => {
const visible = filterVisibleAgents(rawAgents);
return filter ? visible.filter(filter) : visible;
}, [rawAgents, filter]);
const isMobile = useUIStore(state => state.isMobile);
const { isMobile: deviceIsMobile } = useDeviceInfo();
const isActuallyMobile = isMobile || deviceIsMobile;
const [isMobilePanelOpen, setIsMobilePanelOpen] = React.useState(false);
React.useEffect(() => {
if (rawAgents.length > 0) return;
void loadConfigAgents();
void loadAgentsStore();
}, [rawAgents.length, loadConfigAgents, loadAgentsStore]);
const closeMobilePanel = () => setIsMobilePanelOpen(false);
const handleAgentChange = (newAgentName: string) => {
onChange(newAgentName);
};
const renderMobileAgentPanel = () => {
if (!isActuallyMobile) return null;
return (
<MobileOverlayPanel
open={isMobilePanelOpen}
onClose={closeMobilePanel}
title="Select agent"
>
<div className="space-y-1">
<button
type="button"
className={cn(
'flex w-full items-center justify-between rounded-lg border border-border/40 bg-background/95 px-2 py-1.5 text-left',
!agentName ? 'bg-primary/10 text-primary' : 'text-foreground'
)}
onClick={() => {
handleAgentChange('');
closeMobilePanel();
}}
>
<span className={cn('typography-meta', !agentName ? 'font-medium' : 'text-muted-foreground')}>Not selected</span>
{!agentName && <div className="h-2 w-2 rounded-full bg-primary" />}
</button>
{agents.map((agent) => {
const isSelected = agent.name === agentName;
return (
<button
key={agent.name}
type="button"
className={cn(
'flex w-full items-center justify-between rounded-lg border border-border/40 bg-background/95 px-2 py-1.5 text-left',
isSelected ? 'bg-primary/10 text-primary' : 'text-foreground'
)}
onClick={() => {
handleAgentChange(agent.name);
closeMobilePanel();
}}
>
<div className="flex flex-col">
<span className="typography-meta font-medium">{agent.name}</span>
{agent.description && (
<span className="typography-micro text-muted-foreground">
{agent.description}
</span>
)}
</div>
{isSelected && (
<div className="h-2 w-2 rounded-full bg-primary" />
)}
</button>
);
})}
</div>
</MobileOverlayPanel>
);
};
return (
<>
{isActuallyMobile ? (
<button
type="button"
onClick={() => setIsMobilePanelOpen(true)}
className={cn(
'flex w-full items-center justify-between gap-2 rounded-lg border border-border/40 bg-background/95 px-2 py-1.5 text-left',
className
)}
>
<div className="flex items-center gap-2">
<RiRobot2Line className="h-3.5 w-3.5 text-muted-foreground" />
<span className="typography-meta font-medium text-foreground">
{agentName || 'Select agent...'}
</span>
</div>
<RiArrowDownSLine className="h-3 w-3 text-muted-foreground" />
</button>
) : (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className={cn(
'flex items-center gap-2 px-2 rounded-lg bg-interactive-selection/20 border border-border/20 cursor-pointer hover:bg-interactive-hover/30 h-6 w-fit',
className
)}>
<RiRobot2Line className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
<span className="typography-micro font-medium whitespace-nowrap">
{agentName || 'Not selected'}
</span>
<RiArrowDownSLine className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent className="max-w-[300px]">
<DropdownMenuItem
className="typography-meta"
onSelect={() => handleAgentChange('')}
>
<span className="text-muted-foreground">Not selected</span>
</DropdownMenuItem>
{agents.map((agent) => (
<DropdownMenuItem
key={agent.name}
className="typography-meta"
onSelect={() => handleAgentChange(agent.name)}
>
<span className="font-medium">{agent.name}</span>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)}
{renderMobileAgentPanel()}
</>
);
};