mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add network call badges to AI sessions table (#27341)
Surface the total and blocked Agent Firewall network calls on the AI sessions list. Sessions that did not pass through Agent Firewall show as "Disabled". <img width="2842" height="1366" alt="image" src="https://github.com/user-attachments/assets/2a68b4a9-4d93-454d-a06e-5f0d0b734a33" /> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
507ce504f8
commit
f17d488479
@@ -49,7 +49,12 @@ not just what was called.
|
||||
|
||||
The sessions page (`http://<deployment-url>/ai-gateway/sessions`) lists all sessions in
|
||||
reverse-chronological order. Each row shows the last prompt, initiator, provider,
|
||||
client, token usage, thread count, and timestamp.
|
||||
client, token usage, network calls, thread count, and timestamp.
|
||||
|
||||
The network calls column reports the total and blocked
|
||||
[Agent Firewall](../agent-firewall/index.md) calls for the session. It shows
|
||||
`No activity` when the session made no calls, and `Disabled` when the session
|
||||
did not pass through Agent Firewall, so no network calls were monitored.
|
||||
|
||||
Select one to view its full details.
|
||||
|
||||
|
||||
@@ -106,6 +106,13 @@ export const MultipleSessions: Story = {
|
||||
cache_read_input_tokens: 800 * (i + 1),
|
||||
cache_write_input_tokens: 50 * (i + 1),
|
||||
},
|
||||
network_calls: [
|
||||
{ total: 23, blocked: 2 },
|
||||
{ total: 5, blocked: 1 },
|
||||
{ total: 0, blocked: 0 },
|
||||
undefined,
|
||||
{ total: 150, blocked: 0 },
|
||||
][i],
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -86,6 +86,7 @@ export const ListSessionsPageView: FC<ListSessionsPageViewProps> = ({
|
||||
<TableHead className="text-nowrap">Provider</TableHead>
|
||||
<TableHead className="text-nowrap">Client</TableHead>
|
||||
<TableHead className="text-nowrap">In/Out Tokens</TableHead>
|
||||
<TableHead className="text-nowrap">Network Calls</TableHead>
|
||||
<TableHead className="flex items-center flex-nowrap gap-1">
|
||||
Threads
|
||||
<ThreadTooltip>
|
||||
|
||||
@@ -107,3 +107,27 @@ export const LargeTokenCounts: Story = {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const NetworkCallsBlocked: Story = {
|
||||
args: {
|
||||
session: {
|
||||
...MockSession,
|
||||
network_calls: { total: 23, blocked: 2 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const NoNetworkActivity: Story = {
|
||||
args: {
|
||||
session: {
|
||||
...MockSession,
|
||||
network_calls: { total: 0, blocked: 0 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const NetworkDisabled: Story = {
|
||||
args: {
|
||||
session: { ...MockSession, network_calls: undefined },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { AIBridgeClientIcon } from "#/pages/AIBridgePage/icons/AIBridgeClientIcon";
|
||||
import { AIBridgeProviderIcon } from "#/pages/AIBridgePage/icons/AIBridgeProviderIcon";
|
||||
import { DATE_FORMAT, formatDateTime } from "#/utils/time";
|
||||
import { NetworkCallBadges } from "../NetworkCallBadges";
|
||||
import { TokenBadges } from "../TokenBadges";
|
||||
import { getProviderDisplayName } from "../utils";
|
||||
|
||||
@@ -105,6 +106,9 @@ export const ListSessionsRow: FC<ListSessionsRowProps> = ({
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="w-40">
|
||||
<NetworkCallBadges summary={session.network_calls} />
|
||||
</TableCell>
|
||||
<TableCell className="w-32">
|
||||
<Badge className="bg-surface-secondary align-end">
|
||||
{session.threads}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { expect, screen, userEvent, waitFor } from "storybook/test";
|
||||
import { NetworkCallBadges } from "./NetworkCallBadges";
|
||||
|
||||
const meta: Meta<typeof NetworkCallBadges> = {
|
||||
title: "pages/AIBridgePage/NetworkCallBadges",
|
||||
component: NetworkCallBadges,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof NetworkCallBadges>;
|
||||
|
||||
export const TotalAndBlocked: Story = {
|
||||
args: {
|
||||
summary: { total: 23, blocked: 2 },
|
||||
},
|
||||
};
|
||||
|
||||
export const NoBlocked: Story = {
|
||||
args: {
|
||||
summary: { total: 23, blocked: 0 },
|
||||
},
|
||||
};
|
||||
|
||||
export const NoActivity: Story = {
|
||||
args: {
|
||||
summary: { total: 0, blocked: 0 },
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
summary: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
export const LargeCounts: Story = {
|
||||
args: {
|
||||
summary: { total: 12_480, blocked: 320 },
|
||||
},
|
||||
};
|
||||
|
||||
// Tabbing to the badges reveals the breakdown tooltip without a mouse.
|
||||
export const TotalAndBlockedKeyboard: Story = {
|
||||
args: {
|
||||
summary: { total: 23, blocked: 2 },
|
||||
},
|
||||
play: async () => {
|
||||
await userEvent.tab();
|
||||
await waitFor(() => {
|
||||
const tooltip = screen.getByRole("tooltip");
|
||||
expect(tooltip).toHaveTextContent("Total calls");
|
||||
expect(tooltip).toHaveTextContent("Blocked");
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// Tabbing to the disabled indicator's info button and pressing Enter reveals
|
||||
// the reason popover without a mouse.
|
||||
export const DisabledKeyboard: Story = {
|
||||
args: {
|
||||
summary: undefined,
|
||||
},
|
||||
play: async () => {
|
||||
await userEvent.tab();
|
||||
await userEvent.keyboard("{Enter}");
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole("dialog")).toHaveTextContent(
|
||||
"Network call monitoring was not active for this session.",
|
||||
),
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
import { BanIcon } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import type { AIBridgeSessionNetworkCallSummary } from "#/api/typesGenerated";
|
||||
import { Badge } from "#/components/Badge/Badge";
|
||||
import { InfoTooltip } from "#/components/InfoTooltip/InfoTooltip";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "#/components/Tooltip/Tooltip";
|
||||
|
||||
interface NetworkCallBadgesProps {
|
||||
// summary is undefined when network call monitoring was not active for the
|
||||
// session, which renders as "Disabled".
|
||||
summary: AIBridgeSessionNetworkCallSummary | undefined;
|
||||
}
|
||||
|
||||
export const NetworkCallBadges: FC<NetworkCallBadgesProps> = ({ summary }) => {
|
||||
if (!summary) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 whitespace-nowrap text-content-secondary">
|
||||
Disabled
|
||||
<InfoTooltip message="Network call monitoring was not active for this session." />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (summary.total === 0) {
|
||||
return (
|
||||
<span className="whitespace-nowrap text-content-secondary">
|
||||
No activity
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="More info"
|
||||
className="flex items-center whitespace-nowrap border-0 bg-transparent p-0 text-inherit"
|
||||
>
|
||||
<Badge size="sm" className="rounded-e-none">
|
||||
{summary.total.toLocaleString("en-US")}
|
||||
</Badge>
|
||||
<Badge
|
||||
size="sm"
|
||||
svgSize="xs"
|
||||
className="gap-0 bg-surface-tertiary rounded-s-none text-content-warning"
|
||||
>
|
||||
<BanIcon className="flex-shrink-0" />
|
||||
{summary.blocked.toLocaleString("en-US")}
|
||||
</Badge>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="top"
|
||||
align="start"
|
||||
className="text-sm font-normal"
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<span className="text-content-secondary">Total calls</span>
|
||||
<span>{summary.total.toLocaleString("en-US")}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<span className="text-content-secondary">Blocked</span>
|
||||
<span>{summary.blocked.toLocaleString("en-US")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
@@ -5523,6 +5523,10 @@ export const MockSession: TypesGen.AIBridgeSession = {
|
||||
cache_read_input_tokens: 980,
|
||||
cache_write_input_tokens: 120,
|
||||
},
|
||||
network_calls: {
|
||||
total: 23,
|
||||
blocked: 2,
|
||||
},
|
||||
last_prompt: "But *can* I really fix it?",
|
||||
last_active_at: "2026-03-09T10:28:15.03152Z",
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user