refactor(site): refactor AgentsPage createPortal soup (#22438)

This commit is contained in:
Danielle Maywood
2026-02-28 22:11:11 +00:00
committed by GitHub
parent 5945febf06
commit 7860b99597
9 changed files with 596 additions and 558 deletions
@@ -3,10 +3,15 @@ import {
MockWorkspace,
MockWorkspaceAgent,
} from "testHelpers/entities";
import { withAuthProvider, withWebSocket } from "testHelpers/storybook";
import {
withAuthProvider,
withDashboardProvider,
withWebSocket,
} from "testHelpers/storybook";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { API } from "api/api";
import {
chatDiffContentsKey,
chatDiffStatusKey,
chatKey,
chatModelsKey,
@@ -14,7 +19,7 @@ import {
} from "api/queries/chats";
import { workspaceByIdKey } from "api/queries/workspaces";
import type * as TypesGen from "api/typesGenerated";
import { type FC, useRef, useState } from "react";
import type { FC } from "react";
import { Outlet } from "react-router";
import { expect, spyOn, userEvent, waitFor, within } from "storybook/test";
import {
@@ -25,45 +30,25 @@ import AgentDetail from "./AgentDetail";
import type { AgentsOutletContext } from "./AgentsPage";
// ---------------------------------------------------------------------------
// Layout wrapper provides portal targets for the top-bar and right panel
// so the component can render its portaled actions menu and diff panel.
// Layout wrapper provides outlet context for the child route.
// ---------------------------------------------------------------------------
const AgentDetailLayout: FC = () => {
const topBarTitleRef = useRef<HTMLDivElement>(null);
const topBarActionsRef = useRef<HTMLDivElement>(null);
const rightPanelRef = useRef<HTMLDivElement>(null);
const [rightPanelOpen, setRightPanelOpen] = useState(false);
return (
<div className="flex h-full">
<div className="flex min-w-0 flex-1 flex-col">
<div className="flex items-center gap-2 border-b border-border px-4 py-2">
<div ref={topBarTitleRef} className="flex-1" />
<div ref={topBarActionsRef} />
</div>
<div className="flex-1 overflow-hidden">
<Outlet
context={
{
chatErrorReasons: {},
setChatErrorReason: () => {},
clearChatErrorReason: () => {},
topBarTitleRef,
topBarActionsRef,
rightPanelRef,
setRightPanelOpen,
requestArchiveAgent: () => {},
} satisfies AgentsOutletContext
}
/>
</div>
<div className="flex min-w-0 flex-1 flex-col overflow-hidden">
<Outlet
context={
{
chatErrorReasons: {},
setChatErrorReason: () => {},
clearChatErrorReason: () => {},
requestArchiveAgent: () => {},
isSidebarCollapsed: false,
onToggleSidebarCollapsed: () => {},
} satisfies AgentsOutletContext
}
/>
</div>
<div
ref={rightPanelRef}
className={
rightPanelOpen ? "w-[400px] border-l border-border" : "hidden"
}
/>
</div>
);
};
@@ -128,6 +113,21 @@ const baseChatFields = {
// Helpers
// ---------------------------------------------------------------------------
/** A small sample unified diff for stories that show the diff panel. */
const sampleDiff = `diff --git a/main.go b/main.go
index abc1234..def5678 100644
--- a/main.go
+++ b/main.go
@@ -10,6 +10,9 @@ func main() {
fmt.Println("hello")
+ fmt.Println("new feature")
+ fmt.Println("added line")
+ fmt.Println("another addition")
fmt.Println("world")
- fmt.Println("old line")
}
`;
/** Build `parameters.queries` entries for a given chat data object. */
const buildQueries = (
chatData: TypesGen.ChatWithMessages,
@@ -146,6 +146,14 @@ const buildQueries = (
changed_files: opts?.diffUrl ? 2 : 0,
} satisfies TypesGen.ChatDiffStatus,
},
{
key: chatDiffContentsKey(CHAT_ID),
data: {
chat_id: CHAT_ID,
diff: opts?.diffUrl ? sampleDiff : undefined,
pull_request_url: opts?.diffUrl,
} satisfies TypesGen.ChatDiffContents,
},
{
key: workspaceByIdKey(mockWorkspace.id),
data: mockWorkspace,
@@ -167,7 +175,7 @@ const wrapSSE = (payload: unknown): string =>
const meta: Meta<typeof AgentDetailLayout> = {
title: "pages/AgentsPage/AgentDetail",
component: AgentDetailLayout,
decorators: [withAuthProvider, withWebSocket],
decorators: [withAuthProvider, withDashboardProvider, withWebSocket],
parameters: {
layout: "fullscreen",
user: MockUserOwner,
+118 -96
View File
@@ -27,6 +27,7 @@ import {
import { useMutation, useQuery, useQueryClient } from "react-query";
import { useNavigate, useOutletContext, useParams } from "react-router";
import { toast } from "sonner";
import { cn } from "utils/cn";
import { pageTitle } from "utils/page";
import { AgentChatInput } from "./AgentChatInput";
import {
@@ -54,9 +55,11 @@ import {
parseMessagesWithMergedTools,
} from "./AgentDetail/messageParsing";
import { buildStreamTools } from "./AgentDetail/streamState";
import { AgentDetailTopBarPortals } from "./AgentDetail/TopBarPortals";
import { AgentDetailTopBar } from "./AgentDetail/TopBar";
import { useMessageWindow } from "./AgentDetail/useMessageWindow";
import type { AgentsOutletContext } from "./AgentsPage";
import { DiffRightPanel } from "./DiffRightPanel";
import { FilesChangedPanel } from "./FilesChangedPanel";
import {
getModelCatalogStatusMessage,
getModelOptionsFromCatalog,
@@ -68,8 +71,6 @@ const noopSetChatErrorReason: AgentsOutletContext["setChatErrorReason"] =
() => {};
const noopClearChatErrorReason: AgentsOutletContext["clearChatErrorReason"] =
() => {};
const noopSetRightPanelOpen: AgentsOutletContext["setRightPanelOpen"] =
() => {};
const noopRequestArchiveAgent: AgentsOutletContext["requestArchiveAgent"] =
() => {};
const lastModelConfigIDStorageKey = "agents.last-model-config-id";
@@ -471,23 +472,13 @@ const AgentDetail: FC = () => {
outletContext?.setChatErrorReason ?? noopSetChatErrorReason;
const clearChatErrorReason =
outletContext?.clearChatErrorReason ?? noopClearChatErrorReason;
const setRightPanelOpen =
outletContext?.setRightPanelOpen ?? noopSetRightPanelOpen;
const requestArchiveAgent =
outletContext?.requestArchiveAgent ?? noopRequestArchiveAgent;
const isSidebarCollapsed = outletContext?.isSidebarCollapsed ?? false;
const onToggleSidebarCollapsed =
outletContext?.onToggleSidebarCollapsed ?? (() => {});
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
// When switching between chats, reset the scroll container to the
// bottom (scrollTop 0 in a flex-col-reverse container) so the user
// always sees the most recent messages instead of a stale scroll
// position from the previous chat.
useEffect(() => {
void agentId;
if (scrollContainerRef.current) {
scrollContainerRef.current.scrollTop = 0;
}
}, [agentId]);
const chatQuery = useQuery({
...chat(agentId ?? ""),
enabled: Boolean(agentId),
@@ -523,16 +514,6 @@ const AgentDetail: FC = () => {
}
}
// Notify the parent layout about right panel visibility. This
// useEffect is necessary because we're synchronizing with state
// owned by the parent outlet, not adjusting our own state.
useEffect(() => {
setRightPanelOpen(hasDiffStatus && showDiffPanel);
return () => {
setRightPanelOpen(false);
};
}, [hasDiffStatus, setRightPanelOpen, showDiffPanel]);
const modelOptions = useMemo(
() =>
getModelOptionsFromCatalog(
@@ -786,9 +767,6 @@ const AgentDetail: FC = () => {
[promoteQueuedMutation, store],
);
const topBarTitleRef = outletContext?.topBarTitleRef;
const topBarActionsRef = outletContext?.topBarActionsRef;
const rightPanelRef = outletContext?.rightPanelRef;
const chatTitle = chatQuery.data?.chat?.title;
// Update the browser tab title when navigating to / between agents.
@@ -870,6 +848,24 @@ const AgentDetail: FC = () => {
if (chatQuery.isLoading) {
return (
<div className="relative flex h-full min-h-0 min-w-0 flex-1 flex-col">
<AgentDetailTopBar
diff={{
hasDiffStatus: false,
diffStatus: undefined,
showDiffPanel: false,
onToggleFilesChanged: () => {},
}}
workspace={{
canOpenEditors: false,
canOpenWorkspace: false,
onOpenInEditor: () => {},
onViewWorkspace: () => {},
}}
onOpenParentChat={() => {}}
onArchiveAgent={() => {}}
isSidebarCollapsed={isSidebarCollapsed}
onToggleSidebarCollapsed={onToggleSidebarCollapsed}
/>
<div className="flex h-full flex-col-reverse overflow-hidden">
<div className="px-4">
<div className="mx-auto w-full max-w-3xl py-6">
@@ -921,80 +917,106 @@ const AgentDetail: FC = () => {
if (!chatQuery.data || !agentId) {
return (
<div className="flex flex-1 items-center justify-center text-content-secondary">
Chat not found
<div className="flex h-full min-h-0 min-w-0 flex-1 flex-col">
<AgentDetailTopBar
diff={{
hasDiffStatus: false,
diffStatus: undefined,
showDiffPanel: false,
onToggleFilesChanged: () => {},
}}
workspace={{
canOpenEditors: false,
canOpenWorkspace: false,
onOpenInEditor: () => {},
onViewWorkspace: () => {},
}}
onOpenParentChat={() => {}}
onArchiveAgent={() => {}}
isSidebarCollapsed={isSidebarCollapsed}
onToggleSidebarCollapsed={onToggleSidebarCollapsed}
/>
<div className="flex flex-1 items-center justify-center text-content-secondary">
Chat not found
</div>
</div>
);
}
return (
<div className="relative flex h-full min-h-0 min-w-0 flex-1 flex-col">
<AgentDetailTopBarPortals
topBarTitleRef={topBarTitleRef}
topBarActionsRef={topBarActionsRef}
rightPanelRef={rightPanelRef}
chatTitle={chatTitle}
parentChat={parentChat}
onOpenParentChat={(chatId) => navigate(`/agents/${chatId}`)}
diff={{
hasDiffStatus,
diffStatus: diffStatusQuery.data,
showDiffPanel,
onToggleFilesChanged: () => setShowDiffPanel((prev) => !prev),
}}
workspace={{
canOpenEditors,
canOpenWorkspace,
onOpenInEditor: (editor) => {
void handleOpenInEditor(editor);
},
onViewWorkspace: handleViewWorkspace,
}}
onArchiveAgent={handleArchiveAgentAction}
shouldShowDiffPanel={shouldShowDiffPanel}
agentId={agentId}
/>
<div
aria-hidden
className="pointer-events-none absolute inset-x-0 top-0 z-10 h-6 bg-surface-primary"
style={{
maskImage:
"linear-gradient(to bottom, black 0%, rgba(0,0,0,0.6) 40%, rgba(0,0,0,0.2) 70%, transparent 100%)",
WebkitMaskImage:
"linear-gradient(to bottom, black 0%, rgba(0,0,0,0.6) 40%, rgba(0,0,0,0.2) 70%, transparent 100%)",
}}
/>
<div
ref={scrollContainerRef}
className="flex h-full flex-col-reverse overflow-y-auto [scrollbar-width:thin] [scrollbar-color:hsl(var(--surface-quaternary))_transparent]"
>
<div className="px-4">
<AgentDetailConversation
store={store}
chatID={agentId}
persistedErrorReason={
chatErrorReasons[agentId] || chatRecord?.last_error || undefined
}
compressionThreshold={compressionThreshold}
onDeleteQueuedMessage={handleDeleteQueuedMessage}
onPromoteQueuedMessage={handlePromoteQueuedMessage}
onSend={handleSend}
onInterrupt={handleInterrupt}
isInputDisabled={isInputDisabled}
isSendPending={isSubmissionPending}
isInterruptPending={interruptMutation.isPending}
hasModelOptions={hasModelOptions}
selectedModel={selectedModel}
onModelChange={setSelectedModel}
modelOptions={modelOptions}
modelSelectorPlaceholder={modelSelectorPlaceholder}
inputStatusText={inputStatusText}
modelCatalogStatusMessage={modelCatalogStatusMessage}
savingMessageId={pendingEditMessageId}
/>
<div
className={cn(
"flex min-h-0 min-w-0 flex-1",
shouldShowDiffPanel && "flex-col xl:flex-row",
)}
>
<div className="relative flex min-h-0 min-w-0 flex-1 flex-col">
<AgentDetailTopBar
chatTitle={chatTitle}
parentChat={parentChat}
onOpenParentChat={(chatId) => navigate(`/agents/${chatId}`)}
diff={{
hasDiffStatus,
diffStatus: diffStatusQuery.data,
showDiffPanel,
onToggleFilesChanged: () => setShowDiffPanel((prev) => !prev),
}}
workspace={{
canOpenEditors,
canOpenWorkspace,
onOpenInEditor: (editor) => {
void handleOpenInEditor(editor);
},
onViewWorkspace: handleViewWorkspace,
}}
onArchiveAgent={handleArchiveAgentAction}
isSidebarCollapsed={isSidebarCollapsed}
onToggleSidebarCollapsed={onToggleSidebarCollapsed}
/>
<div
aria-hidden
className="pointer-events-none absolute inset-x-0 top-0 z-10 h-6 bg-surface-primary"
style={{
maskImage:
"linear-gradient(to bottom, black 0%, rgba(0,0,0,0.6) 40%, rgba(0,0,0,0.2) 70%, transparent 100%)",
WebkitMaskImage:
"linear-gradient(to bottom, black 0%, rgba(0,0,0,0.6) 40%, rgba(0,0,0,0.2) 70%, transparent 100%)",
}}
/>
<div
ref={scrollContainerRef}
className="flex h-full flex-col-reverse overflow-y-auto [scrollbar-width:thin] [scrollbar-color:hsl(var(--surface-quaternary))_transparent]"
>
<div className="px-4">
<AgentDetailConversation
store={store}
chatID={agentId}
persistedErrorReason={
chatErrorReasons[agentId] || chatRecord?.last_error || undefined
}
compressionThreshold={compressionThreshold}
onDeleteQueuedMessage={handleDeleteQueuedMessage}
onPromoteQueuedMessage={handlePromoteQueuedMessage}
onSend={handleSend}
onInterrupt={handleInterrupt}
isInputDisabled={isInputDisabled}
isSendPending={isSubmissionPending}
isInterruptPending={interruptMutation.isPending}
hasModelOptions={hasModelOptions}
selectedModel={selectedModel}
onModelChange={setSelectedModel}
modelOptions={modelOptions}
modelSelectorPlaceholder={modelSelectorPlaceholder}
inputStatusText={inputStatusText}
modelCatalogStatusMessage={modelCatalogStatusMessage}
savingMessageId={pendingEditMessageId}
/>
</div>
</div>
</div>
<DiffRightPanel isOpen={shouldShowDiffPanel}>
<FilesChangedPanel chatId={agentId} />
</DiffRightPanel>
</div>
);
};
@@ -0,0 +1,98 @@
import { MockUserOwner } from "testHelpers/entities";
import { withAuthProvider, withDashboardProvider } from "testHelpers/storybook";
import type { Meta, StoryObj } from "@storybook/react-vite";
import type { ChatDiffStatusResponse } from "api/api";
import { AgentDetailTopBar } from "./TopBar";
const mockDiffStatus: ChatDiffStatusResponse = {
chat_id: "chat-1",
changes_requested: false,
additions: 42,
deletions: 7,
changed_files: 5,
};
const defaultProps = {
chatTitle: "Build authentication feature",
onOpenParentChat: () => {},
diff: {
hasDiffStatus: false,
diffStatus: undefined,
showDiffPanel: false,
onToggleFilesChanged: () => {},
},
workspace: {
canOpenEditors: true,
canOpenWorkspace: true,
onOpenInEditor: () => {},
onViewWorkspace: () => {},
},
onArchiveAgent: () => {},
isSidebarCollapsed: false,
onToggleSidebarCollapsed: () => {},
} satisfies React.ComponentProps<typeof AgentDetailTopBar>;
const meta: Meta<typeof AgentDetailTopBar> = {
title: "pages/AgentsPage/AgentDetail/TopBar",
component: AgentDetailTopBar,
decorators: [withAuthProvider, withDashboardProvider],
parameters: {
layout: "fullscreen",
user: MockUserOwner,
},
args: defaultProps,
};
export default meta;
type Story = StoryObj<typeof AgentDetailTopBar>;
export const Default: Story = {};
export const WithDiffStats: Story = {
args: {
diff: {
hasDiffStatus: true,
diffStatus: mockDiffStatus,
showDiffPanel: false,
onToggleFilesChanged: () => {},
},
},
};
export const WithDiffPanelOpen: Story = {
args: {
diff: {
hasDiffStatus: true,
diffStatus: mockDiffStatus,
showDiffPanel: true,
onToggleFilesChanged: () => {},
},
},
};
export const WithParentChat: Story = {
args: {
parentChat: {
id: "parent-chat-1",
owner_id: "owner-id",
last_model_config_id: "model-config-1",
title: "Set up CI/CD pipeline",
status: "completed",
last_error: null,
created_at: "2026-02-18T00:00:00.000Z",
updated_at: "2026-02-18T00:00:00.000Z",
archived: false,
},
},
};
export const SidebarCollapsed: Story = {
args: {
isSidebarCollapsed: true,
},
};
export const NoTitle: Story = {
args: {
chatTitle: undefined,
},
};
@@ -0,0 +1,218 @@
import type { ChatDiffStatusResponse } from "api/api";
import type * as TypesGen from "api/typesGenerated";
import { Button } from "components/Button/Button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "components/DropdownMenu/DropdownMenu";
import { useAuthenticated } from "hooks";
import {
ArchiveIcon,
ArrowLeftIcon,
ChevronRightIcon,
EllipsisIcon,
ExternalLinkIcon,
MonitorIcon,
PanelLeftIcon,
PanelRightCloseIcon,
PanelRightOpenIcon,
} from "lucide-react";
import { UserDropdown } from "modules/dashboard/Navbar/UserDropdown/UserDropdown";
import { useDashboard } from "modules/dashboard/useDashboard";
import type { FC } from "react";
import { useNavigate } from "react-router";
interface DiffStatsBadgeProps {
status: ChatDiffStatusResponse;
isOpen: boolean;
onToggle: () => void;
}
const DiffStatsBadge: FC<DiffStatsBadgeProps> = ({
status,
isOpen,
onToggle,
}) => {
const additions = status.additions ?? 0;
const deletions = status.deletions ?? 0;
return (
<Button
variant="subtle"
onClick={onToggle}
className="gap-3 px-2 py-1 text-content-secondary hover:text-content-primary"
>
<span className="font-mono text-sm font-semibold text-content-success">
+{additions}
</span>
<span className="font-mono text-sm font-semibold text-content-destructive">
{deletions}
</span>
{isOpen ? (
<PanelRightCloseIcon className="h-4 w-4" />
) : (
<PanelRightOpenIcon className="h-4 w-4" />
)}
</Button>
);
};
interface DiffPanelState {
hasDiffStatus: boolean;
diffStatus: ChatDiffStatusResponse | undefined;
showDiffPanel: boolean;
onToggleFilesChanged: () => void;
}
interface WorkspaceActions {
canOpenEditors: boolean;
canOpenWorkspace: boolean;
onOpenInEditor: (editor: "cursor" | "vscode") => void;
onViewWorkspace: () => void;
}
type AgentDetailTopBarProps = {
chatTitle?: string;
parentChat?: TypesGen.Chat;
onOpenParentChat: (chatId: string) => void;
diff: DiffPanelState;
workspace: WorkspaceActions;
onArchiveAgent: () => void;
isSidebarCollapsed: boolean;
onToggleSidebarCollapsed: () => void;
};
export const AgentDetailTopBar: FC<AgentDetailTopBarProps> = ({
chatTitle,
parentChat,
onOpenParentChat,
diff,
workspace,
onArchiveAgent,
isSidebarCollapsed,
onToggleSidebarCollapsed,
}) => {
const navigate = useNavigate();
const { user, signOut } = useAuthenticated();
const { appearance, buildInfo } = useDashboard();
return (
<div className="flex shrink-0 items-center gap-2 px-4 py-0.5">
{/* Mobile back button */}
<Button
variant="subtle"
size="icon"
onClick={() => navigate("/agents")}
aria-label="Back"
className="inline-flex h-7 w-7 min-w-0 shrink-0 md:hidden"
>
<ArrowLeftIcon />
</Button>
{/* Desktop expand button: visible when sidebar is manually collapsed. */}
{isSidebarCollapsed && (
<Button
variant="subtle"
size="icon"
onClick={onToggleSidebarCollapsed}
aria-label="Expand sidebar"
className="hidden h-7 w-7 min-w-0 shrink-0 md:inline-flex"
>
<PanelLeftIcon />
</Button>
)}
{/* Title area */}
<div className="flex min-w-0 flex-1 items-center">
{chatTitle && (
<div className="flex min-w-0 items-center gap-1.5">
{parentChat && (
<>
<Button
size="sm"
variant="subtle"
className="h-auto max-w-[16rem] rounded-sm px-1 py-0.5 text-xs text-content-secondary shadow-none hover:bg-transparent hover:text-content-primary"
onClick={() => onOpenParentChat(parentChat.id)}
>
<span className="truncate">{parentChat.title}</span>
</Button>
<ChevronRightIcon className="h-3.5 w-3.5 shrink-0 text-content-secondary/70" />
</>
)}
<span className="truncate text-sm text-content-primary">
{chatTitle}
</span>
</div>
)}
</div>
{/* Actions area */}
<div className="flex items-center gap-2">
{diff.hasDiffStatus && diff.diffStatus && (
<DiffStatsBadge
status={diff.diffStatus}
isOpen={diff.showDiffPanel}
onToggle={diff.onToggleFilesChanged}
/>
)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon"
variant="subtle"
className="h-7 w-7 text-content-secondary hover:text-content-primary"
aria-label="Open agent actions"
>
<EllipsisIcon className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
disabled={!workspace.canOpenEditors}
onSelect={() => {
workspace.onOpenInEditor("cursor");
}}
>
<ExternalLinkIcon className="h-3.5 w-3.5" />
Open in Cursor
</DropdownMenuItem>
<DropdownMenuItem
disabled={!workspace.canOpenEditors}
onSelect={() => {
workspace.onOpenInEditor("vscode");
}}
>
<ExternalLinkIcon className="h-3.5 w-3.5" />
Open in VS Code
</DropdownMenuItem>
<DropdownMenuItem
disabled={!workspace.canOpenWorkspace}
onSelect={workspace.onViewWorkspace}
>
<MonitorIcon className="h-3.5 w-3.5" />
View Workspace
</DropdownMenuItem>
<DropdownMenuItem
className="text-content-destructive focus:text-content-destructive"
onSelect={onArchiveAgent}
>
<ArchiveIcon className="h-3.5 w-3.5" />
Archive Agent
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex items-center [&_span]:!rounded-full [&_span]:!size-8 [&_span]:!text-xs">
<UserDropdown
user={user}
buildInfo={buildInfo}
supportLinks={
appearance.support_links?.filter(
(link) => link.location !== "navbar",
) ?? []
}
onSignOut={signOut}
/>
</div>
</div>
);
};
@@ -1,199 +0,0 @@
import type { ChatDiffStatusResponse } from "api/api";
import type * as TypesGen from "api/typesGenerated";
import { Button } from "components/Button/Button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "components/DropdownMenu/DropdownMenu";
import {
ArchiveIcon,
ChevronRightIcon,
EllipsisIcon,
ExternalLinkIcon,
MonitorIcon,
PanelRightCloseIcon,
PanelRightOpenIcon,
} from "lucide-react";
import type { FC, RefObject } from "react";
import { createPortal } from "react-dom";
import { FilesChangedPanel } from "../FilesChangedPanel";
interface DiffStatsBadgeProps {
status: ChatDiffStatusResponse;
isOpen: boolean;
onToggle: () => void;
}
const DiffStatsBadge: FC<DiffStatsBadgeProps> = ({
status,
isOpen,
onToggle,
}) => {
const additions = status.additions ?? 0;
const deletions = status.deletions ?? 0;
return (
<div
role="button"
tabIndex={0}
onClick={onToggle}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
onToggle();
}
}}
className="flex cursor-pointer items-center gap-3 px-2 py-1 text-content-secondary transition-colors hover:text-content-primary"
>
<span className="font-mono text-sm font-semibold text-content-success">
+{additions}
</span>
<span className="font-mono text-sm font-semibold text-content-destructive">
{deletions}
</span>
{isOpen ? (
<PanelRightCloseIcon className="h-4 w-4" />
) : (
<PanelRightOpenIcon className="h-4 w-4" />
)}
</div>
);
};
interface DiffPanelState {
hasDiffStatus: boolean;
diffStatus: ChatDiffStatusResponse | undefined;
showDiffPanel: boolean;
onToggleFilesChanged: () => void;
}
interface WorkspaceActions {
canOpenEditors: boolean;
canOpenWorkspace: boolean;
onOpenInEditor: (editor: "cursor" | "vscode") => void;
onViewWorkspace: () => void;
}
type AgentDetailTopBarPortalsProps = {
topBarTitleRef?: RefObject<HTMLDivElement | null>;
topBarActionsRef?: RefObject<HTMLDivElement | null>;
rightPanelRef?: RefObject<HTMLDivElement | null>;
chatTitle?: string;
parentChat?: TypesGen.Chat;
onOpenParentChat: (chatId: string) => void;
diff: DiffPanelState;
workspace: WorkspaceActions;
onArchiveAgent: () => void;
shouldShowDiffPanel: boolean;
agentId: string;
};
export const AgentDetailTopBarPortals: FC<AgentDetailTopBarPortalsProps> = ({
topBarTitleRef,
topBarActionsRef,
rightPanelRef,
chatTitle,
parentChat,
onOpenParentChat,
diff,
workspace,
onArchiveAgent,
shouldShowDiffPanel,
agentId,
}) => {
return (
<>
{chatTitle &&
topBarTitleRef?.current &&
createPortal(
<div className="flex min-w-0 items-center gap-1.5">
{parentChat && (
<>
<Button
size="sm"
variant="subtle"
className="h-auto max-w-[16rem] rounded-sm px-1 py-0.5 text-xs text-content-secondary shadow-none hover:bg-transparent hover:text-content-primary"
onClick={() => onOpenParentChat(parentChat.id)}
>
<span className="truncate">{parentChat.title}</span>
</Button>
<ChevronRightIcon className="h-3.5 w-3.5 shrink-0 text-content-secondary/70" />
</>
)}
<span className="truncate text-sm text-content-primary">
{chatTitle}
</span>
</div>,
topBarTitleRef.current,
)}
{diff.hasDiffStatus &&
diff.diffStatus &&
topBarActionsRef?.current &&
createPortal(
<DiffStatsBadge
status={diff.diffStatus}
isOpen={diff.showDiffPanel}
onToggle={diff.onToggleFilesChanged}
/>,
topBarActionsRef.current,
)}
{topBarActionsRef?.current &&
createPortal(
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon"
variant="subtle"
className="h-7 w-7 text-content-secondary hover:text-content-primary"
aria-label="Open agent actions"
>
<EllipsisIcon className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
disabled={!workspace.canOpenEditors}
onSelect={() => {
workspace.onOpenInEditor("cursor");
}}
>
<ExternalLinkIcon className="h-3.5 w-3.5" />
Open in Cursor
</DropdownMenuItem>
<DropdownMenuItem
disabled={!workspace.canOpenEditors}
onSelect={() => {
workspace.onOpenInEditor("vscode");
}}
>
<ExternalLinkIcon className="h-3.5 w-3.5" />
Open in VS Code
</DropdownMenuItem>
<DropdownMenuItem
disabled={!workspace.canOpenWorkspace}
onSelect={workspace.onViewWorkspace}
>
<MonitorIcon className="h-3.5 w-3.5" />
View Workspace
</DropdownMenuItem>
<DropdownMenuItem
className="text-content-destructive focus:text-content-destructive"
onSelect={onArchiveAgent}
>
<ArchiveIcon className="h-3.5 w-3.5" />
Archive Agent
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>,
topBarActionsRef.current,
)}
{shouldShowDiffPanel &&
rightPanelRef?.current &&
createPortal(
<FilesChangedPanel chatId={agentId} />,
rightPanelRef.current,
)}
</>
);
};
@@ -1,7 +1,6 @@
import { MockWorkspace } from "testHelpers/entities";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { API } from "api/api";
import { useRef } from "react";
import {
expect,
fn,
@@ -24,32 +23,9 @@ const modelOptions = [
const behaviorStorageKey = "agents.system-prompt";
/**
* Wrapper that creates the top-bar actions ref that AgentsEmptyState
* portals its admin button into.
*/
const AgentsEmptyStateWithPortal = (
props: Omit<
React.ComponentProps<typeof AgentsEmptyState>,
"topBarActionsRef"
>,
) => {
const topBarActionsRef = useRef<HTMLDivElement>(null);
return (
<>
<div
ref={topBarActionsRef}
data-testid="topbar-actions-host"
className="flex items-center gap-2"
/>
<AgentsEmptyState {...props} topBarActionsRef={topBarActionsRef} />
</>
);
};
const meta: Meta<typeof AgentsEmptyStateWithPortal> = {
const meta: Meta<typeof AgentsEmptyState> = {
title: "pages/AgentsPage/AgentsEmptyState",
component: AgentsEmptyStateWithPortal,
component: AgentsEmptyState,
args: {
onCreateChat: fn(),
isCreating: false,
@@ -62,6 +38,8 @@ const meta: Meta<typeof AgentsEmptyStateWithPortal> = {
modelCatalogError: undefined,
canSetSystemPrompt: true,
canManageChatModelConfigs: false,
isConfigureAgentsDialogOpen: false,
onConfigureAgentsDialogOpenChange: fn(),
},
beforeEach: () => {
localStorage.clear();
@@ -73,7 +51,7 @@ const meta: Meta<typeof AgentsEmptyStateWithPortal> = {
};
export default meta;
type Story = StoryObj<typeof AgentsEmptyStateWithPortal>;
type Story = StoryObj<typeof AgentsEmptyState>;
export const Default: Story = {};
@@ -120,18 +98,10 @@ export const WithWorkspaces: Story = {
};
export const SavesBehaviorPromptAndRestores: Story = {
play: async ({ canvasElement }) => {
const host = canvasElement.ownerDocument.querySelector(
'[data-testid="topbar-actions-host"]',
)!;
// Open the admin dialog via the portalled button.
await userEvent.click(
await within(host as HTMLElement).findByRole("button", {
name: "Admin",
}),
);
args: {
isConfigureAgentsDialogOpen: true,
},
play: async () => {
const dialog = await screen.findByRole("dialog");
const textarea = await within(dialog).findByPlaceholderText(
"Optional. Set deployment-wide instructions for all new chats.",
@@ -147,56 +117,3 @@ export const SavesBehaviorPromptAndRestores: Story = {
});
},
};
export const UsesSavedBehaviorPromptOnSend: Story = {
play: async ({ canvasElement, args }) => {
const host = canvasElement.ownerDocument.querySelector(
'[data-testid="topbar-actions-host"]',
)!;
// First, save a behavior prompt.
await userEvent.click(
await within(host as HTMLElement).findByRole("button", {
name: "Admin",
}),
);
const dialog = await screen.findByRole("dialog");
const textarea = await within(dialog).findByPlaceholderText(
"Optional. Set deployment-wide instructions for all new chats.",
);
await userEvent.type(textarea, "Use concise and actionable answers.");
await userEvent.click(within(dialog).getByRole("button", { name: "Save" }));
// Modify without saving, then close.
await userEvent.clear(textarea);
await userEvent.type(textarea, "Unsaved draft prompt");
await userEvent.click(
within(dialog).getByRole("button", { name: "Close" }),
);
// Wait for the dialog to fully close (exit animation) before
// interacting with the page content underneath.
await waitFor(() => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
// Type a chat message and send.
await userEvent.type(
screen.getByPlaceholderText(
"Ask Coder to build, fix bugs, or explore your project...",
),
"Create a README checklist",
);
await userEvent.click(screen.getByRole("button", { name: "Send" }));
await waitFor(() => {
expect(args.onCreateChat).toHaveBeenCalledWith(
expect.objectContaining({
message: "Create a README checklist",
}),
);
});
},
};
+65 -99
View File
@@ -26,7 +26,7 @@ import {
SelectValue,
} from "components/Select/Select";
import { useAuthenticated } from "hooks";
import { ArrowLeftIcon, MonitorIcon, PanelLeftIcon } from "lucide-react";
import { MonitorIcon, PanelLeftIcon } from "lucide-react";
import { UserDropdown } from "modules/dashboard/Navbar/UserDropdown/UserDropdown";
import { useDashboard } from "modules/dashboard/useDashboard";
import {
@@ -38,7 +38,6 @@ import {
useRef,
useState,
} from "react";
import { createPortal } from "react-dom";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { NavLink, Outlet, useNavigate, useParams } from "react-router";
import { toast } from "sonner";
@@ -47,7 +46,6 @@ import { pageTitle } from "utils/page";
import { AgentChatInput } from "./AgentChatInput";
import { AgentsSidebar } from "./AgentsSidebar";
import { ConfigureAgentsDialog } from "./ConfigureAgentsDialog";
import { DiffRightPanel } from "./DiffRightPanel";
import {
getModelCatalogStatusMessage,
getModelOptionsFromCatalog,
@@ -88,11 +86,9 @@ export interface AgentsOutletContext {
chatErrorReasons: Record<string, string>;
setChatErrorReason: (chatId: string, reason: string) => void;
clearChatErrorReason: (chatId: string) => void;
topBarTitleRef: React.RefObject<HTMLDivElement | null>;
topBarActionsRef: React.RefObject<HTMLDivElement | null>;
rightPanelRef: React.RefObject<HTMLDivElement | null>;
setRightPanelOpen: (isOpen: boolean) => void;
requestArchiveAgent: (chatId: string) => void;
isSidebarCollapsed: boolean;
onToggleSidebarCollapsed: () => void;
}
const AgentsPage: FC = () => {
@@ -125,7 +121,8 @@ const AgentsPage: FC = () => {
const createMutation = useMutation(createChat(queryClient));
const archiveMutation = useMutation(archiveChat(queryClient));
const [archivingChatId, setArchivingChatId] = useState<string | null>(null);
const [isRightPanelOpen, setIsRightPanelOpen] = useState(false);
const [isConfigureAgentsDialogOpen, setConfigureAgentsDialogOpen] =
useState(false);
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
const [chatErrorReasons, setChatErrorReasons] = useState<
Record<string, string>
@@ -185,9 +182,6 @@ const AgentsPage: FC = () => {
return next;
});
}, []);
const topBarTitleRef = useRef<HTMLDivElement>(null);
const topBarActionsRef = useRef<HTMLDivElement>(null);
const rightPanelRef = useRef<HTMLDivElement>(null);
const chatList = chatsQuery.data ?? [];
const requestArchiveAgent = useCallback(
async (chatId: string) => {
@@ -220,22 +214,26 @@ const AgentsPage: FC = () => {
},
[archiveMutation, queryClient, agentId, navigate, clearChatErrorReason],
);
const handleToggleSidebarCollapsed = useCallback(
() => setIsSidebarCollapsed((prev) => !prev),
[],
);
const outletContext: AgentsOutletContext = useMemo(
() => ({
chatErrorReasons,
setChatErrorReason,
clearChatErrorReason,
topBarTitleRef,
topBarActionsRef,
rightPanelRef,
setRightPanelOpen: setIsRightPanelOpen,
requestArchiveAgent,
isSidebarCollapsed,
onToggleSidebarCollapsed: handleToggleSidebarCollapsed,
}),
[
chatErrorReasons,
setChatErrorReason,
clearChatErrorReason,
requestArchiveAgent,
isSidebarCollapsed,
handleToggleSidebarCollapsed,
],
);
const handleCreateChat = async (options: CreateChatOptions) => {
@@ -360,12 +358,6 @@ const AgentsPage: FC = () => {
onNewAgent: handleNewAgent,
});
useEffect(() => {
if (!agentId) {
setIsRightPanelOpen(false);
}
}, [agentId]);
return (
<div className="flex h-full min-h-0 flex-col overflow-hidden bg-surface-primary md:flex-row">
<div
@@ -397,15 +389,15 @@ const AgentsPage: FC = () => {
<div
className={cn(
"flex min-h-0 min-w-0 bg-surface-primary",
agentId ? "flex-1" : "order-1 md:order-none flex-none md:flex-1",
isRightPanelOpen && "flex-col xl:flex-row",
"flex min-h-0 min-w-0 flex-1 flex-col bg-surface-primary",
!agentId && "order-1 md:order-none flex-none md:flex-1",
)}
>
<div className="flex min-h-0 min-w-0 flex-1 flex-col bg-surface-primary">
<div className="flex shrink-0 items-center gap-2 px-4 py-0.5">
{/* Mobile logo: visible when no agent is selected. */}
{!agentId && (
{agentId ? (
<Outlet key={agentId} context={outletContext} />
) : (
<>
<div className="flex shrink-0 items-center gap-2 px-4 py-0.5">
<NavLink
to="/workspaces"
className="inline-flex shrink-0 opacity-50 md:hidden"
@@ -420,52 +412,43 @@ const AgentsPage: FC = () => {
<CoderIcon className="h-6 w-6 fill-content-primary" />
)}
</NavLink>
)}
{/* Mobile back button: visible on mobile when an agent is selected. */}
{agentId && (
<Button
variant="subtle"
size="icon"
onClick={() => navigate("/agents")}
aria-label="Back"
className="inline-flex h-7 w-7 min-w-0 shrink-0 md:hidden"
>
<ArrowLeftIcon />
</Button>
)}
{/* Desktop expand button: visible when sidebar is manually collapsed. */}
{isSidebarCollapsed && (
<Button
variant="subtle"
size="icon"
onClick={() => setIsSidebarCollapsed(false)}
aria-label="Expand sidebar"
className="hidden h-7 w-7 min-w-0 shrink-0 md:inline-flex"
>
<PanelLeftIcon />
</Button>
)}
<div
ref={topBarTitleRef}
className="flex min-w-0 flex-1 items-center"
/>
<div ref={topBarActionsRef} className="flex items-center gap-2" />
<div className="flex items-center [&_span]:!rounded-full [&_span]:!size-8 [&_span]:!text-xs">
<UserDropdown
user={user}
buildInfo={buildInfo}
supportLinks={
appearance.support_links?.filter(
(link) => link.location !== "navbar",
) ?? []
}
onSignOut={signOut}
/>
{isSidebarCollapsed && (
<Button
variant="subtle"
size="icon"
onClick={() => setIsSidebarCollapsed(false)}
aria-label="Expand sidebar"
className="hidden h-7 w-7 min-w-0 shrink-0 md:inline-flex"
>
<PanelLeftIcon />
</Button>
)}
<div className="flex min-w-0 flex-1 items-center" />
<div className="flex items-center gap-2">
{isAgentsAdmin && (
<Button
variant="subtle"
disabled={createMutation.isPending}
className="h-8 gap-1.5 border-none bg-transparent px-1 text-[13px] shadow-none hover:bg-transparent"
onClick={() => setConfigureAgentsDialogOpen(true)}
>
Admin
</Button>
)}
</div>
<div className="flex items-center [&_span]:!rounded-full [&_span]:!size-8 [&_span]:!text-xs">
<UserDropdown
user={user}
buildInfo={buildInfo}
supportLinks={
appearance.support_links?.filter(
(link) => link.location !== "navbar",
) ?? []
}
onSignOut={signOut}
/>
</div>
</div>
</div>
{agentId ? (
<Outlet context={outletContext} />
) : (
<AgentsEmptyState
onCreateChat={handleCreateChat}
isCreating={createMutation.isPending}
@@ -478,14 +461,11 @@ const AgentsPage: FC = () => {
modelCatalogError={chatModelsQuery.error}
canSetSystemPrompt={canSetSystemPrompt}
canManageChatModelConfigs={isAgentsAdmin}
topBarActionsRef={topBarActionsRef}
isConfigureAgentsDialogOpen={isConfigureAgentsDialogOpen}
onConfigureAgentsDialogOpenChange={setConfigureAgentsDialogOpen}
/>
)}
</div>
<DiffRightPanel
ref={rightPanelRef}
isOpen={Boolean(agentId && isRightPanelOpen)}
/>
</>
)}
</div>
</div>
);
@@ -503,7 +483,8 @@ interface AgentsEmptyStateProps {
modelCatalogError: unknown;
canSetSystemPrompt: boolean;
canManageChatModelConfigs: boolean;
topBarActionsRef: React.RefObject<HTMLDivElement | null>;
isConfigureAgentsDialogOpen: boolean;
onConfigureAgentsDialogOpenChange: (open: boolean) => void;
}
export const AgentsEmptyState: FC<AgentsEmptyStateProps> = ({
@@ -518,7 +499,8 @@ export const AgentsEmptyState: FC<AgentsEmptyStateProps> = ({
modelCatalogError,
canSetSystemPrompt,
canManageChatModelConfigs,
topBarActionsRef,
isConfigureAgentsDialogOpen,
onConfigureAgentsDialogOpenChange,
}) => {
const [inputValue, setInputValue] = useState(() => {
if (typeof window === "undefined") {
@@ -595,8 +577,6 @@ export const AgentsEmptyState: FC<AgentsEmptyStateProps> = ({
useState(initialSystemPrompt);
const [systemPromptDraft, setSystemPromptDraft] =
useState(initialSystemPrompt);
const [isConfigureAgentsDialogOpen, setConfigureAgentsDialogOpen] =
useState(false);
const workspacesQuery = useQuery(workspaces({ q: "owner:me", limit: 50 }));
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState<string | null>(
() => {
@@ -720,20 +700,6 @@ export const AgentsEmptyState: FC<AgentsEmptyStateProps> = ({
return (
<div className="flex min-h-0 flex-1 items-start justify-center overflow-auto p-4 pt-12 md:h-full md:items-center md:pt-4">
{hasAdminControls &&
topBarActionsRef.current &&
createPortal(
<Button
variant="subtle"
disabled={isCreating}
className="h-8 gap-1.5 border-none bg-transparent px-1 text-[13px] shadow-none hover:bg-transparent"
onClick={() => setConfigureAgentsDialogOpen(true)}
>
Admin
</Button>,
topBarActionsRef.current,
)}
<div className="mx-auto flex w-full max-w-3xl flex-col gap-4">
{createError ? <ErrorAlert error={createError} /> : null}
{workspacesQuery.isError && (
@@ -794,7 +760,7 @@ export const AgentsEmptyState: FC<AgentsEmptyStateProps> = ({
{hasAdminControls && (
<ConfigureAgentsDialog
open={isConfigureAgentsDialogOpen}
onOpenChange={setConfigureAgentsDialogOpen}
onOpenChange={onConfigureAgentsDialogOpenChange}
canManageChatModelConfigs={canManageChatModelConfigs}
canSetSystemPrompt={canSetSystemPrompt}
systemPromptDraft={systemPromptDraft}
+36 -27
View File
@@ -45,35 +45,44 @@ export const AgentsPageSkeleton: FC = () => (
/**
* Skeleton shown while the AgentDetail chunk is loading. Mimics a
* chat conversation layout (user bubble + assistant response lines)
* inside the same scroll/padding wrapper used by the real view.
* top bar + chat conversation layout so the user sees navigable
* structure during the brief Suspense fallback.
*/
export const AgentDetailSkeleton: FC = () => (
<div className="flex h-full flex-col-reverse overflow-hidden">
<div className="px-4">
<div className="mx-auto w-full max-w-3xl py-6">
<div className="flex flex-col gap-3">
{/* User message bubble (right-aligned) */}
<div className="flex w-full justify-end">
<Skeleton className="h-10 w-2/3 rounded-lg" />
</div>
{/* Assistant response lines (left-aligned) */}
<div className="space-y-3">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-5/6" />
<Skeleton className="h-4 w-4/6" />
</div>
{/* Second user message bubble */}
<div className="mt-3 flex w-full justify-end">
<Skeleton className="h-10 w-1/2 rounded-lg" />
</div>
{/* Second assistant response */}
<div className="space-y-3">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-5/6" />
<Skeleton className="h-4 w-4/6" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/5" />
<div className="flex h-full min-h-0 min-w-0 flex-1 flex-col">
{/* Minimal skeleton top bar */}
<div className="flex shrink-0 items-center gap-2 px-4 py-2">
<Skeleton className="h-7 w-7 rounded" />
<Skeleton className="h-4 w-32" />
<div className="flex-1" />
<Skeleton className="h-8 w-8 rounded-full" />
</div>
<div className="flex h-full flex-col-reverse overflow-hidden">
<div className="px-4">
<div className="mx-auto w-full max-w-3xl py-6">
<div className="flex flex-col gap-3">
{/* User message bubble (right-aligned) */}
<div className="flex w-full justify-end">
<Skeleton className="h-10 w-2/3 rounded-lg" />
</div>
{/* Assistant response lines (left-aligned) */}
<div className="space-y-3">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-5/6" />
<Skeleton className="h-4 w-4/6" />
</div>
{/* Second user message bubble */}
<div className="mt-3 flex w-full justify-end">
<Skeleton className="h-10 w-1/2 rounded-lg" />
</div>
{/* Second assistant response */}
<div className="space-y-3">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-5/6" />
<Skeleton className="h-4 w-4/6" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/5" />
</div>
</div>
</div>
</div>
+7 -8
View File
@@ -1,6 +1,6 @@
import {
type ReactNode,
type PointerEvent as ReactPointerEvent,
type Ref,
useCallback,
useEffect,
useRef,
@@ -29,17 +29,16 @@ function loadPersistedWidth(): number {
}
interface DiffRightPanelProps {
ref?: Ref<HTMLDivElement>;
isOpen: boolean;
children?: ReactNode;
}
/**
* The right-side panel for the diff/files-changed view. Always mounted
* so the portal ref is always available (fixes blank-on-reopen). When
* closed the panel is hidden via CSS and takes no layout space. On xl+
* The right-side panel for the diff/files-changed view. When closed
* the panel is hidden via CSS and takes no layout space. On xl+
* screens the panel is horizontally resizable via a drag handle.
*/
export const DiffRightPanel = ({ ref, isOpen }: DiffRightPanelProps) => {
export const DiffRightPanel = ({ isOpen, children }: DiffRightPanelProps) => {
const [width, setWidth] = useState(loadPersistedWidth);
const isDragging = useRef(false);
const startX = useRef(0);
@@ -93,7 +92,6 @@ export const DiffRightPanel = ({ ref, isOpen }: DiffRightPanelProps) => {
return (
<div
ref={ref}
data-testid="agents-detail-right-panel"
style={
isOpen
@@ -103,7 +101,7 @@ export const DiffRightPanel = ({ ref, isOpen }: DiffRightPanelProps) => {
className={cn(
"relative min-h-0 min-w-0 border-t border-border-default bg-surface-primary",
isOpen
? "h-[42dvh] min-h-[260px] max-h-[56dvh] xl:h-auto xl:max-h-none xl:w-[var(--panel-width)] xl:min-w-[360px] xl:max-w-[960px] xl:border-l xl:border-t-0"
? "flex h-[42dvh] min-h-[260px] max-h-[56dvh] flex-col xl:h-auto xl:max-h-none xl:w-[var(--panel-width)] xl:min-w-[360px] xl:max-w-[960px] xl:border-l xl:border-t-0"
: "hidden",
)}
>
@@ -114,6 +112,7 @@ export const DiffRightPanel = ({ ref, isOpen }: DiffRightPanelProps) => {
onPointerUp={handlePointerUp}
className="absolute top-0 left-0 z-10 hidden h-full w-1 cursor-col-resize select-none transition-colors hover:bg-content-link xl:block"
/>
{children}
</div>
);
};