From 14824b09556ac3310643174ec33a405d909bb129 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sat, 1 Aug 2026 16:26:26 +0100 Subject: [PATCH] feat(webapp): fix agent overview page scroll bug + layout fixes on task and agent pages (#4454) ## Summary The task, scheduled task and agent pages now name their runs table with its own title bar, and the controls that page the table sit beside it rather than in the bar at the top of the page. The top bar keeps just the date filter. Two agent page layout bugs are fixed along the way: 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. ## Fix The charts moved because the runs table had no horizontal scroller of its own. `stickyHeader` swaps the table's `overflow-x-auto` for `overflow-visible`, so the overflow escaped up to the page scroll box, and setting only `overflow-y-auto` on that box leaves the computed `overflow-x` at `visible`, which CSS then promotes to `auto`. The chart grid is a sibling inside that box, so it scrolled too. The table now keeps its own scroller (the same rule the queues list already documents) and the page box clips x so this cannot recur. The short panel was a second `PageContainer` wrapping the agent routes. `PageContainer` is `grid-rows-[auto_1fr]`, so a lone child lands in the `auto` row and its `h-full` resolves against content height instead of the viewport. This also reverts the global tooltip `max-w-[230px]` introduced in [#4131](https://github.com/triggerdotdev/trigger.dev/pull/4131), so longer tooltips are no longer squeezed into a narrow column. ### Agent overview page showing table now scrolling CleanShot 2026-08-01 at 12 04
38@2x --- .../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 12d0fcd74..9a41c4b26 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 53c9d8229..5cd3ec845 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 dccb26df4..55dd2e4e2 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 1128a9c26..569e27143 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 000000000..8b935093b --- /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 6f12e408c..cb9eaf036 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 76beaf297..5d9a86e4a 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 f6723ddeb..9c8c2c8fa 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 6d81de239..b3a2b852e 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 12227dd66..15df31370 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 ? ( + + ) : ( + + ) + } + + +