From 4d403d1cb63a00c753c074de3a24d1e6d23ff08e Mon Sep 17 00:00:00 2001 From: shivam <91240327+shivamhwp@users.noreply.github.com> Date: Fri, 18 Sep 2026 13:24:33 +0000 Subject: [PATCH 1/9] fix(web): free up the chat header on phone screens --- apps/web/src/components/chat/ChatHeader.tsx | 96 +++++++++++++++------ apps/web/src/components/ui/popover.tsx | 4 +- 2 files changed, 71 insertions(+), 29 deletions(-) diff --git a/apps/web/src/components/chat/ChatHeader.tsx b/apps/web/src/components/chat/ChatHeader.tsx index fbebc323a950..cf42ddedf32c 100644 --- a/apps/web/src/components/chat/ChatHeader.tsx +++ b/apps/web/src/components/chat/ChatHeader.tsx @@ -11,7 +11,7 @@ import { isAtomCommandInterrupted, squashAtomCommandFailure, } from "@t3tools/client-runtime/state/runtime"; -import { ChevronDownIcon } from "lucide-react"; +import { ChevronDownIcon, EllipsisIcon } from "lucide-react"; import { memo, useCallback, @@ -47,6 +47,9 @@ import { WorkspaceBreadcrumbSeparator, } from "../WorkspaceBreadcrumb"; import { cn } from "~/lib/utils"; +import { useIsMobile } from "~/hooks/useMediaQuery"; +import { Button } from "../ui/button"; +import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover"; interface ChatHeaderProps { activeThreadEnvironmentId: EnvironmentId; @@ -155,6 +158,7 @@ export const ChatHeader = memo(function ChatHeader({ breakpoint: { value: HEADER_ACTIONS_EXPANDED_BREAKPOINT_REM, unit: "rem" }, }); }, [panelAnimationDurationMs, panelAnimationsActive]); + const isMobile = useIsMobile(); const primaryEnvironmentId = usePrimaryEnvironmentId(); const activeProjectName = activeProject?.title; const activeProjectCwd = activeProject?.workspaceRoot ?? null; @@ -312,6 +316,49 @@ export const ChatHeader = memo(function ChatHeader({ }, [commitRename], ); + // Keep the controls mounted inside the closed popover so editor shortcuts, + // pending Git actions, and their dialogs retain the same lifecycle as desktop. + const headerActions = ( + <> + {activeProjectScripts && ( +
+ {isMobile && Project actions} + +
+ )} + {showOpenInPicker && ( +
+ {isMobile && Open in editor} + +
+ )} + {activeProjectName && gitCwd && ( +
+ {isMobile && Git} + +
+ )} + + ); return (
- {activeProjectScripts && ( - - )} - {showOpenInPicker && ( - - )} - {activeProjectName && ( - + {isMobile && (activeProjectScripts || showOpenInPicker || (activeProjectName && gitCwd)) ? ( + + } + > + + + + {headerActions} + + + ) : ( + headerActions )}
diff --git a/apps/web/src/components/ui/popover.tsx b/apps/web/src/components/ui/popover.tsx index 9534112e4e72..38574c1a1d99 100644 --- a/apps/web/src/components/ui/popover.tsx +++ b/apps/web/src/components/ui/popover.tsx @@ -25,6 +25,7 @@ function PopoverPopup({ sideOffset = 4, alignOffset = 0, tooltipStyle = false, + keepMounted = false, anchor, ...props }: PopoverPrimitive.Popup.Props & { @@ -34,10 +35,11 @@ function PopoverPopup({ sideOffset?: PopoverPrimitive.Positioner.Props["sideOffset"]; alignOffset?: PopoverPrimitive.Positioner.Props["alignOffset"]; tooltipStyle?: boolean; + keepMounted?: boolean; anchor?: PopoverPrimitive.Positioner.Props["anchor"]; }) { return ( - + Date: Fri, 18 Sep 2026 13:34:11 +0000 Subject: [PATCH 2/9] fix(web): preserve header action dialogs across resizing --- apps/web/src/components/chat/ChatHeader.tsx | 71 +++++++++++++++------ 1 file changed, 50 insertions(+), 21 deletions(-) diff --git a/apps/web/src/components/chat/ChatHeader.tsx b/apps/web/src/components/chat/ChatHeader.tsx index cf42ddedf32c..97ce8df1830b 100644 --- a/apps/web/src/components/chat/ChatHeader.tsx +++ b/apps/web/src/components/chat/ChatHeader.tsx @@ -22,6 +22,7 @@ import { type KeyboardEvent as ReactKeyboardEvent, type MouseEvent as ReactMouseEvent, } from "react"; +import { createPortal } from "react-dom"; import GitActionsControl from "../GitActionsControl"; import { isTrailingDoubleClick } from "../Sidebar.logic"; import { type DraftId } from "~/composerDraftStore"; @@ -159,6 +160,27 @@ export const ChatHeader = memo(function ChatHeader({ }); }, [panelAnimationDurationMs, panelAnimationsActive]); const isMobile = useIsMobile(); + const [actionsOpen, setActionsOpen] = useState(false); + const [actionsContainer] = useState(() => { + const container = document.createElement("div"); + container.className = "contents"; + return container; + }); + // Reparent the DOM host, not the React controls: rotating a phone or resizing + // a window must not discard an unsaved script or Git dialog. + const mountInlineActions = useCallback( + (node: HTMLDivElement | null) => { + if (node && !isMobile) node.appendChild(actionsContainer); + }, + [actionsContainer, isMobile], + ); + const mountPopoverActions = useCallback( + (node: HTMLDivElement | null) => { + if (node && isMobile) node.appendChild(actionsContainer); + }, + [actionsContainer, isMobile], + ); + if (!isMobile && actionsOpen) setActionsOpen(false); const primaryEnvironmentId = usePrimaryEnvironmentId(); const activeProjectName = activeProject?.title; const activeProjectCwd = activeProject?.workspaceRoot ?? null; @@ -456,27 +478,34 @@ export const ChatHeader = memo(function ChatHeader({ "[[data-panel-animations=true]_&]:motion-safe:transition-[padding-right] [[data-panel-animations=true]_&]:motion-safe:[transition-duration:var(--panel-animation-duration)] [[data-panel-animations=true]_&]:motion-safe:ease-out", )} > - {isMobile && (activeProjectScripts || showOpenInPicker || (activeProjectName && gitCwd)) ? ( - - } - > - - - - {headerActions} - - - ) : ( - headerActions - )} + + } + > + + +
+ +
+ + {createPortal( +
{headerActions}
, + actionsContainer, + )} +
); From 74d612d1e4dc2c1e8cc7d6cae8dbfb7f615c7117 Mon Sep 17 00:00:00 2001 From: shivam <91240327+shivamhwp@users.noreply.github.com> Date: Fri, 18 Sep 2026 14:15:09 +0000 Subject: [PATCH 3/9] fix(web): align narrow headers and use full-width action menus --- apps/web/src/components/GitActionsControl.tsx | 272 +++++++++++------- .../src/components/ProjectScriptsControl.tsx | 148 ++++++---- apps/web/src/components/chat/ChatHeader.tsx | 77 ++--- apps/web/src/components/chat/OpenInPicker.tsx | 90 ++++-- apps/web/src/components/ui/menu.tsx | 4 +- apps/web/src/components/ui/popover.tsx | 4 +- 6 files changed, 377 insertions(+), 218 deletions(-) diff --git a/apps/web/src/components/GitActionsControl.tsx b/apps/web/src/components/GitActionsControl.tsx index 99db055b667b..c7a65d8a47af 100644 --- a/apps/web/src/components/GitActionsControl.tsx +++ b/apps/web/src/components/GitActionsControl.tsx @@ -79,7 +79,15 @@ import { } from "~/components/ui/dialog"; import { Group, GroupSeparator } from "~/components/ui/group"; import { Input } from "~/components/ui/input"; -import { Menu, MenuItem, MenuPopup, MenuTrigger } from "~/components/ui/menu"; +import { + Menu, + MenuItem, + MenuPopup, + MenuSub, + MenuSubTrigger, + MenuSubPopup, + MenuTrigger, +} from "~/components/ui/menu"; import { Popover, PopoverPopup, PopoverTrigger } from "~/components/ui/popover"; import { ScrollArea } from "~/components/ui/scroll-area"; import { Textarea } from "~/components/ui/textarea"; @@ -108,6 +116,7 @@ import { useOpenLink } from "~/browser/useOpenLink"; import { useOpenPrLink } from "~/lib/openPullRequestLink"; interface GitActionsControlProps { + presentation?: "toolbar" | "menu"; gitCwd: string | null; activeThreadRef: ScopedThreadRef | null; draftId?: DraftId; @@ -374,24 +383,25 @@ function GitActionItemIcon({ function GitQuickActionIcon({ quickAction, SourceControlIcon, + className = "size-3.5", }: { quickAction: GitQuickAction; + className?: string; SourceControlIcon: ReturnType["Icon"]; }) { - const iconClassName = "size-3.5"; - if (quickAction.kind === "open_pr") return ; - if (quickAction.kind === "open_publish") return ; - if (quickAction.kind === "run_pull") return ; + if (quickAction.kind === "open_pr") return ; + if (quickAction.kind === "open_publish") return ; + if (quickAction.kind === "run_pull") return ; if (quickAction.kind === "run_action") { - if (quickAction.action === "commit") return ; + if (quickAction.action === "commit") return ; if (quickAction.action === "push" || quickAction.action === "commit_push") { - return ; + return ; } - return ; + return ; } - if (quickAction.label === "Commit") return ; - if (quickAction.label === "Push") return ; - return ; + if (quickAction.label === "Commit") return ; + if (quickAction.label === "Push") return ; + return ; } interface PublishRepositoryDialogProps { @@ -944,6 +954,7 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) { } export default function GitActionsControl({ + presentation = "toolbar", gitCwd, activeThreadRef, draftId, @@ -1632,33 +1643,153 @@ export default function GitActionsControl({ const canPublishRepository = isRepo && gitStatusForActions !== null && !hasPrimaryRemote; - if (!gitCwd) return null; - - return ( + const initializeGit = () => { + void (async () => { + const result = await initAction.run(); + if (result._tag === "Success" || isAtomCommandInterrupted(result)) { + return; + } + const error = squashAtomCommandFailure(result); + toastManager.add( + stackedThreadToast({ + type: "error", + title: "Git initialization failed", + description: error instanceof Error ? error.message : "An error occurred.", + ...(threadToastData !== undefined ? { data: threadToastData } : {}), + }), + ); + })(); + }; + const gitItems = ( <> - {!isRepo ? ( - + + + ); + })} + {importMenuItems} + + + Add action + + + ); + return ( <> - {primaryScript ? ( + {presentation === "menu" ? ( + <> + {primaryScript && ( + onRunScript(primaryScript)}> + + Run {primaryScript.name} + + {shortcutLabelForCommand(keybindings, commandForProjectScript(primaryScript.id))} + + + )} + {primaryScript || importableScripts.length > 0 ? ( + setActionsMenuOpen({ scripts: open, imports: false })} + > + + + Project actions + + + {scriptItems} + + + ) : ( + + + Add project action… + + )} + + ) : primaryScript ? ( - - {scripts.map((script) => { - const shortcutLabel = shortcutLabelForCommand( - keybindings, - commandForProjectScript(script.id), - ); - return ( - onRunScript(script)} - > - - - {script.runOnWorktreeCreate ? `${script.name} (setup)` : script.name} - - - {shortcutLabel && ( - - {shortcutLabel} - - )} - - - - ); - })} - {importMenuItems} - - - Add action - - + {scriptItems} ) : importableScripts.length > 0 ? ( diff --git a/apps/web/src/components/chat/ChatHeader.tsx b/apps/web/src/components/chat/ChatHeader.tsx index 97ce8df1830b..5bd402db4600 100644 --- a/apps/web/src/components/chat/ChatHeader.tsx +++ b/apps/web/src/components/chat/ChatHeader.tsx @@ -50,7 +50,7 @@ import { import { cn } from "~/lib/utils"; import { useIsMobile } from "~/hooks/useMediaQuery"; import { Button } from "../ui/button"; -import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover"; +import { Menu, MenuPopup, MenuSeparator, MenuTrigger } from "../ui/menu"; interface ChatHeaderProps { activeThreadEnvironmentId: EnvironmentId; @@ -160,6 +160,18 @@ export const ChatHeader = memo(function ChatHeader({ }); }, [panelAnimationDurationMs, panelAnimationsActive]); const isMobile = useIsMobile(); + // Side panels can leave a desktop header narrower than a phone. + const [isNarrowHeader, setIsNarrowHeader] = useState(false); + useEffect(() => { + const container = headerActionsRef.current?.parentElement; + if (!container) return; + const update = () => setIsNarrowHeader(container.clientWidth < 512); + update(); + const observer = new ResizeObserver(update); + observer.observe(container); + return () => observer.disconnect(); + }, []); + const actionsCollapsed = isMobile || isNarrowHeader; const [actionsOpen, setActionsOpen] = useState(false); const [actionsContainer] = useState(() => { const container = document.createElement("div"); @@ -170,17 +182,17 @@ export const ChatHeader = memo(function ChatHeader({ // a window must not discard an unsaved script or Git dialog. const mountInlineActions = useCallback( (node: HTMLDivElement | null) => { - if (node && !isMobile) node.appendChild(actionsContainer); + if (node && !actionsCollapsed) node.appendChild(actionsContainer); }, - [actionsContainer, isMobile], + [actionsContainer, actionsCollapsed], ); - const mountPopoverActions = useCallback( + const mountMenuActions = useCallback( (node: HTMLDivElement | null) => { - if (node && isMobile) node.appendChild(actionsContainer); + if (node && actionsCollapsed) node.appendChild(actionsContainer); }, - [actionsContainer, isMobile], + [actionsContainer, actionsCollapsed], ); - if (!isMobile && actionsOpen) setActionsOpen(false); + if (!actionsCollapsed && actionsOpen) setActionsOpen(false); const primaryEnvironmentId = usePrimaryEnvironmentId(); const activeProjectName = activeProject?.title; const activeProjectCwd = activeProject?.workspaceRoot ?? null; @@ -338,14 +350,12 @@ export const ChatHeader = memo(function ChatHeader({ }, [commitRename], ); - // Keep the controls mounted inside the closed popover so editor shortcuts, - // pending Git actions, and their dialogs retain the same lifecycle as desktop. const headerActions = ( <> {activeProjectScripts && ( -
- {isMobile && Project actions} + <> -
+ )} {showOpenInPicker && ( -
- {isMobile && Open in editor} + <> + {actionsCollapsed && activeProjectScripts && } -
+ )} {activeProjectName && gitCwd && ( -
- {isMobile && Git} + <> + {actionsCollapsed && (activeProjectScripts || showOpenInPicker) && } -
+ )} ); @@ -474,38 +486,35 @@ export const ChatHeader = memo(function ChatHeader({ data-chat-header-actions className={cn( "flex shrink-0 items-center justify-end gap-2 @3xl/header-actions:gap-3", - rightPanelOpen ? "pr-0" : "pr-16", + rightPanelOpen ? "pr-0" : "pr-20 sm:pr-16", "[[data-panel-animations=true]_&]:motion-safe:transition-[padding-right] [[data-panel-animations=true]_&]:motion-safe:[transition-duration:var(--panel-animation-duration)] [[data-panel-animations=true]_&]:motion-safe:ease-out", )} > - - + } + render={ ); From 369d307014e8c919ef1681d28ec4df3023f9edb4 Mon Sep 17 00:00:00 2001 From: shivam <91240327+shivamhwp@users.noreply.github.com> Date: Sat, 19 Sep 2026 06:52:01 +0000 Subject: [PATCH 6/9] fix(web): visually center breadcrumb text beside project icons --- .../src/components/WorkspaceBreadcrumb.tsx | 31 +++++++++++++++++-- apps/web/src/components/chat/ChatHeader.tsx | 23 ++++++++------ 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/apps/web/src/components/WorkspaceBreadcrumb.tsx b/apps/web/src/components/WorkspaceBreadcrumb.tsx index f1c3d8b15d04..3e3beb8f39e5 100644 --- a/apps/web/src/components/WorkspaceBreadcrumb.tsx +++ b/apps/web/src/components/WorkspaceBreadcrumb.tsx @@ -26,6 +26,27 @@ interface WorkspaceBreadcrumbItemProps { readonly current?: boolean; } +export function WorkspaceBreadcrumbText({ + children, + className, +}: { + readonly children: ReactNode; + readonly className?: string; +}) { + return ( + + {children} + + ); +} + export function WorkspaceBreadcrumbItem({ children, className, @@ -45,10 +66,16 @@ export function WorkspaceBreadcrumbItem({ ); } -export function WorkspaceBreadcrumbSeparator({ className }: { readonly className?: string }) { +export function WorkspaceBreadcrumbSeparator({ + className, + children = "/", +}: { + readonly className?: string; + readonly children?: ReactNode; +}) { return ( ); } diff --git a/apps/web/src/components/chat/ChatHeader.tsx b/apps/web/src/components/chat/ChatHeader.tsx index 71b0075b1b92..1c8bc520485f 100644 --- a/apps/web/src/components/chat/ChatHeader.tsx +++ b/apps/web/src/components/chat/ChatHeader.tsx @@ -46,6 +46,7 @@ import { WorkspaceBreadcrumb, WorkspaceBreadcrumbItem, WorkspaceBreadcrumbSeparator, + WorkspaceBreadcrumbText, } from "../WorkspaceBreadcrumb"; import { cn } from "~/lib/utils"; import { useIsMobile } from "~/hooks/useMediaQuery"; @@ -421,12 +422,16 @@ export const ChatHeader = memo(function ChatHeader({ } > - {activeProjectName} + + {activeProjectName} + New thread in {activeProjectName} - + + / + ) : null} @@ -459,7 +464,9 @@ export const ChatHeader = memo(function ChatHeader({ /> } > -

{activeThreadTitle}

+

+ {activeThreadTitle} +

- {activeThreadTitle} - - } - /> + render={

} + > + {activeThreadTitle} + {activeThreadTitle} )} From 5b8a6c33838b49f167498938e46ee9164438bebf Mon Sep 17 00:00:00 2001 From: shivam <91240327+shivamhwp@users.noreply.github.com> Date: Sun, 20 Sep 2026 12:34:59 +0000 Subject: [PATCH 7/9] fix(web): align header menu text with icons --- apps/web/src/components/GitActionsControl.tsx | 17 ++++++++++------- .../src/components/ProjectScriptsControl.tsx | 15 ++++++++------- apps/web/src/components/chat/OpenInPicker.tsx | 7 ++++--- apps/web/src/components/ui/menu.tsx | 17 +++++++++++++++++ 4 files changed, 39 insertions(+), 17 deletions(-) diff --git a/apps/web/src/components/GitActionsControl.tsx b/apps/web/src/components/GitActionsControl.tsx index 7df964f28597..92c28560135e 100644 --- a/apps/web/src/components/GitActionsControl.tsx +++ b/apps/web/src/components/GitActionsControl.tsx @@ -82,6 +82,7 @@ import { Input } from "~/components/ui/input"; import { Menu, MenuItem, + MenuItemLabel, MenuPopup, MenuSub, MenuSubTrigger, @@ -1674,7 +1675,7 @@ export default function GitActionsControl({
- {item.label} + {item.label}

{disabledReason}

@@ -1694,7 +1695,7 @@ export default function GitActionsControl({ disabled > - {item.label} + {item.label} @@ -1714,7 +1715,7 @@ export default function GitActionsControl({ }} > - {item.label} + {item.label} ); })} @@ -1727,7 +1728,7 @@ export default function GitActionsControl({ }} > - Publish repository... + Publish repository... ) : null} {gitStatusForActions?.refName === null && ( @@ -1759,7 +1760,9 @@ export default function GitActionsControl({ onClick={initializeGit} > - {initAction.isPending ? "Initializing..." : "Initialize Git"} + + {initAction.isPending ? "Initializing..." : "Initialize Git"} + ) : ( <> @@ -1774,7 +1777,7 @@ export default function GitActionsControl({ quickAction={quickAction} SourceControlIcon={SourceControlIcon} /> - {quickAction.label} + {quickAction.label} {quickActionDisabledReason && (

@@ -1788,7 +1791,7 @@ export default function GitActionsControl({ > - Git actions + Git actions {gitItems} diff --git a/apps/web/src/components/ProjectScriptsControl.tsx b/apps/web/src/components/ProjectScriptsControl.tsx index a4bef0c53f7a..9736821afa67 100644 --- a/apps/web/src/components/ProjectScriptsControl.tsx +++ b/apps/web/src/components/ProjectScriptsControl.tsx @@ -28,6 +28,7 @@ import { MenuGroup, MenuGroupLabel, MenuItem, + MenuItemLabel, MenuPopup, MenuSeparator, MenuShortcut, @@ -156,7 +157,7 @@ export default function ProjectScriptsControl({ onClick={() => void importFileScript(fileScript)} > - {fileScript.name} + {fileScript.name} @@ -181,9 +182,9 @@ export default function ProjectScriptsControl({ onClick={() => onRunScript(script)} > - + {script.runOnWorktreeCreate ? `${script.name} (setup)` : script.name} - + {shortcutLabel && ( - Add action + Add action ); @@ -240,7 +241,7 @@ export default function ProjectScriptsControl({ onClick={() => onRunScript(primaryScript)} > - Run {primaryScript.name} + Run {primaryScript.name} {shortcutLabelForCommand(keybindings, commandForProjectScript(primaryScript.id))} @@ -255,7 +256,7 @@ export default function ProjectScriptsControl({ > - Project actions + Project actions {scriptItems} @@ -267,7 +268,7 @@ export default function ProjectScriptsControl({ onClick={openAddDialog} > - Add project action… + Add project action… )} diff --git a/apps/web/src/components/chat/OpenInPicker.tsx b/apps/web/src/components/chat/OpenInPicker.tsx index a6e07feb354c..0f8f8cbc45c3 100644 --- a/apps/web/src/components/chat/OpenInPicker.tsx +++ b/apps/web/src/components/chat/OpenInPicker.tsx @@ -21,6 +21,7 @@ import { Group, GroupSeparator } from "../ui/group"; import { Menu, MenuItem, + MenuItemLabel, MenuPopup, MenuShortcut, MenuSub, @@ -305,7 +306,7 @@ export const OpenInPicker = memo(function OpenInPicker({ onClick={() => openInEditor(value)} >