From 6c9f7be3cbb6265d7df0917fd62e47deffa008c9 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sat, 1 Aug 2026 13:31:31 +0100 Subject: [PATCH] feat(webapp): runs title bar and layout fixes on task and agent pages The task, scheduled task and agent pages now name their runs table with its own title bar, with the page controls beside the table they act on rather than in the top bar. Also fixes two agent page layout bugs: scrolling a wide runs table sideways dragged the charts off screen with it, and the details panel stopped short of the bottom of the window. Reverts the global tooltip max-width so longer tooltips are no longer squeezed into a column. --- .../app/components/layout/MetricsLayout.tsx | 6 +- .../app/components/primitives/Headers.tsx | 2 +- .../app/components/primitives/Table.tsx | 2 +- .../webapp/app/components/primitives/Tabs.tsx | 79 ++++++++++++++-- .../app/components/primitives/TitleBar.tsx | 26 ++++++ .../app/components/primitives/Tooltip.tsx | 2 +- .../route.tsx | 81 ++++++++-------- .../route.tsx | 8 +- .../route.tsx | 92 ++++++++++--------- .../route.tsx | 68 +++++++------- 10 files changed, 236 insertions(+), 130 deletions(-) create mode 100644 apps/webapp/app/components/primitives/TitleBar.tsx 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 ? ( + + ) : ( + + ) + } + + +