Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion apps/web/src/components/GitActionsControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import { readNativeApi } from "~/nativeApi";
interface GitActionsControlProps {
gitCwd: string | null;
activeThreadId: ThreadId | null;
inMenu?: boolean;
}

interface PendingDefaultBranchAction {
Expand Down Expand Up @@ -203,7 +204,11 @@ function GitQuickActionIcon({ quickAction }: { quickAction: GitQuickAction }) {
return <InfoIcon className={iconClassName} />;
}

export default function GitActionsControl({ gitCwd, activeThreadId }: GitActionsControlProps) {
export default function GitActionsControl({
gitCwd,
activeThreadId,
inMenu,
}: GitActionsControlProps) {
const threadToastData = useMemo(
() => (activeThreadId ? { threadId: activeThreadId } : undefined),
[activeThreadId],
Expand Down Expand Up @@ -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 (
<Popover key={`${item.id}-${item.label}`}>
<PopoverTrigger
openOnHover
nativeButton={false}
render={<span className="block w-max cursor-not-allowed" />}
>
<MenuItem className="w-full" disabled>
<GitActionItemIcon icon={item.icon} />
{item.label}
</MenuItem>
</PopoverTrigger>
<PopoverPopup tooltipStyle side="left" align="center">
{disabledReason}
</PopoverPopup>
</Popover>
);
}

return (
<MenuItem
key={`${item.id}-${item.label}`}
disabled={item.disabled}
onClick={() => {
openDialogForMenuItem(item);
}}
>
<GitActionItemIcon icon={item.icon} />
{item.label}
</MenuItem>
);
})}
</>
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dialogs missing when git actions rendered in menu mode

High Severity

The inMenu render path returns early with only the menu items, but both the commit Dialog and the default-branch confirmation Dialog are only rendered in the non-inMenu return block further below. Clicking "Commit" in the overflow menu calls setIsCommitDialogOpen(true) via openDialogForMenuItem, but the dialog JSX is never mounted so nothing appears. Similarly, push/PR actions on a default branch silently set pendingDefaultBranchAction state with no confirmation dialog ever rendering, causing the action to be silently dropped.

Additional Locations (1)
Fix in Cursor Fix in Web


return (
<>
{!isRepo ? (
Expand Down
92 changes: 86 additions & 6 deletions apps/web/src/components/chat/ChatHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -61,8 +66,35 @@ export const ChatHeader = memo(function ChatHeader({
onToggleTerminal,
onToggleDiff,
}: ChatHeaderProps) {
const containerRef = useRef<HTMLDivElement | null>(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 (
<div className="@container/header-actions flex min-w-0 flex-1 items-center gap-2">
<div
ref={containerRef}
className="@container/header-actions flex min-w-0 flex-1 items-center gap-2"
>
<div className="flex min-w-0 flex-1 items-center gap-2 overflow-hidden sm:gap-3">
<SidebarTrigger className="size-7 shrink-0 md:hidden" />
<h2
Expand All @@ -83,7 +115,53 @@ export const ChatHeader = memo(function ChatHeader({
)}
</div>
<div className="flex shrink-0 items-center justify-end gap-2 @3xl/header-actions:gap-3">
{activeProjectScripts && (
{hasOverflowActions && useOverflowPopover ? (
<Menu>
<MenuTrigger
render={<Button size="icon-xs" variant="outline" aria-label="More actions" />}
>
<EllipsisIcon className="size-4" />
</MenuTrigger>
<MenuPopup align="end">
{activeProjectScripts && activeProjectScripts.length > 0 && (
<>
<div className="px-2 py-1.5 font-medium text-muted-foreground text-xs">
Run script
</div>
{activeProjectScripts.slice(0, 5).map((script) => (
<MenuItem key={script.id} onClick={() => onRunProjectScript(script)}>
<PlayIcon className="size-4 shrink-0" />
{script.name}
</MenuItem>
))}
{activeProjectScripts.length > 5 && (
<MenuItem disabled className="text-muted-foreground">
+{activeProjectScripts.length - 5} more
</MenuItem>
)}
<MenuDivider />
</>
)}
{activeProjectName && (
<>
<MenuItem
onClick={() => {
const api = readNativeApi();
if (!api || !openInCwd) return;
void api.shell.openInEditor(openInCwd, "file-manager");
}}
>
<FolderOpenIcon className="size-4 shrink-0" />
Open folder
</MenuItem>
<MenuDivider />
<GitActionsControl inMenu gitCwd={gitCwd} activeThreadId={activeThreadId} />
</>
)}
</MenuPopup>
</Menu>
) : null}
{!useOverflowPopover && activeProjectScripts && (
<ProjectScriptsControl
scripts={activeProjectScripts}
keybindings={keybindings}
Expand All @@ -94,14 +172,16 @@ export const ChatHeader = memo(function ChatHeader({
onDeleteScript={onDeleteProjectScript}
/>
)}
{activeProjectName && (
{!useOverflowPopover && activeProjectName && (
<OpenInPicker
keybindings={keybindings}
availableEditors={availableEditors}
openInCwd={openInCwd}
/>
)}
{activeProjectName && <GitActionsControl gitCwd={gitCwd} activeThreadId={activeThreadId} />}
{!useOverflowPopover && activeProjectName && (
<GitActionsControl gitCwd={gitCwd} activeThreadId={activeThreadId} />
)}
<Tooltip>
<TooltipTrigger
render={
Expand Down