fix(site): sessions list and timeline polish (#23885)

- Prompt table was collapsing and sizing improperly, fixed 
- Make pretty much everything `text-sm` and `font-normal`
- Add model filter
- Back button on session threads page now navigates back instead of
going straight to `/aibridge/sessions`

---------

Co-authored-by: Jake Howell <jake@hwll.me>
This commit is contained in:
Jeremy Ruppel
2026-04-01 14:42:08 +11:00
committed by GitHub
co-authored by Jake Howell
parent b86161e0a6
commit 1d16ff1ca6
14 changed files with 80 additions and 47 deletions
@@ -20,6 +20,7 @@ const defaultFilterProps = getDefaultFilterProps<
user: MockMenu,
provider: MockMenu,
client: MockMenu,
model: MockMenu,
},
});
@@ -49,6 +50,7 @@ export const WithQuery: Story = {
user: MockMenu,
provider: MockMenu,
client: MockMenu,
model: MockMenu,
},
used: true,
}),
@@ -62,6 +64,7 @@ export const Loading: Story = {
user: { ...MockMenu, isInitializing: true },
provider: MockMenu,
client: MockMenu,
model: MockMenu,
},
},
};
@@ -9,6 +9,10 @@ import {
ClientFilter,
type ClientFilterMenu,
} from "../RequestLogsPage/RequestLogsFilter/ClientFilter";
import {
ModelFilter,
type ModelFilterMenu,
} from "../RequestLogsPage/RequestLogsFilter/ModelFilter";
import {
ProviderFilter,
type ProviderFilterMenu,
@@ -21,6 +25,7 @@ interface ListSessionsFilterProps {
user: UserFilterMenu;
provider: ProviderFilterMenu;
client: ClientFilterMenu;
model: ModelFilterMenu;
};
}
@@ -50,6 +55,7 @@ export const ListSessionsFilter: FC<ListSessionsFilterProps> = ({
<UserMenu menu={menus.user} placeholder="All users" />
<ProviderFilter menu={menus.provider} />
<ClientFilter menu={menus.client} />
<ModelFilter menu={menus.model} />
</>
}
/>
@@ -10,6 +10,7 @@ import { RequirePermission } from "#/modules/permissions/RequirePermission";
import { pageTitle } from "#/utils/page";
import { getAIBridgePermissions } from "../getAIBridgePermissions";
import { useClientFilterMenu } from "../RequestLogsPage/RequestLogsFilter/ClientFilter";
import { useModelFilterMenu } from "../RequestLogsPage/RequestLogsFilter/ModelFilter";
import { useProviderFilterMenu } from "../RequestLogsPage/RequestLogsFilter/ProviderFilter";
import { ListSessionsPageView } from "./ListSessionsPageView";
@@ -64,6 +65,15 @@ const AISessionListPage: FC = () => {
}),
});
const modelMenu = useModelFilterMenu({
value: filter.values.model,
onChange: (option) =>
filter.update({
...filter.values,
model: option?.value,
}),
});
return (
<RequirePermission isFeatureVisible={hasPermission}>
<title>{pageTitle("Sessions", "AI Bridge")}</title>
@@ -85,6 +95,7 @@ const AISessionListPage: FC = () => {
user: userMenu,
provider: providerMenu,
client: clientMenu,
model: modelMenu,
},
}}
/>
@@ -24,6 +24,7 @@ const defaultFilterProps = getDefaultFilterProps<FilterProps>({
user: MockMenu,
provider: MockMenu,
client: MockMenu,
model: MockMenu,
},
});
@@ -43,11 +43,13 @@ const ThreadTooltip: FC<PropsWithChildren> = ({ children }) => (
<TooltipTrigger asChild>
<div className="flex-shrink-0 flex items-center">{children}</div>
</TooltipTrigger>
<TooltipContent side="top" align="end" className="max-w-xs">
<p className="text-sm">
A thread is a multi-part interaction between human and agent involving
an initial human prompt and a subsequent agentic loop.
</p>
<TooltipContent
side="top"
align="end"
className="max-w-xs text-sm font-normal"
>
A thread is a multi-part interaction between human and agent involving
an initial human prompt and a subsequent agentic loop.
</TooltipContent>
</Tooltip>
</TooltipProvider>
@@ -78,9 +80,9 @@ export const ListSessionsPageView: FC<ListSessionsPageViewProps> = ({
<ListSessionsFilter {...filterProps} />
<PaginationContainer query={sessionsQuery} paginationUnitLabel="sessions">
<Table className="text-sm">
<Table className="text-sm font-normal">
<TableHeader>
<TableRow className="text-xs">
<TableRow>
<TableHead>Last Prompt</TableHead>
<TableHead>User</TableHead>
<TableHead>Provider</TableHead>
@@ -33,14 +33,15 @@ export const ListSessionsRow: FC<ListSessionsRowProps> = ({
onClick?.();
}}
>
<TableCell className="max-w-32 flex-1 overflow-auto">
<TableCell className="max-w-32 flex-1 overflow-auto font-normal">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<p className="truncate">{session.last_prompt}</p>
</TooltipTrigger>
<TooltipContent className="max-w-64">
{session.last_prompt}
<TooltipContent className="max-w-64" side="top" align="start">
<div className="font-bold">Last prompt</div>
<div>{session.last_prompt}</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
@@ -54,7 +55,7 @@ export const ListSessionsRow: FC<ListSessionsRowProps> = ({
size="lg"
className="flex-shrink-0"
/>
<div className="font-medium truncate min-w-0 flex-1 overflow-hidden">
<div className="font-normal truncate min-w-0 flex-1 overflow-hidden">
{session.initiator.name ?? session.initiator.username}
</div>
</div>
@@ -103,7 +104,7 @@ export const ListSessionsRow: FC<ListSessionsRowProps> = ({
{session.threads}
</Badge>
</TableCell>
<TableCell className="w-48 whitespace-nowrap">
<TableCell className="w-48 whitespace-nowrap font-normal">
<div className="flex items-center justify-between">
<span>
{formatDateTime(
@@ -45,28 +45,30 @@ export const SessionSummaryTable = ({
return (
<dl
className={cn(
"text-xs text-content-secondary m-0",
"grid grid-cols-[auto_1fr] gap-y-0.5 [&_dd]:ml-0 [&_dd]:text-content-primary",
"text-sm text-content-secondary m-0 whitespace-nowrap",
"grid grid-cols-[auto_1fr] gap-y-2 [&_dd]:ml-0 [&_dd]:text-content-primary",
"[&_dd]:h-6 [&_dd]:flex [&_dd]:min-w-0 [&_dd]:items-center [&_dd]:justify-end",
"[&_dt]:h-6 [&_dt]:inline-flex [&_dt]:items-center [&_dt]:font-normal",
)}
>
<dt>Session ID</dt>
<dd className="font-mono min-w-0" title={sessionId}>
<dd className="text-xs font-mono min-w-0" title={sessionId}>
<span className="truncate w-full text-right">{sessionId}</span>
</dd>
<dt>Start time</dt>
<dd className="font-mono" title={formatDateTime(startTime)}>
<dd className="text-xs font-mono" title={formatDateTime(startTime)}>
{formatDateTime(startTime)}
</dd>
<dt>End time</dt>
<dd className="font-mono">{endTime ? formatDateTime(endTime) : "—"}</dd>
<dd className="text-xs font-mono">
{endTime ? formatDateTime(endTime) : "—"}
</dd>
<dt>Duration</dt>
<dd
className="font-mono"
className="text-xs font-mono"
title={durationInMs !== undefined ? `${durationInMs} ms` : undefined}
>
{durationInMs !== undefined
@@ -1,6 +1,6 @@
import type { FC } from "react";
import { useInfiniteQuery } from "react-query";
import { useParams } from "react-router";
import { useNavigate, useParams } from "react-router";
import { infiniteSessionThreads } from "#/api/queries/aiBridge";
import { useAuthenticated } from "#/hooks/useAuthenticated";
import { useDashboard } from "#/modules/dashboard/useDashboard";
@@ -12,6 +12,7 @@ import { SessionThreadsPageView } from "./SessionThreadsPageView";
const SessionThreadsPage: FC = () => {
const { permissions } = useAuthenticated();
const { entitlements } = useDashboard();
const navigate = useNavigate();
const { isEntitled, isEnabled, hasPermission } = getAIBridgePermissions(
entitlements,
@@ -44,6 +45,7 @@ const SessionThreadsPage: FC = () => {
onFetchNextPage={sessionQuery.fetchNextPage}
isAISessionsEnabled={isEnabled}
isAISessionsEntitled={isEntitled}
onBackClicked={() => navigate(-1)}
/>
</RequirePermission>
);
@@ -1,6 +1,5 @@
import { ArrowLeftIcon, InfoIcon } from "lucide-react";
import type { FC, PropsWithChildren } from "react";
import { Link as RouterLink } from "react-router";
import type {
AIBridgeSessionThreadsResponse,
AIBridgeThread,
@@ -27,7 +26,7 @@ const SessionSummaryTooltip: FC<PropsWithChildren> = ({ children }) => (
<TooltipContent
side="top"
align="start"
className="max-w-xs flex flex-col gap-1 text-sm p-3"
className="max-w-xs flex flex-col gap-1 text-sm font-normal p-3"
>
<p className="m-0 leading-snug">
A session is a set of threads or interceptions logically grouped by a
@@ -47,6 +46,7 @@ interface SessionThreadsPageViewProps {
onFetchNextPage: () => void;
isAISessionsEnabled: boolean;
isAISessionsEntitled: boolean;
onBackClicked: () => void;
}
export const SessionThreadsPageView: FC<SessionThreadsPageViewProps> = ({
@@ -58,6 +58,7 @@ export const SessionThreadsPageView: FC<SessionThreadsPageViewProps> = ({
onFetchNextPage,
isAISessionsEnabled,
isAISessionsEntitled,
onBackClicked,
}) => {
if (!isAISessionsEntitled) {
return <PaywallAIGovernance />;
@@ -81,19 +82,20 @@ export const SessionThreadsPageView: FC<SessionThreadsPageViewProps> = ({
variant="outline"
size="lg"
title="Back to AI Bridge sessions list"
onClick={onBackClicked}
>
<RouterLink to="/aibridge/sessions">
<span>
<ArrowLeftIcon />
Back
</RouterLink>
</span>
</Button>
</nav>
<div className="flex flex-col md:flex-row md:items-start gap-6">
<aside className="md:w-64 md:shrink-0 px-3 py-2.5 border border-solid rounded-md flex flex-col gap-1">
<aside className="md:w-80 md:shrink-0 px-3 py-2.5 border border-solid rounded-md flex flex-col gap-1">
<h2 className="text-sm font-semibold flex items-center m-0">
Session summary
<SessionSummaryTooltip>
<InfoIcon className="ml-2 text-content-secondary size-icon-sm" />
<InfoIcon className="ml-2 text-content-secondary size-icon-xs" />
</SessionSummaryTooltip>
</h2>
{loading && <Loader className="my-4" />}
@@ -21,7 +21,7 @@ export const AgenticLoopTable: FC<AgenticLoopTableProps> = ({
return (
<div
className={cn(
"text-sm text-content-secondary flex flex-col gap-1",
"text-sm text-content-secondary font-normal flex flex-col gap-1",
className,
)}
>
@@ -31,7 +31,7 @@ export const PromptTable: FC<PromptTableProps> = ({
return (
<dl
className={cn(
"text-sm text-content-secondary m-0 grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 items-center",
"text-sm text-content-secondary font-normal m-0 grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 items-center",
"[&_dt]:whitespace-nowrap py-1",
"[&_dt]:pr-4 [&_dt]:flex [&_dt]:items-center [&_dt]:h-6",
"[&_dd]:m-0 [&_dd]:min-w-0 [&_dd]:h-6",
@@ -160,7 +160,10 @@ const ThinkingBlock: FC<ThinkingBlockProps> = ({ text }) => (
<LoaderIcon className="size-icon-xs text-content-secondary" />
<span className="font-mono ml-2 text-xs">Thinking...</span>
</div>
<ExpandableText text={text} className="text-sm text-pretty m-0" />
<ExpandableText
text={text}
className="text-sm text-pretty font-normal m-0"
/>
</BracketConnector>
);
@@ -191,7 +194,7 @@ const ToolCallBlock: FC<ToolCallBlockProps> = ({
<BracketConnector contentClassName="mt-2 mr-4 border border-solid rounded-md overflow-x-auto">
<div className="flex items-center">
<CollapseButton isOpen={isOpen} onClick={() => setIsOpen(!isOpen)}>
<span className="text-sm">Tool call</span>
<span className="text-sm font-normal">Tool call</span>
<Badge size="xs" className="font-mono ml-1">
{tool}
</Badge>
@@ -236,13 +239,13 @@ const AgenticLoopCompletedBlock: FC<AgenticLoopCompletedBlockProps> = ({
>
<div className="flex items-center">
<CollapseButton isOpen={isOpen} onClick={() => setIsOpen(!isOpen)}>
<span className="text-sm">Agentic loop completed</span>
<span className="text-sm font-normal">Agentic loop completed</span>
</CollapseButton>
</div>
{isOpen && (
<div className="mb-4 ml-3 mr-4 flex flex-col gap-2 lg:w-1/2 text-sm text-content-secondary">
<div className="flex items-center justify-between">
<span className="font-medium">In / out tokens</span>
<span className="font-normal">In / out tokens</span>
<TokenBadges
inputTokens={inputTokens}
outputTokens={outputTokens}
@@ -302,9 +305,9 @@ const ThreadItem: FC<ThreadItemProps> = ({ thread, initiator }) => {
return (
<>
<div className="border border-solid rounded-md flex flex-col lg:flex-row gap-6 p-2">
<div className="border border-solid rounded-md flex flex-col items-start w-full lg:w-auto lg:flex-row gap-6 p-2">
{/* left column: avatar and username */}
<div className="flex flex-row items-items-start gap-1">
<div className="flex flex-row items-center gap-1">
<Avatar
src={initiator.avatar_url}
fallback={initiator.name ?? initiator.username}
@@ -323,7 +326,7 @@ const ThreadItem: FC<ThreadItemProps> = ({ thread, initiator }) => {
<div className="text-sm text-content-secondary font-normal my-1">
Prompt
</div>
<p className="text-sm text-content-secondary bg-surface-secondary leading-relaxed rounded-md p-3 overflow-auto m-0 text-pretty">
<p className="text-sm text-content-secondary font-normal bg-surface-secondary leading-relaxed rounded-md p-3 overflow-auto m-0 text-pretty">
{thread.prompt}
</p>
</>
@@ -332,7 +335,7 @@ const ThreadItem: FC<ThreadItemProps> = ({ thread, initiator }) => {
{/* right column: details */}
<PromptTable
className="lg:max-w-64 flex-shrink-0"
className="lg:max-w-64 flex-shrink-0 w-full lg:w-auto"
timestamp={new Date(thread.started_at)}
model={thread.model}
inputTokens={thread.token_usage.input_tokens}
@@ -352,7 +355,7 @@ const ThreadItem: FC<ThreadItemProps> = ({ thread, initiator }) => {
isOpen={agenticLoopOpen}
onClick={() => setAgenticLoopOpen(!agenticLoopOpen)}
>
<span className="text-sm">Agentic loop</span>
<span className="text-sm font-normal">Agentic loop</span>
</CollapseButton>
</div>
@@ -440,7 +443,7 @@ export const SessionTimeline: FC<SessionTimelineProps> = ({
/>
</div>
<div className="row-start-1 col-start-4 col-span-2 flex items-center h-10">
<span className="text-content-secondary ml-4 py-1 text-sm">
<span className="text-content-secondary font-normal ml-4 py-1 text-sm">
Session started
</span>
</div>
@@ -456,7 +459,7 @@ export const SessionTimeline: FC<SessionTimelineProps> = ({
</div>
{/* row 3/4: AI Governance tooltip */}
<div className="row-start-3 col-start-5 row-span-2 flex items-center text-sm text-content-secondary px-2 pt-1">
<div className="row-start-3 col-start-5 row-span-2 flex items-center text-sm text-content-secondary font-normal px-2 pt-1">
AI Governance
<TooltipProvider>
<Tooltip>
@@ -464,11 +467,11 @@ export const SessionTimeline: FC<SessionTimelineProps> = ({
<InfoIcon className="size-icon-sm p-0.5 ml-1" />
</TooltipTrigger>
<TooltipContent
className="max-w-64 text-sm"
className="max-w-80 text-sm font-normal"
align="end"
side="top"
>
<div className="text-content-secondary font-medium mb-1">
<div className="text-content-secondary mb-1">
Controls and logs AI tooling so AI use stays secure,
compliant, and visible.
</div>
@@ -554,7 +557,7 @@ export const SessionTimeline: FC<SessionTimelineProps> = ({
/>
</div>
<div className="row-start-8 col-start-4 flex items-center">
<span className="text-content-success ml-4 text-sm py-1">
<span className="text-content-success font-normal ml-4 text-sm py-1">
Session completed
</span>
</div>
@@ -25,7 +25,7 @@ export const ToolCallTable: FC<ToolCallTableProps> = ({
<div
className={cn(
className,
"flex flex-col gap-2 text-sm text-content-secondary",
"flex flex-col gap-2 text-sm text-content-secondary font-normal",
)}
>
<div className="flex items-center justify-between whitespace-nowrap">
+5 -5
View File
@@ -55,8 +55,8 @@ export const TokenBadges: FC<TokenBadgesProps> = ({
</span>
</div>
<div className="flex items-center justify-between gap-4">
<div className="text-xs text-content-secondary">Input</div>
<div className="text-xs text-content-secondary">
<div className="text-sm text-content-secondary">Input</div>
<div className="text-sm text-content-secondary">
{inputTokens}
</div>
</div>
@@ -70,8 +70,8 @@ export const TokenBadges: FC<TokenBadgesProps> = ({
</span>
</div>
<div className="flex items-center justify-between gap-4">
<div className="text-xs text-content-secondary">Output</div>
<div className="text-xs text-content-secondary">
<div className="text-sm text-content-secondary">Output</div>
<div className="text-sm text-content-secondary">
{outputTokens}
</div>
</div>
@@ -82,7 +82,7 @@ export const TokenBadges: FC<TokenBadgesProps> = ({
<div className="text-content-primary text-sm mt-4">
Token usage metadata
</div>
<pre className="mt-2 p-4 bg-surface-secondary rounded text-xs overflow-x-auto">
<pre className="mt-2 mb-1 p-4 bg-surface-secondary rounded overflow-x-auto">
<JsonPrettyPrinter input={JSON.stringify(tokenUsageMetadata)} />
</pre>
</>