Files
kandev/apps/web/components/task/mode-selector.tsx
T

114 lines
3.7 KiB
TypeScript

"use client";
import { memo, useCallback, useRef, useState } from "react";
import { IconCheck, IconChevronDown } from "@tabler/icons-react";
import { Button } from "@kandev/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from "@kandev/ui/dropdown-menu";
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
import { useAppStore } from "@/components/state-provider";
import { setSessionMode } from "@/lib/api/domains/session-api";
type ModeSelectorProps = {
sessionId: string | null;
};
export const ModeSelector = memo(function ModeSelector({ sessionId }: ModeSelectorProps) {
const modeState = useAppStore((state) =>
sessionId ? state.sessionMode.bySessionId[sessionId] : undefined,
);
const [dropdownOpen, setDropdownOpen] = useState(false);
const [tooltipOpen, setTooltipOpen] = useState(false);
const recentlyClosedRef = useRef(false);
const handleModeChange = useCallback(
async (modeId: string) => {
if (!sessionId) return;
try {
await setSessionMode(sessionId, modeId);
} catch (err) {
console.error("[ModeSelector] set-mode API failed:", err);
}
},
[sessionId],
);
const handleDropdownOpenChange = useCallback((open: boolean) => {
setDropdownOpen(open);
if (!open) {
recentlyClosedRef.current = true;
setTooltipOpen(false);
setTimeout(() => {
recentlyClosedRef.current = false;
}, 200);
}
}, []);
const handleTooltipOpenChange = useCallback(
(open: boolean) => {
if (open && (dropdownOpen || recentlyClosedRef.current)) return;
setTooltipOpen(open);
},
[dropdownOpen],
);
if (
!sessionId ||
!modeState ||
!modeState.availableModes ||
modeState.availableModes.length <= 1
) {
return null;
}
const currentMode = modeState.availableModes.find((m) => m.id === modeState.currentModeId);
const displayName = currentMode?.name || modeState.currentModeId || "Mode";
return (
<DropdownMenu open={dropdownOpen} onOpenChange={handleDropdownOpenChange}>
<Tooltip open={tooltipOpen} onOpenChange={handleTooltipOpenChange}>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
data-testid="session-mode-selector"
className="h-7 gap-1 px-2 cursor-pointer hover:bg-muted/40 whitespace-nowrap"
>
<span className="text-xs">{displayName}</span>
<IconChevronDown className="h-3 w-3 text-muted-foreground shrink-0" />
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="top">Agent permission mode</TooltipContent>
</Tooltip>
<DropdownMenuContent align="start" side="top" className="min-w-[280px]">
<DropdownMenuLabel>Available Modes</DropdownMenuLabel>
{modeState.availableModes.map((mode) => (
<DropdownMenuItem
key={mode.id}
onClick={() => handleModeChange(mode.id)}
className={`cursor-pointer relative pr-7 ${mode.id === modeState.currentModeId ? "bg-muted" : ""}`}
>
<div className="min-w-0 flex-1">
<div>{mode.name}</div>
{mode.description && (
<div className="text-xs text-muted-foreground">{mode.description}</div>
)}
</div>
{mode.id === modeState.currentModeId && (
<IconCheck className="absolute right-2 h-4 w-4" />
)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
});