Files
tutti/packages/agent/gui/workbench/AgentGuiWorkbenchSessionMenu.tsx
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

153 lines
4.8 KiB
TypeScript

import {
useCallback,
useRef,
type MouseEvent,
type PointerEvent,
type ReactNode
} from "react";
import { FileText, MoreHorizontal, Pencil, AtSign } from "lucide-react";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger
} from "@tutti-os/ui-system";
import type {
AgentGuiWorkbenchSessionAction,
AgentGuiWorkbenchSessionMenuCopy
} from "./commands.ts";
const menuContentClassName =
"w-max min-w-44 nodrag [-webkit-app-region:no-drag]";
export interface AgentGuiWorkbenchSessionMenuAdditionalAction {
id: string;
label: string;
icon?: ReactNode;
onSelect: () => void;
}
export interface AgentGuiWorkbenchSessionMenuProps {
actions?: readonly AgentGuiWorkbenchSessionAction[];
additionalActions?: readonly AgentGuiWorkbenchSessionMenuAdditionalAction[];
copy: AgentGuiWorkbenchSessionMenuCopy;
onAction: (action: AgentGuiWorkbenchSessionAction) => void;
}
export function AgentGuiWorkbenchSessionMenu({
actions = ["rename", "copy-markdown", "copy-reference"],
additionalActions = [],
copy,
onAction
}: AgentGuiWorkbenchSessionMenuProps): ReactNode {
const showRename = actions.includes("rename");
const showCopyMarkdown = actions.includes("copy-markdown");
const showCopyReference = actions.includes("copy-reference");
const showCopyActions = showCopyMarkdown || showCopyReference;
const showBuiltInActions = showRename || showCopyActions;
const pendingActionRef = useRef(false);
const select = useCallback((onSelect: () => void) => {
if (pendingActionRef.current) {
return;
}
pendingActionRef.current = true;
// Radix renders the menu through a React portal. Portal events still
// bubble through the React tree, so dispatching synchronously from
// pointerup can update/unmount header chrome while its draggable region
// is handling the same gesture. Let the menu gesture finish first.
window.requestAnimationFrame(() => {
pendingActionRef.current = false;
onSelect();
});
}, []);
const actionProps = useCallback(
(onSelect: () => void) => ({
onClick: () => select(onSelect),
onPointerUp: (event: PointerEvent) => {
if (event.button === 0) {
select(onSelect);
}
},
onSelect: () => select(onSelect)
}),
[select]
);
return (
<DropdownMenu
onOpenChange={(open) => {
if (open) {
pendingActionRef.current = false;
}
}}
>
<DropdownMenuTrigger asChild>
<Button
aria-label={copy.moreSessionActions}
title={copy.moreSessionActions}
className="agent-gui-workbench-header__icon-button agent-gui-workbench-header__session-menu-trigger"
data-testid="agent-gui-session-menu-trigger"
size="icon-sm"
type="button"
variant="ghost"
onClick={(event) => event.stopPropagation()}
onDoubleClick={(event) => event.stopPropagation()}
onPointerDown={(event) => event.stopPropagation()}
>
<MoreHorizontal
aria-hidden="true"
className="agent-gui-workbench-header__icon"
/>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className={menuContentClassName}
onClick={stopMousePropagation}
onDoubleClick={stopMousePropagation}
onPointerDown={stopPointerPropagation}
onPointerUp={stopPointerPropagation}
sideOffset={6}
>
{additionalActions.map((action) => (
<DropdownMenuItem key={action.id} {...actionProps(action.onSelect)}>
{action.icon}
<span>{action.label}</span>
</DropdownMenuItem>
))}
{additionalActions.length > 0 && showBuiltInActions ? (
<DropdownMenuSeparator />
) : null}
{showRename ? (
<DropdownMenuItem {...actionProps(() => onAction("rename"))}>
<Pencil aria-hidden="true" />
<span>{copy.renameSession}</span>
</DropdownMenuItem>
) : null}
{showRename && showCopyActions ? <DropdownMenuSeparator /> : null}
{showCopyMarkdown ? (
<DropdownMenuItem {...actionProps(() => onAction("copy-markdown"))}>
<FileText aria-hidden="true" />
<span>{copy.copyAsMarkdown}</span>
</DropdownMenuItem>
) : null}
{showCopyReference ? (
<DropdownMenuItem {...actionProps(() => onAction("copy-reference"))}>
<AtSign aria-hidden="true" />
<span>{copy.copyAsReference}</span>
</DropdownMenuItem>
) : null}
</DropdownMenuContent>
</DropdownMenu>
);
}
function stopMousePropagation(event: MouseEvent): void {
event.stopPropagation();
}
function stopPointerPropagation(event: PointerEvent): void {
event.stopPropagation();
}