114 lines
3.7 KiB
TypeScript
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>
|
|
);
|
|
});
|