Skip to content
Merged
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
13 changes: 13 additions & 0 deletions site/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
jaaydenh marked this conversation as resolved.
// 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: {
Expand Down
117 changes: 115 additions & 2 deletions site/src/pages/AgentsPage/AgentsPageView.stories.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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";

Expand Down Expand Up @@ -202,6 +203,32 @@ const agentsRouting = {
],
};

const AgentTopBarRouteElement = () => {
const { isSidebarCollapsed, onToggleSidebarCollapsed } =
useOutletContext<AgentsOutletContext>();
return (
<ChatTopBar
chatTitle="Collapsed sidebar agent"
panel={{ showSidebarPanel: false, onToggleSidebar: fn() }}
onArchiveAgent={fn()}
onArchiveAndDeleteWorkspace={fn()}
onRegenerateTitle={fn()}
onUnarchiveAgent={fn()}
isSidebarCollapsed={isSidebarCollapsed}
onToggleSidebarCollapsed={onToggleSidebarCollapsed}
/>
);
};

const agentsWithChatTopBarRouting = {
...agentsRouting,
children: agentsRouting.children.map((route) =>
"path" in route && route.path === ":agentId"
? { ...route, element: <AgentTopBarRouteElement /> }
: route,
),
};

const defaultArgs: ComponentProps<typeof AgentsPageView> = {
agentId: undefined,
chatList: [],
Expand Down Expand Up @@ -482,6 +509,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,
Expand Down
21 changes: 13 additions & 8 deletions site/src/pages/AgentsPage/AgentsPageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,17 +154,21 @@ export const AgentsPageView: FC<AgentsPageViewProps> = ({
};

return (
<div className="flex h-full min-h-0 flex-col overflow-hidden bg-surface-primary md:flex-row">
<div
data-testid="agents-page-layout"
className="flex h-full min-h-0 flex-col overflow-hidden bg-surface-primary sm:flex-row"
Comment thread
jaaydenh marked this conversation as resolved.
>
<title>{pageTitle("Agents")}</title>
<div
data-testid="agents-sidebar-panel"
className={cn(
"md:h-full md:w-[320px] md:min-h-0 md:border-b-0",
"sm:h-full sm:w-[320px] sm:min-h-0 sm:border-b-0",
agentId
? "hidden md:block shrink-0 h-[42dvh] min-h-[240px] border-b border-border-default"
? "hidden sm:block shrink-0 h-[42dvh] min-h-[240px] border-b border-border-default"
: isSettingsDetail || isAnalytics
? "hidden md:block shrink-0"
: "order-2 md:order-none flex-1 min-h-0 border-b border-border-default md:flex-none md:border-t-0 md:border-b-0",
isSidebarCollapsed && "md:hidden",
? "hidden sm:block shrink-0"
: "order-2 sm:order-none flex-1 min-h-0 border-b border-border-default sm:flex-none sm:border-t-0 sm:border-b-0",
isSidebarCollapsed && "sm:hidden",
Comment thread
jaaydenh marked this conversation as resolved.
Comment thread
jaaydenh marked this conversation as resolved.
)}
>
<AgentsSidebar
Expand Down Expand Up @@ -198,13 +202,14 @@ export const AgentsPageView: FC<AgentsPageViewProps> = ({
/>
</div>
<div
data-testid="agents-main-panel"
className={cn(
"min-h-0 min-w-0 flex-1 flex-col bg-surface-primary",
isSettingsIndex ? "hidden md:flex" : "flex",
isSettingsIndex ? "hidden sm:flex" : "flex",
!agentId &&
!isSettingsDetail &&
sidebarView.panel === "chats" &&
"contents md:flex md:flex-1 md:flex-col",
"contents sm:flex sm:flex-1 sm:flex-col",
)}
>
<Outlet context={outletContextValue} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,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();
Expand Down
7 changes: 4 additions & 3 deletions site/src/pages/AgentsPage/components/AgentChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -720,8 +720,9 @@ export const AgentChatInput: FC<AgentChatInputProps> = ({
)}
<div
ref={setComposerElement}
data-testid="chat-composer"
className={cn(
"rounded-2xl border border-border-default/80 bg-surface-secondary md:bg-surface-secondary/45 p-1 shadow-sm has-[textarea:focus]:ring-2 has-[textarea:focus]:ring-content-link/40",
"rounded-2xl border border-border-default/80 bg-surface-secondary sm:bg-surface-secondary/45 p-1 shadow-sm has-[textarea:focus]:ring-2 has-[textarea:focus]:ring-content-link/40",
isDragging && "ring-2 ring-content-link/40",
isEditingHistoryMessage &&
"shadow-[0_0_0_2px_hsla(var(--border-warning),0.6)]",
Expand Down Expand Up @@ -1080,7 +1081,7 @@ export const AgentChatInput: FC<AgentChatInputProps> = ({
/>
)}
{planModeEnabled && (
<span className="hidden shrink-0 items-center gap-1 rounded-full bg-surface-secondary px-2 py-0.5 text-xs font-medium text-content-secondary md:inline-flex">
<span className="hidden shrink-0 items-center gap-1 rounded-full bg-surface-secondary px-2 py-0.5 text-xs font-medium text-content-secondary sm:inline-flex">
Comment thread
jaaydenh marked this conversation as resolved.
<PencilIcon className="size-3" />
Planning
{onPlanModeToggle && (
Expand All @@ -1099,7 +1100,7 @@ export const AgentChatInput: FC<AgentChatInputProps> = ({
* when there's no overflow but still occupies
* layout space, preventing measurement flicker. */}
{workspace && workspaceAgent && chatId && (
<span className="ml-1 md:ml-0">
<span className="ml-1 sm:ml-0">
<WorkspacePill
workspace={workspace}
agent={workspaceAgent}
Expand Down
24 changes: 24 additions & 0 deletions site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,30 @@ export const WithOrganizationPicker: Story = {
},
};

export const OrgPickerTightSpacing: Story = {
parameters: {
showOrganizations: true,
organizations: [MockDefaultOrganization, MockOrganization2],
queries: [
{
key: permittedOrgsKey,
data: [MockDefaultOrganization, MockOrganization2],
},
],
},
play: async ({ canvasElement }) => {
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
Expand Down
6 changes: 3 additions & 3 deletions site/src/pages/AgentsPage/components/AgentCreateForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -412,8 +412,8 @@ export const AgentCreateForm: FC<AgentCreateFormProps> = ({

return (
<>
<div className="order-last flex min-h-0 flex-none items-end justify-center overflow-auto p-4 pb-4 md:order-none md:h-full md:flex-1 md:items-center md:pt-12">
<div className="mx-auto flex w-full max-w-3xl flex-col gap-4">
<div className="order-last flex min-h-0 flex-none items-end justify-center overflow-auto px-4 pb-4 sm:order-none sm:h-full sm:flex-1 sm:items-center">
<div className="mx-auto flex w-full max-w-3xl flex-col gap-2">
{isForbidden ? (
<ChatAccessDeniedAlert />
) : createError ? (
Expand Down Expand Up @@ -496,7 +496,7 @@ export const AgentCreateForm: FC<AgentCreateFormProps> = ({
{modelSelectorHelp}
</div>
) : null}
<p className="mt-1 text-center text-xs text-content-secondary/50">
<p className="text-center text-xs text-content-secondary/50">
<a
href={docs("/ai-coder/agents")}
target="_blank"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,15 @@ const createMatchMediaController = (initialDesktop: boolean) => {
const dispatch = (): void => {
const event = {
matches: desktop,
media: "(min-width: 768px)",
media: "(min-width: 640px)",
} as MediaQueryListEvent;
for (const listener of listeners) {
listener(event);
}
};

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,
Expand Down
16 changes: 8 additions & 8 deletions site/src/pages/AgentsPage/components/AgentPageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,11 @@ export const AgentPageHeader: FC<AgentPageHeaderProps> = ({
const chimeEnabled = controlledChimeEnabled ?? internalChimeEnabled;
const webPush = controlledWebPush ?? internalWebPush;
const [isDesktop, setIsDesktop] = useState<boolean>(() => {
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);
};
Expand Down Expand Up @@ -115,21 +115,21 @@ export const AgentPageHeader: FC<AgentPageHeaderProps> = ({
};

return (
<div className="order-first flex shrink-0 items-center gap-2 pl-4 pr-2 pt-3 pb-0.5 md:order-none md:px-4 md:py-0.5">
<div className="order-first flex shrink-0 items-center gap-2 pl-4 pr-2 pt-3 pb-0.5 sm:order-none sm:px-4 sm:py-0.5">
{mobileBack ? (
<Button
asChild
variant="subtle"
size="icon"
aria-label={mobileBack.label}
className="h-7 w-7 shrink-0 md:hidden"
className="h-7 w-7 shrink-0 sm:hidden"
>
<Link to={mobileBack.to}>
<ArrowLeftIcon />
</Link>
</Button>
) : (
<div className="inline-flex shrink-0 items-center gap-2 md:hidden">
<div className="inline-flex shrink-0 items-center gap-2 sm:hidden">
<NavLink to="/workspaces" className="inline-flex">
<ProductLogo className="size-6" />
</NavLink>
Expand All @@ -142,14 +142,14 @@ export const AgentPageHeader: FC<AgentPageHeaderProps> = ({
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"
>
<PanelLeftIcon />
</Button>
)}
<div className="min-w-0 flex-1" />
{children && isDesktop && (
<div className="hidden items-center gap-2 md:flex">{children}</div>
<div className="hidden items-center gap-2 sm:flex">{children}</div>
)}
{/* Mobile: meatball menu with all actions */}
{!mobileBack && !isDesktop && (
Expand All @@ -159,7 +159,7 @@ export const AgentPageHeader: FC<AgentPageHeaderProps> = ({
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"
>
<EllipsisIcon />
</Button>
Expand Down
Loading
Loading