feat(site): show network calls list on AI session detail - #27426
Conversation
f9e824c to
c60d5c1
Compare
c60d5c1 to
7ad90bb
Compare
Backend for the AI session network summary. Exposes total/blocked
network calls and top destination domains on the session threads
endpoint (`GET /api/v2/ai-gateway/sessions/{id}`).
Total and blocked reuse the existing Agent Firewall aggregation from the
sessions list query, so the numbers match the sessions table. Top
domains are a new server-side aggregation
(`GetAIBridgeSessionTopDomains`) over boundary logs, using the same
interception-window correlation. There is no network-error state,
matching the current data model.
Frontend consuming these fields is in a separate stacked PR.
### PR map (merge strictly bottom-up)
This change is a 4-PR stack. Each PR depends on all the ones below it,
so merge in this exact order:
1. #27417 — backend network summary (base `main`)
2. #27418 — frontend summary rows (base #27417)
3. #27425 — backend per-call list `network_call_logs` (base #27418)
4. #27426 — frontend network-calls panel (base #27425)
Refs AIGOV-463
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
Co-authored-by: Cian Johnston <[email protected]>
Frontend for the AI session network summary. Adds Network calls, Blocked network requests, and Top domains rows to the Session summary card on the individual AI session detail page, driven by the network fields on the session threads response. Renders "Disabled" when network monitoring was not active and "No activity" when there were no calls. Covered by Storybook stories for each state. ### PR map (merge strictly bottom-up) This change is a 4-PR stack. Each PR depends on all the ones below it, so merge in this exact order: 1. #27417 — backend network summary 2. #27418 — frontend summary rows 3. #27425 — backend per-call list `network_call_logs` 4. #27426 — frontend network-calls panel Refs AIGOV-463 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]> Co-authored-by: Cian Johnston <[email protected]>
|
Can we rebase this on the latest main or something? I'm not sure what the new changes are 🙏 |
The AI session threads API returned only a network call *summary* (total/blocked counts + top domains). This adds the per-call list so the session detail can render individual Agent Firewall network calls. `ListAIBridgeSessionNetworkCalls` reuses the same sequence-number windowing as the existing summary and includes all protocols. The list is exposed as `network_call_logs` on the threads response and is capped server-side at 100 rows. The summary (`network_calls.total`/`blocked`) remains authoritative for whole-session totals: the list length and its blocked count equal the summary only when a session has at most 100 calls, and are truncated beyond that. ### PR map (merge strictly bottom-up) This change is a 4-PR stack. Each PR depends on all the ones below it, so merge in this exact order: 1. #27417 — backend network summary 2. #27418 — frontend summary rows 3. #27425 — backend per-call list `network_call_logs` 4. #27426 — frontend network-calls panel Refs AIGOV-464 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
7ad90bb to
70a8dd2
Compare
Docs previewCheck off each page once it's been reviewed. If a page changes in a later push, its checkbox clears automatically so it gets a fresh look. Pages not yet wired into the docs navigation aren't listed here. |
Documentation CheckUpdates Needed
Not flagged: Automated review via Coder Agents |
Render the per-call network calls as a collapsible panel above the session threads. Each row shows the method, allowed/blocked status, URL, and timestamp, and expands to show the protocol, matched rule, and full detail. The panel is omitted when the session did not pass through Agent Firewall. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
70a8dd2 to
d753830
Compare
EhabY
left a comment
There was a problem hiding this comment.
pls fix compilation and linting 🙏
| {session ? ( | ||
| <SessionTimeline | ||
| initiator={session.initiator} | ||
| threads={threads} | ||
| networkCallSummary={session.network_calls} | ||
| networkCalls={session.network_call_logs ?? []} |
There was a problem hiding this comment.
Does it make sense to just pass session as a whole?
There was a problem hiding this comment.
session has quite a large footprint. I like passing only what the SessionTimeline needs as a form of self documentation. Happy to change it if you deem that better, but I think its fine as it?
There was a problem hiding this comment.
We could pass the session and do something like Pick<AIBridgeSessionThreadsResponse, "initiator" | "network_calls" | "network_call_logs">
I do not feel strongly but that is easier to pass IMO (if we want pass another field we just add | "new_field")
| )} | ||
| </div> | ||
|
|
||
| {isOpen && ( |
There was a problem hiding this comment.
Maybe we separate this into a components for easier reading? We can avoid the ternary operators here and just have a if (...) else ...
This could actually resolve a bug where we render both when there's a summary but an empty list:
No network calls were recorded for this session.
Showing the first 0 of 4 network calls.
Please add a storybook for this actually
There was a problem hiding this comment.
I've added a storybook for this. However, this this is not a state that is possible from the API's side? If there is no summary, there will be no calls?
There was a problem hiding this comment.
Ah yeah fair enough, tbh I prefer the frontend to be fully tested regardless of the backend implicit contract. I prefer this but not pressed about it
| // of rendered rows. | ||
| export const BlockedBadge: Story = { | ||
| play: async ({ canvas }) => { | ||
| const header = canvas.getByText("Network calls (4)").closest("div"); |
There was a problem hiding this comment.
Maybe give the header a data-testid, or query the badge directly by its accessible name since this is brittle
| // The blocked-count badge in the header reflects the summary, not the number | ||
| // of rendered rows. |
There was a problem hiding this comment.
This claims that the badge "reflects the summary, not the number of rendered rows", but it inherits the meta args where blocked is 2 and exactly 2 rendered rows are blocked, so the story cannot tell the two apart. Give it summary: { total: 4, blocked: 9 } to actually pin the claim
| await expect(canvas.queryByText("Protocol")).not.toBeInTheDocument(); | ||
|
|
||
| const rowButtons = canvas.getAllByRole("button", { expanded: false }); | ||
| // The first row button toggles the first call's detail open. |
The API now returns firewall log entries via the shared AgentFirewallLog type, replacing the bespoke AIBridgeSessionNetworkCall type. Update the components and mock data to match.
|
@EhabY apologies. There was an unfortunate window after I merged the prior PR and before I had pushed the rebase in which you saw a bunch of irrelevant changes. That's been fixed. I'm tending to the comments you've already made now. Will re-request new review when done. |
Removes hand-rolled open/close state and CSS-driven chevron rotation in favor of the shared Collapsible primitive, matching the pattern used elsewhere in the codebase.
9025554 to
c88a182
Compare
| {session ? ( | ||
| <SessionTimeline | ||
| initiator={session.initiator} | ||
| threads={threads} | ||
| networkCallSummary={session.network_calls} | ||
| networkCalls={session.network_call_logs ?? []} |
There was a problem hiding this comment.
We could pass the session and do something like Pick<AIBridgeSessionThreadsResponse, "initiator" | "network_calls" | "network_call_logs">
I do not feel strongly but that is easier to pass IMO (if we want pass another field we just add | "new_field")
| )} | ||
| </div> | ||
|
|
||
| {isOpen && ( |
There was a problem hiding this comment.
Ah yeah fair enough, tbh I prefer the frontend to be fully tested regardless of the backend implicit contract. I prefer this but not pressed about it
| await expect( | ||
| canvas.getByText("Blocked network calls: 9"), | ||
| ).toBeInTheDocument(); |
There was a problem hiding this comment.
getByText("Blocked network calls: 9") never matches:
the sr-only span and the 9 are sibling nodes (NetworkCallsTable.tsx:42-46), and the matcher
reads only direct text children. This play function fails. Put the full string in one node, or
use aria-label + getByLabelText. Keep line 39.
| * Drives the header count and blocked badge. Reflects the whole session, so | ||
| * its total can exceed the number of rows in `calls`, which is capped | ||
| * server-side. | ||
| */ |
There was a problem hiding this comment.
nit: JSDoc says summary drives the header and badge, but NetworkCallsList (:56) reuses this interface and renders neither.
Something like:
Session-wide totals.
totalcan exceedcalls.length, which is capped server-side.
| created_at: "2026-03-09T09:28:19.000Z", | ||
| }, | ||
| ]; | ||
| export const MockAIBridgeSessionNetworkCalls: readonly TypesGen.AgentFirewallLog[] = |
There was a problem hiding this comment.
nit: we should probably rename this now that the type changed (MockAgentFirewallLogs)



Renders the per-call network calls as a collapsible panel above the session threads, using the
network_call_logsfield added to the threads API.Each row shows the request method, allowed/blocked status, URL, and timestamp, and expands to show the protocol, matched rule, and full detail. The header shows the total count and a blocked-count badge from the session summary; when the server caps the list, the panel notes how many calls are shown. The panel is omitted when the session did not pass through Agent Firewall.
PR map (merge strictly bottom-up)
This change is a 4-PR stack. Each PR depends on all the ones below it, so merge in this exact order:
main)network_call_logs(base feat: show network request summary on AI session detail card #27418)Refs AIGOV-464
🤖 Generated with Claude Code