diff --git a/site/.storybook/preview.tsx b/site/.storybook/preview.tsx index a864321530..ebfc1b383c 100644 --- a/site/.storybook/preview.tsx +++ b/site/.storybook/preview.tsx @@ -50,6 +50,19 @@ export const parameters: Parameters = { }, type: "mobile", }, + // Approximates a 1440x900 desktop viewed at 200% browser zoom, + // which collapses the CSS viewport to 720x450. Used by stories + // that verify the desktop layout still renders at common zoom + // levels. Below the Tailwind sm: breakpoint (640 px), the + // AgentsPage collapses into the mobile stack, so 720 px stays + // on the desktop branch. + desktopZoom200: { + name: "Desktop @ 200% zoom (720x450)", + styles: { + height: "450px", + width: "720px", + }, + }, terminal: { name: "Terminal", styles: { diff --git a/site/src/pages/AgentsPage/AgentsPageView.stories.tsx b/site/src/pages/AgentsPage/AgentsPageView.stories.tsx index 930e415a3d..f68f0f64b6 100644 --- a/site/src/pages/AgentsPage/AgentsPageView.stories.tsx +++ b/site/src/pages/AgentsPage/AgentsPageView.stories.tsx @@ -1,7 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import dayjs from "dayjs"; import { type ComponentProps, useState } from "react"; -import { Navigate } from "react-router"; +import { Navigate, useOutletContext } from "react-router"; import { expect, fn, @@ -35,8 +35,9 @@ import AgentSettingsInstructionsPage from "./AgentSettingsInstructionsPage"; import AgentSettingsLifecyclePage from "./AgentSettingsLifecyclePage"; import AgentSettingsPage from "./AgentSettingsPage"; import AgentSettingsSpendPage from "./AgentSettingsSpendPage"; -import { AgentsPageView } from "./AgentsPageView"; +import { type AgentsOutletContext, AgentsPageView } from "./AgentsPageView"; import type { ModelSelectorOption } from "./components/ChatElements"; +import { ChatTopBar } from "./components/ChatTopBar"; const defaultModelConfigID = "model-config-1"; @@ -213,6 +214,32 @@ const agentsRouting = { ], }; +const AgentTopBarRouteElement = () => { + const { isSidebarCollapsed, onToggleSidebarCollapsed } = + useOutletContext(); + return ( + + ); +}; + +const agentsWithChatTopBarRouting = { + ...agentsRouting, + children: agentsRouting.children.map((route) => + "path" in route && route.path === ":agentId" + ? { ...route, element: } + : route, + ), +}; + const defaultArgs: ComponentProps = { agentId: undefined, chatList: [], @@ -497,6 +524,92 @@ export const WithToolbarEndContent: Story = { }, }; +export const EmptyStateZoom200Desktop: Story = { + parameters: { + viewport: { defaultViewport: "desktopZoom200" }, + chromatic: { viewports: [720] }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const layout = await canvas.findByTestId("agents-page-layout"); + const sidebar = await canvas.findByTestId("agents-sidebar-panel"); + const main = await canvas.findByTestId("agents-main-panel"); + + await waitFor(() => { + const layoutStyles = getComputedStyle(layout); + const sidebarStyles = getComputedStyle(sidebar); + const mainStyles = getComputedStyle(main); + const sidebarRect = sidebar.getBoundingClientRect(); + const mainRect = main.getBoundingClientRect(); + + expect(layoutStyles.flexDirection).toBe("row"); + expect(sidebarStyles.display).not.toBe("none"); + expect(mainStyles.display).toBe("flex"); + expect(sidebarRect.width).toBeGreaterThan(0); + expect(mainRect.width).toBeGreaterThan(0); + expect(sidebarRect.left).toBeLessThan(mainRect.left); + expect(sidebarRect.right).toBeLessThanOrEqual(mainRect.left + 1); + }); + + await expect(canvas.getByRole("link", { name: "Settings" })).toBeVisible(); + await expect(canvas.getByRole("link", { name: "New Agent" })).toBeVisible(); + await expect( + canvas.getByRole("button", { name: "Collapse sidebar" }), + ).toBeVisible(); + await expect( + canvas.getByRole("button", { name: /TestUser/ }), + ).toBeVisible(); + }, +}; + +export const CollapsedSidebarZoom200Desktop: Story = { + args: { + isSidebarCollapsed: true, + }, + parameters: { + viewport: { defaultViewport: "desktopZoom200" }, + chromatic: { viewports: [720] }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const expandButton = await canvas.findByRole("button", { + name: "Expand sidebar", + }); + + await expect(expandButton).toBeVisible(); + }, +}; + +export const CollapsedSidebarZoom200DesktopWithAgent: Story = { + args: { + agentId: "chat-1", + isSidebarCollapsed: true, + chatList: [ + buildChat({ + id: "chat-1", + title: "Collapsed sidebar agent", + updated_at: todayTimestamp, + }), + ], + }, + parameters: { + viewport: { defaultViewport: "desktopZoom200" }, + chromatic: { viewports: [720] }, + reactRouter: reactRouterParameters({ + location: { path: "/agents/chat-1" }, + routing: agentsWithChatTopBarRouting, + }), + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const expandButton = await canvas.findByRole("button", { + name: "Expand sidebar", + }); + + await expect(expandButton).toBeVisible(); + }, +}; + export const CreatingAgent: Story = { args: { isCreating: true, diff --git a/site/src/pages/AgentsPage/AgentsPageView.tsx b/site/src/pages/AgentsPage/AgentsPageView.tsx index b23a111333..f4d7787574 100644 --- a/site/src/pages/AgentsPage/AgentsPageView.tsx +++ b/site/src/pages/AgentsPage/AgentsPageView.tsx @@ -156,17 +156,21 @@ export const AgentsPageView: FC = ({ }; return ( -
+
{pageTitle("Agents")}
= ({ />
diff --git a/site/src/pages/AgentsPage/components/AgentChatInput.stories.tsx b/site/src/pages/AgentsPage/components/AgentChatInput.stories.tsx index 21b818523b..c9a936f1df 100644 --- a/site/src/pages/AgentsPage/components/AgentChatInput.stories.tsx +++ b/site/src/pages/AgentsPage/components/AgentChatInput.stories.tsx @@ -652,6 +652,10 @@ export const PlanningIndicator: Story = { planModeEnabled: true, onPlanModeToggle: fn(), }, + parameters: { + viewport: { defaultViewport: "desktopZoom200" }, + chromatic: { viewports: [720] }, + }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText("Planning")).toBeVisible(); diff --git a/site/src/pages/AgentsPage/components/AgentChatInput.tsx b/site/src/pages/AgentsPage/components/AgentChatInput.tsx index 1bc2b5eb94..a6c1721a94 100644 --- a/site/src/pages/AgentsPage/components/AgentChatInput.tsx +++ b/site/src/pages/AgentsPage/components/AgentChatInput.tsx @@ -736,8 +736,9 @@ export const AgentChatInput: FC = ({ )}
= ({ /> )} {planModeEnabled && ( - + Planning {onPlanModeToggle && ( @@ -1115,7 +1116,7 @@ export const AgentChatInput: FC = ({ * when there's no overflow but still occupies * layout space, preventing measurement flicker. */} {workspace && workspaceAgent && chatId && ( - + { + const canvas = within(canvasElement); + const orgTrigger = await canvas.findByTestId("compact-org-selector"); + const composer = await canvas.findByTestId("chat-composer"); + + const orgRect = orgTrigger.getBoundingClientRect(); + const composerRect = composer.getBoundingClientRect(); + const gap = composerRect.top - orgRect.bottom; + expect(gap).toBeGreaterThanOrEqual(0); + expect(gap).toBeLessThan(16); + }, +}; + /** * Standalone story for the org-change confirmation dialog. Renders * the ConfirmDialog directly in its open state, following the same diff --git a/site/src/pages/AgentsPage/components/AgentCreateForm.tsx b/site/src/pages/AgentsPage/components/AgentCreateForm.tsx index 3bfabaec5b..a5a6d20e92 100644 --- a/site/src/pages/AgentsPage/components/AgentCreateForm.tsx +++ b/site/src/pages/AgentsPage/components/AgentCreateForm.tsx @@ -448,8 +448,8 @@ export const AgentCreateForm: FC = ({ return ( <> -
-
+
+
{isForbidden ? ( ) : createError ? ( @@ -534,7 +534,7 @@ export const AgentCreateForm: FC = ({ {modelSelectorHelp}
) : null} -

+

{ const dispatch = (): void => { const event = { matches: desktop, - media: "(min-width: 768px)", + media: "(min-width: 640px)", } as MediaQueryListEvent; for (const listener of listeners) { listener(event); @@ -52,7 +52,7 @@ const createMatchMediaController = (initialDesktop: boolean) => { }; const matchMedia = ((query: string): MediaQueryList => { - const isDesktopQuery = /\(\s*min-width\s*:\s*768px\s*\)/.test(query); + const isDesktopQuery = /\(\s*min-width\s*:\s*640px\s*\)/.test(query); return { matches: isDesktopQuery ? desktop : false, media: query, diff --git a/site/src/pages/AgentsPage/components/AgentPageHeader.tsx b/site/src/pages/AgentsPage/components/AgentPageHeader.tsx index d4b1d577b8..c37ce8998a 100644 --- a/site/src/pages/AgentsPage/components/AgentPageHeader.tsx +++ b/site/src/pages/AgentsPage/components/AgentPageHeader.tsx @@ -63,11 +63,11 @@ export const AgentPageHeader: FC = ({ const chimeEnabled = controlledChimeEnabled ?? internalChimeEnabled; const webPush = controlledWebPush ?? internalWebPush; const [isDesktop, setIsDesktop] = useState(() => { - return window.matchMedia("(min-width: 768px)").matches; + return window.matchMedia("(min-width: 640px)").matches; }); useEffect(() => { - const mediaQuery = window.matchMedia("(min-width: 768px)"); + const mediaQuery = window.matchMedia("(min-width: 640px)"); const onMediaChange = (event: MediaQueryListEvent) => { setIsDesktop(event.matches); }; @@ -115,21 +115,21 @@ export const AgentPageHeader: FC = ({ }; return ( -

+
{mobileBack ? ( ) : ( -
+
@@ -142,14 +142,14 @@ export const AgentPageHeader: FC = ({ size="icon" onClick={onExpandSidebar} aria-label="Expand sidebar" - className="hidden h-7 w-7 min-w-0 shrink-0 md:inline-flex" + className="hidden h-7 w-7 min-w-0 shrink-0 sm:inline-flex" > )}
{children && isDesktop && ( -
{children}
+
{children}
)} {/* Mobile: meatball menu with all actions */} {!mobileBack && !isDesktop && ( @@ -159,7 +159,7 @@ export const AgentPageHeader: FC = ({ variant="subtle" size="icon" aria-label="More options" - className="h-7 w-7 text-content-secondary hover:text-content-primary md:hidden" + className="h-7 w-7 text-content-secondary hover:text-content-primary sm:hidden" > diff --git a/site/src/pages/AgentsPage/components/AgentsSkeletons.tsx b/site/src/pages/AgentsPage/components/AgentsSkeletons.tsx index e7fb815da1..18278448ad 100644 --- a/site/src/pages/AgentsPage/components/AgentsSkeletons.tsx +++ b/site/src/pages/AgentsPage/components/AgentsSkeletons.tsx @@ -29,11 +29,11 @@ function getRightPanelState(): { open: boolean; width: number } { * immediately instead of a fullscreen spinner. */ export const AgentsPageSkeleton: FC = () => ( -
-
+
+
-
+
@@ -67,7 +67,7 @@ export const AgentsPageSkeleton: FC = () => (
-
+
); diff --git a/site/src/pages/AgentsPage/components/ChatTopBar.tsx b/site/src/pages/AgentsPage/components/ChatTopBar.tsx index 75c8106509..740b5bb618 100644 --- a/site/src/pages/AgentsPage/components/ChatTopBar.tsx +++ b/site/src/pages/AgentsPage/components/ChatTopBar.tsx @@ -86,7 +86,7 @@ export const ChatTopBar: FC = ({ asChild variant="subtle" size="icon" - className="inline-flex h-7 w-7 min-w-0 shrink-0 md:hidden" + className="inline-flex h-7 w-7 min-w-0 shrink-0 sm:hidden" > = ({ size="icon" onClick={onToggleSidebarCollapsed} aria-label="Expand sidebar" - className="hidden h-7 w-7 min-w-0 shrink-0 md:inline-flex" + className="hidden h-7 w-7 min-w-0 shrink-0 sm:inline-flex" > @@ -173,10 +173,10 @@ export const ChatTopBar: FC = ({ draft={prDraft} className="!size-3.5 shrink-0" /> - + {prTitle || (prNumberMatch ? `#${prNumberMatch}` : "PR")} - + {prNumberMatch ? prNumberMatch : "PR"} diff --git a/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx b/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx index fdf4fe00b5..9d38553fa7 100644 --- a/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx +++ b/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx @@ -1079,13 +1079,13 @@ export const AgentsSidebar: FC = (props) => { {/* ── Panel 1: Chats ── */}
-
+
@@ -1140,10 +1140,10 @@ export const AgentsSidebar: FC = (props) => { viewportClassName={cn( "[mask-image:linear-gradient(to_bottom,transparent_0,black_20px,black_calc(100%-20px),transparent_100%)]", "[-webkit-mask-image:linear-gradient(to_bottom,transparent_0,black_20px,black_calc(100%-20px),transparent_100%)]", - "md:[mask-image:none] md:[-webkit-mask-image:none]", + "sm:[mask-image:none] sm:[-webkit-mask-image:none]", )} > -
+
{loadError ? (
@@ -1284,7 +1284,7 @@ export const AgentsSidebar: FC = (props) => {
-
+
@@ -1325,14 +1325,14 @@ export const AgentsSidebar: FC = (props) => { {/* ── Panel 2: Sub-navigation (Settings) ── */}
{/* Back header */} -
+
{subNavTitle} @@ -1371,7 +1371,7 @@ export const AgentsSidebar: FC = (props) => { size="icon" onClick={onCollapse} aria-label="Collapse sidebar" - className="relative z-10 hidden h-7 w-7 min-w-0 text-content-secondary hover:text-content-primary md:inline-flex" + className="relative z-10 hidden h-7 w-7 min-w-0 text-content-secondary hover:text-content-primary sm:inline-flex" >