From 496fde5613fc8ba24962f5f2349c01e972481e93 Mon Sep 17 00:00:00 2001 From: inj-src Date: Sat, 28 Mar 2026 10:17:28 +0000 Subject: [PATCH] fix(web): compact chat header actions into an overflow menu --- apps/web/src/components/GitActionsControl.tsx | 54 ++++++++++- apps/web/src/components/chat/ChatHeader.tsx | 92 +++++++++++++++++-- 2 files changed, 139 insertions(+), 7 deletions(-) diff --git a/apps/web/src/components/GitActionsControl.tsx b/apps/web/src/components/GitActionsControl.tsx index 1593a151da..92c54d680a 100644 --- a/apps/web/src/components/GitActionsControl.tsx +++ b/apps/web/src/components/GitActionsControl.tsx @@ -54,6 +54,7 @@ import { readNativeApi } from "~/nativeApi"; interface GitActionsControlProps { gitCwd: string | null; activeThreadId: ThreadId | null; + inMenu?: boolean; } interface PendingDefaultBranchAction { @@ -203,7 +204,11 @@ function GitQuickActionIcon({ quickAction }: { quickAction: GitQuickAction }) { return ; } -export default function GitActionsControl({ gitCwd, activeThreadId }: GitActionsControlProps) { +export default function GitActionsControl({ + gitCwd, + activeThreadId, + inMenu, +}: GitActionsControlProps) { const threadToastData = useMemo( () => (activeThreadId ? { threadId: activeThreadId } : undefined), [activeThreadId], @@ -738,6 +743,53 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions if (!gitCwd) return null; + if (inMenu) { + return ( + <> + {gitActionMenuItems.map((item) => { + const disabledReason = getMenuActionDisabledReason({ + item, + gitStatus: gitStatusForActions, + isBusy: isGitActionRunning, + hasOriginRemote, + }); + if (item.disabled && disabledReason) { + return ( + + } + > + + + {item.label} + + + + {disabledReason} + + + ); + } + + return ( + { + openDialogForMenuItem(item); + }} + > + + {item.label} + + ); + })} + + ); + } + return ( <> {!isRepo ? ( diff --git a/apps/web/src/components/chat/ChatHeader.tsx b/apps/web/src/components/chat/ChatHeader.tsx index f04c9879fa..2b243f4a76 100644 --- a/apps/web/src/components/chat/ChatHeader.tsx +++ b/apps/web/src/components/chat/ChatHeader.tsx @@ -4,15 +4,20 @@ import { type ResolvedKeybindingsConfig, type ThreadId, } from "@t3tools/contracts"; -import { memo } from "react"; +import { EllipsisIcon, DiffIcon, TerminalSquareIcon, PlayIcon, FolderOpenIcon } from "lucide-react"; +import { memo, useEffect, useRef, useState } from "react"; import GitActionsControl from "../GitActionsControl"; -import { DiffIcon, TerminalSquareIcon } from "lucide-react"; import { Badge } from "../ui/badge"; +import { Button } from "../ui/button"; +import { Menu, MenuItem, MenuPopup, MenuSeparator as MenuDivider, MenuTrigger } from "../ui/menu"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import ProjectScriptsControl, { type NewProjectScriptInput } from "../ProjectScriptsControl"; import { Toggle } from "../ui/toggle"; import { SidebarTrigger } from "../ui/sidebar"; import { OpenInPicker } from "./OpenInPicker"; +import { readNativeApi } from "~/nativeApi"; + +const HEADER_ACTIONS_COMPACT_BREAKPOINT_PX = 860; interface ChatHeaderProps { activeThreadId: ThreadId; @@ -61,8 +66,35 @@ export const ChatHeader = memo(function ChatHeader({ onToggleTerminal, onToggleDiff, }: ChatHeaderProps) { + const containerRef = useRef(null); + const [useOverflowPopover, setUseOverflowPopover] = useState(false); + const hasOverflowActions = activeProjectScripts !== undefined || activeProjectName !== undefined; + + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + const updateLayout = (width: number) => { + setUseOverflowPopover(width < HEADER_ACTIONS_COMPACT_BREAKPOINT_PX); + }; + + updateLayout(container.clientWidth); + + if (typeof ResizeObserver === "undefined") return; + const observer = new ResizeObserver((entries) => { + const entry = entries[0]; + if (!entry) return; + updateLayout(entry.contentRect.width); + }); + observer.observe(container); + return () => observer.disconnect(); + }, []); + return ( -
+

- {activeProjectScripts && ( + {hasOverflowActions && useOverflowPopover ? ( + + } + > + + + + {activeProjectScripts && activeProjectScripts.length > 0 && ( + <> +
+ Run script +
+ {activeProjectScripts.slice(0, 5).map((script) => ( + onRunProjectScript(script)}> + + {script.name} + + ))} + {activeProjectScripts.length > 5 && ( + + +{activeProjectScripts.length - 5} more + + )} + + + )} + {activeProjectName && ( + <> + { + const api = readNativeApi(); + if (!api || !openInCwd) return; + void api.shell.openInEditor(openInCwd, "file-manager"); + }} + > + + Open folder + + + + + )} +
+
+ ) : null} + {!useOverflowPopover && activeProjectScripts && ( )} - {activeProjectName && ( + {!useOverflowPopover && activeProjectName && ( )} - {activeProjectName && } + {!useOverflowPopover && activeProjectName && ( + + )}