diff --git a/apps/webapp/app/components/layout/MetricsLayout.tsx b/apps/webapp/app/components/layout/MetricsLayout.tsx index 12d0fcd743c..9a41c4b2613 100644 --- a/apps/webapp/app/components/layout/MetricsLayout.tsx +++ b/apps/webapp/app/components/layout/MetricsLayout.tsx @@ -175,10 +175,12 @@ function MetricsLayoutMain({ children, scroll }: { children: ReactNode; scroll: return (
{filters} + {/* overflow-x-clip: without it `overflow-y-auto` promotes x to auto and wide content drags + the charts sideways. Wide children must scroll in their own container. */}
@@ -286,7 +288,7 @@ function MetricsLayoutFilters({ return (
diff --git a/apps/webapp/app/components/primitives/Headers.tsx b/apps/webapp/app/components/primitives/Headers.tsx index 53c9d822994..5cd3ec84559 100644 --- a/apps/webapp/app/components/primitives/Headers.tsx +++ b/apps/webapp/app/components/primitives/Headers.tsx @@ -1,6 +1,6 @@ import { cn } from "~/utils/cn"; -const headerVariants = { +export const headerVariants = { header1: { text: "font-sans text-2xl leading-5 md:leading-6 lg:leading-7 font-semibold tracking-tight", spacing: "mb-2", diff --git a/apps/webapp/app/components/primitives/Table.tsx b/apps/webapp/app/components/primitives/Table.tsx index dccb26df454..55dd2e4e200 100644 --- a/apps/webapp/app/components/primitives/Table.tsx +++ b/apps/webapp/app/components/primitives/Table.tsx @@ -181,7 +181,7 @@ type TableCellBasicProps = { type TableHeaderCellProps = TableCellBasicProps & { hiddenLabel?: boolean; tooltip?: ReactNode; - /** Extra class merged onto the tooltip content — e.g. widen it past the default max-width. */ + /** Extra class merged onto the tooltip content. */ tooltipContentClassName?: string; disableTooltipHoverableContent?: boolean; /** diff --git a/apps/webapp/app/components/primitives/Tabs.tsx b/apps/webapp/app/components/primitives/Tabs.tsx index 1128a9c267f..569e271434f 100644 --- a/apps/webapp/app/components/primitives/Tabs.tsx +++ b/apps/webapp/app/components/primitives/Tabs.tsx @@ -3,9 +3,19 @@ import { motion } from "framer-motion"; import { type ReactNode, useRef } from "react"; import { type ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys"; import { cn } from "~/utils/cn"; +import { headerVariants } from "./Headers"; import { ShortcutKey } from "./ShortcutKey"; -export type Variants = "underline" | "pipe-divider" | "segmented"; +/** `"title"` names the table below it: header2 text, filter-bar height, underline on the border. */ +export type Variants = "underline" | "pipe-divider" | "segmented" | "title"; + +/** Shared with `TitleBar` so the tabbed and tab-less bars match. */ +export const TITLE_BAR_CHROME = "flex h-10 shrink-0 gap-x-6 border-b border-grid-bright"; + +const titleTabLabel = cn(headerVariants.header2.text, "transition duration-200"); +const titleTabIndicator = "h-0.5 w-full bg-indigo-500"; +const titleTabIndicatorIdle = + "h-0.5 w-full bg-surface-control-active opacity-0 transition duration-200 group-hover:opacity-100"; export type TabsProps = { tabs: { @@ -58,6 +68,10 @@ export function TabContainer({ ); } + if (variant === "title") { + return
{children}
; + } + if (variant === "underline") { return (
{children}
@@ -117,6 +131,39 @@ export function TabLink({ ); } + if (variant === "title") { + return ( + + {({ isActive, isPending }) => { + const active = isActive || isPending; + return ( + <> +
+ + {children} + +
+ {active ? ( + + ) : ( +
+ )} + + ); + }} + + ); + } + if (variant === "pipe-divider") { return ( ) { const ref = useRef(null); @@ -197,10 +246,13 @@ export function TabButton({ }); } + const title = variant === "title"; + return ( diff --git a/apps/webapp/app/components/primitives/TitleBar.tsx b/apps/webapp/app/components/primitives/TitleBar.tsx new file mode 100644 index 00000000000..8b935093b02 --- /dev/null +++ b/apps/webapp/app/components/primitives/TitleBar.tsx @@ -0,0 +1,26 @@ +import { type ReactNode } from "react"; +import { cn } from "~/utils/cn"; +import { Header2 } from "./Headers"; +import { TITLE_BAR_CHROME } from "./Tabs"; + +/** + * Names the table below it. Bottom rule only — it doubles as the table's top edge, so render the + * table with `showTopBorder={false}`. Use `TabContainer variant="title"` for the tabbed form. + */ +export function TitleBar({ + title, + children, + className, +}: { + title: ReactNode; + /** Right-aligned controls. */ + children?: ReactNode; + className?: string; +}) { + return ( +
+ {title} + {children ?
{children}
: null} +
+ ); +} diff --git a/apps/webapp/app/components/primitives/Tooltip.tsx b/apps/webapp/app/components/primitives/Tooltip.tsx index 6f12e408c7f..cb9eaf0364d 100644 --- a/apps/webapp/app/components/primitives/Tooltip.tsx +++ b/apps/webapp/app/components/primitives/Tooltip.tsx @@ -42,7 +42,7 @@ const TooltipContent = React.forwardRef< ref={ref} sideOffset={sideOffset} className={cn( - "z-50 max-w-[230px] overflow-hidden animate-in data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1 focus-visible:outline-hidden", + "z-50 overflow-hidden animate-in data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1 focus-visible:outline-hidden", variantClasses[variant], className )} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.agents.$agentParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.agents.$agentParam/route.tsx index 76beaf297f3..5d9a86e4a25 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.agents.$agentParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.agents.$agentParam/route.tsx @@ -234,28 +234,10 @@ export default function Page() { - {/* Filters — the pinned bar under the NavBar: the TimeFilter and pagination that used to - be fused with the tabs now live here, above the charts (Queues list pattern). Left and - right clusters are child divs; the slot's baked justify-between spreads them. */}
-
- {tab === "sessions" ? ( - - - {(list) => (list ? : null)} - - - ) : ( - - - {(list) => (list ? : null)} - - - )} -
{/* Activity / LLM spend / Token charts as a fixed-height chart row (three-up), synced + @@ -315,23 +297,45 @@ export default function Page() { {/* Tabs alone on their row (Queue detail pattern), then the table below them. */} - - setTab("sessions")} - > - Sessions - - setTab("runs")} - > - Runs - - - + {/* Single child so Content's gap-2.5 can't separate the bar from the table. */} +
+ +
+ setTab("sessions")} + > + Sessions + + setTab("runs")} + > + Runs + +
+
+ {tab === "sessions" ? ( + + + {(list) => (list ? : null)} + + + ) : ( + + + {(list) => (list ? : null)} + + + )} +
+
+ +
) { - // The table flows in the page-level scroll (MetricsLayout.Root scroll="page"); a sticky header - // keeps the column labels pinned as the whole column scrolls. + // No `stickyHeader` — it drops the table's own overflow-x-auto and the charts scroll with it. return tab === "sessions" ? ( }> }> @@ -367,7 +370,7 @@ function AgentContentArea({ sessions={list.sessions} filters={list.filters} hasFilters={list.hasFilters} - stickyHeader + showTopBorder={false} /> ) : ( @@ -386,7 +389,7 @@ function AgentContentArea({ filters={list.filters} runs={list.runs} variant="dimmed" - stickyHeader + showTopBorder={false} /> ) : ( diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.agents/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.agents/route.tsx index f6723ddebaa..9c8c2c8fa5c 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.agents/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.agents/route.tsx @@ -1,10 +1,6 @@ import { Outlet } from "@remix-run/react"; -import { PageContainer } from "~/components/layout/AppLayout"; +// No PageContainer — the child pages render their own; nesting two collapses the inner one's height. export default function Page() { - return ( - - - - ); + return ; } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.scheduled.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.scheduled.$taskParam/route.tsx index 6d81de239fe..b3a2b852ed9 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.scheduled.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.scheduled.$taskParam/route.tsx @@ -28,6 +28,7 @@ import { DialogTrigger, } from "~/components/primitives/Dialog"; import { Header2 } from "~/components/primitives/Headers"; +import { TitleBar } from "~/components/primitives/TitleBar"; import { InfoPanel } from "~/components/primitives/InfoPanel"; import { NavBar, PageTitle } from "~/components/primitives/PageHeader"; import { PaginationControls } from "~/components/primitives/Pagination"; @@ -292,31 +293,15 @@ export default function Page() {
- {/* Top bar — title on the left; actions + TimeFilter + pagination on the right. - h-10 matches the right-hand sidebar header height. */} -
- Runs +
+
- - {newRunsCount > 0 ? ( - showNewRunsRef.current()} /> - ) : null} - View all runs @@ -335,11 +320,18 @@ export default function Page() { > Bulk replay… - - - {(list) => (list ? : null)} - - +
@@ -363,23 +355,39 @@ export default function Page() { {/* Runs table */} -
- }> - }> - {(list) => - list ? ( - - ) : ( - - ) - } - - +
+ {/* -mt-px absorbs the spare pixel below the handle's rule, centring the title. */} + + {newRunsCount > 0 ? ( + showNewRunsRef.current()} + /> + ) : null} + + + {(list) => (list ? : null)} + + + +
+ }> + }> + {(list) => + list ? ( + + ) : ( + + ) + } + + +
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.standard.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.standard.$taskParam/route.tsx index 12227dd66fd..15df31370d2 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.standard.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.standard.$taskParam/route.tsx @@ -20,6 +20,7 @@ import { statusColor } from "~/components/primitives/charts/statusColors"; import { CopyableText } from "~/components/primitives/CopyableText"; import { DateTime } from "~/components/primitives/DateTime"; import { Header2 } from "~/components/primitives/Headers"; +import { TitleBar } from "~/components/primitives/TitleBar"; import { NavBar, PageTitle } from "~/components/primitives/PageHeader"; import { Paragraph } from "~/components/primitives/Paragraph"; import * as Property from "~/components/primitives/PropertyTable"; @@ -234,21 +235,8 @@ export default function Page() {
- {/* Top bar — title on the left; TimeFilter + pagination on the right. - h-10 matches the right-hand sidebar header height. */} -
- Runs -
- {newRunsCount > 0 ? ( - showNewRunsRef.current()} /> - ) : null} - - - - {(list) => (list ? : null)} - - -
+
+
@@ -263,23 +251,39 @@ export default function Page() { {/* Runs table */} -
- }> - }> - {(list) => - list ? ( - - ) : ( - - ) - } - - +
+ {/* -mt-px absorbs the spare pixel below the handle's rule, centring the title. */} + + {newRunsCount > 0 ? ( + showNewRunsRef.current()} + /> + ) : null} + + + {(list) => (list ? : null)} + + + +
+ }> + }> + {(list) => + list ? ( + + ) : ( + + ) + } + + +