Files
openchamber/packages/ui/src/components/sections/commands/AgentSelector.tsx
T
Bohdan Triapitsyn ddedc02687 feat: introduced a token-based theming system across the UI
* feat: added themes system

* feat: smart sidebar auto-hide for files/diff tabs + lower files sidebar threshold

* feat: added Checkbox component and update theming

- Add reusable Checkbox component for toggles across UI
- Replace several inputs with Checkbox in settings and commands panels
- Add DiffIcon and apply surface/border theming to key UI areas

* feat: Add convert-vscode-theme.cjs to convert VS Code themes to OpenChamber format

* refactor: remove unused permission logic from ChatInput

- Remove unused permission rules parsing logic from ChatInput
- Memoize renderTheme in DiffWorkerProvider to avoid unnecessary recalculations
- Remove forceOpaque helper in vscode theme adapter

* fix: guard VSCode theme loading in MarkdownRenderer

* feat: add custom user themes loading and reload

- Load user themes from ~/.config/openchamber/themes at runtime
- Expose /api/config/themes to fetch custom themes
- Allow theme reloading from Settings → Theme → Reload themes in the UI
2026-02-01 18:29:34 +02:00

156 lines
6.5 KiB
TypeScript

import React from 'react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { useAgentsStore } from '@/stores/useAgentsStore';
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;
}
export const AgentSelector: React.FC<AgentSelectorProps> = ({
agentName,
onChange,
className
}) => {
const { loadAgents, getVisibleAgents } = useAgentsStore();
const agents = getVisibleAgents();
const isMobile = useUIStore(state => state.isMobile);
const { isMobile: deviceIsMobile } = useDeviceInfo();
const isActuallyMobile = isMobile || deviceIsMobile;
const [isMobilePanelOpen, setIsMobilePanelOpen] = React.useState(false);
React.useEffect(() => {
loadAgents();
}, [loadAgents]);
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">
{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>
);
})}
<button
type="button"
className="flex w-full items-center justify-between rounded-lg border border-border/40 bg-background/95 px-2 py-1.5 text-left"
onClick={() => {
handleAgentChange('');
closeMobilePanel();
}}
>
<span className="typography-meta text-muted-foreground">No agent (optional)</span>
</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]">
{agents.map((agent) => (
<DropdownMenuItem
key={agent.name}
className="typography-meta"
onSelect={() => handleAgentChange(agent.name)}
>
<span className="font-medium">{agent.name}</span>
</DropdownMenuItem>
))}
<DropdownMenuItem
className="typography-meta"
onSelect={() => handleAgentChange('')}
>
<span className="text-muted-foreground">No agent (optional)</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
{renderMobileAgentPanel()}
</>
);
};