1import { ProviderInteractionMode, RuntimeMode } from "@t3tools/contracts";
2import { memo, type ReactNode } from "react";
3import { EllipsisIcon } from "lucide-react";
4import {
5 Menu,
6 MenuPopup,
7 MenuRadioGroup,
8 MenuRadioItem,
9 MenuSeparator as MenuDivider,
10 MenuTrigger,
11} from "../ui/menu";
12import { ComposerControl, ComposerControlIcon } from "./ComposerControl";
13import { useComposerMenuProps } from "./composerEventScope";
14import { useComposerMenuState } from "./useComposerMenuState";
15
16export const CompactComposerControlsMenu = memo(function CompactComposerControlsMenu(props: {
17 interactionMode: ProviderInteractionMode;
18 runtimeMode: RuntimeMode;
19 runtimeModeOptions: ReadonlyArray<{
20 readonly mode: RuntimeMode;
21 readonly label: string;
22 }>;
23 showInteractionModeToggle: boolean;
24 traitsMenuContent?: ReactNode;
25 size?: "sm" | "xs";
26 /**
27 * The resting strip keeps this menu mounted out of flow while every block
28 * fits inline. Its portaled popup would outlive that transition, so an
29 * open menu closes when its trigger hides.
30 */
31 hidden?: boolean;
32 onToggleInteractionMode: () => void;
33 onRuntimeModeChange: (mode: RuntimeMode) => void;
34}) {
35 const composerFloatingLayerProps = useComposerMenuProps();
36 const size = props.size ?? "sm";
37 const [open, setOpen] = useComposerMenuState(props.hidden);
38
39 return (
40 <Menu open={open} onOpenChange={setOpen}>
41 <MenuTrigger
42 render={
43 <ComposerControl
44 size={size}
45 className="shrink-0"
46 aria-label="More composer controls"
47 data-composer-shortcut={
48 props.traitsMenuContent ? "composer.mode composer.effort" : "composer.mode"
49 }
50 />
51 }
52 >
53 <ComposerControlIcon icon={EllipsisIcon} size={size} />
54 </MenuTrigger>
55 <MenuPopup align="start" {...composerFloatingLayerProps}>
56 {props.traitsMenuContent ? (
57 <>
58 {props.traitsMenuContent}
59 <MenuDivider />
60 </>
61 ) : null}
62 {props.showInteractionModeToggle ? (
63 <>
64 <div className="px-2 py-1.5 font-medium text-muted-foreground text-xs">Mode</div>
65 <MenuRadioGroup
66 value={props.interactionMode}
67 onValueChange={(value) => {
68 if (!value || value === props.interactionMode) return;
69 props.onToggleInteractionMode();
70 }}
71 >
72 <MenuRadioItem value="default">Chat</MenuRadioItem>
73 <MenuRadioItem value="plan">Plan</MenuRadioItem>
74 </MenuRadioGroup>
75 <MenuDivider />
76 </>
77 ) : null}
78 <div className="px-2 py-1.5 font-medium text-muted-foreground text-xs">Access</div>
79 <MenuRadioGroup
80 value={props.runtimeMode}
81 onValueChange={(value) => {
82 if (!value || value === props.runtimeMode) return;
83 props.onRuntimeModeChange(value as RuntimeMode);
84 }}
85 >
86 {props.runtimeModeOptions.map((option) => (
87 <MenuRadioItem key={option.mode} value={option.mode}>
88 {option.label}
89 </MenuRadioItem>
90 ))}
91 </MenuRadioGroup>
92 </MenuPopup>
93 </Menu>
94 );
95});