diff --git a/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/tabs/ConversationTab.tsx b/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/tabs/ConversationTab.tsx
new file mode 100644
index 0000000000..ee30cf0e16
--- /dev/null
+++ b/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/tabs/ConversationTab.tsx
@@ -0,0 +1,44 @@
+"use client";
+
+import type { InterceptedRequest } from "@/mitm/inspector/types";
+import { normalizeConversation } from "@/mitm/inspector/conversationNormalizer";
+import { ChatBubble } from "../chat/ChatBubble";
+
+interface ConversationTabProps {
+ request: InterceptedRequest;
+}
+
+export function ConversationTab({ request }: ConversationTabProps) {
+ const conversation = normalizeConversation(request);
+
+ if (!conversation) {
+ return (
+
+ Conversation data not available. This may not be an LLM request or the body
+ could not be parsed.
+
+ );
+ }
+
+ const allTurns = [...conversation.request, ...conversation.response];
+
+ if (allTurns.length === 0) {
+ return (
+ No messages found in this request.
+ );
+ }
+
+ return (
+
+ {conversation.contextKey && (
+
+ Context fingerprint:{" "}
+ #{conversation.contextKey.slice(0, 12)}
+
+ )}
+ {allTurns.map((turn, i) => (
+
+ ))}
+
+ );
+}
diff --git a/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/tabs/HeadersTab.tsx b/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/tabs/HeadersTab.tsx
new file mode 100644
index 0000000000..46dbcc2f1b
--- /dev/null
+++ b/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/tabs/HeadersTab.tsx
@@ -0,0 +1,27 @@
+"use client";
+
+import type { InterceptedRequest } from "@/mitm/inspector/types";
+import { HeaderTable } from "../shared/HeaderTable";
+
+interface HeadersTabProps {
+ request: InterceptedRequest;
+}
+
+export function HeadersTab({ request }: HeadersTabProps) {
+ return (
+
+
+
+ Request Headers
+
+
+
+
+
+ Response Headers
+
+
+
+
+ );
+}
diff --git a/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/tabs/LlmDetailsTab.tsx b/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/tabs/LlmDetailsTab.tsx
new file mode 100644
index 0000000000..3f26d123ba
--- /dev/null
+++ b/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/tabs/LlmDetailsTab.tsx
@@ -0,0 +1,60 @@
+"use client";
+
+import type { InterceptedRequest } from "@/mitm/inspector/types";
+import { extractLlmMetadata } from "@/mitm/inspector/llmMetadataExtractor";
+import { TokenBadge } from "../shared/TokenBadge";
+
+interface LlmDetailsTabProps {
+ request: InterceptedRequest;
+}
+
+export function LlmDetailsTab({ request }: LlmDetailsTabProps) {
+ const meta = extractLlmMetadata(request);
+
+ if (!meta) {
+ return (
+
+ LLM metadata not available for this request.
+
+ );
+ }
+
+ const rows: Array<{ label: string; value: string | null | undefined }> = [
+ { label: "Detected provider", value: meta.provider },
+ { label: "API kind", value: meta.apiKind },
+ { label: "Model", value: meta.model },
+ { label: "Messages", value: meta.messages > 0 ? String(meta.messages) : null },
+ { label: "Stream", value: meta.streamed ? "yes (SSE)" : "no" },
+ { label: "Mapped to", value: meta.mappedTo },
+ {
+ label: "Cost estimate",
+ value: meta.costEstimateUsd != null ? `$${meta.costEstimateUsd.toFixed(6)}` : null,
+ },
+ ];
+
+ return (
+
+
+
+
+ {rows.map(({ label, value }) => (
+
+ | {label} |
+ {value ?? "—"} |
+
+ ))}
+
+
+
+
+
+
+ {(meta.tokensIn != null || meta.tokensOut != null) && (
+
+ Total: {(meta.tokensIn ?? 0) + (meta.tokensOut ?? 0)} tokens
+
+ )}
+
+
+ );
+}
diff --git a/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/tabs/RequestBodyTab.tsx b/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/tabs/RequestBodyTab.tsx
new file mode 100644
index 0000000000..0e14f4e5fc
--- /dev/null
+++ b/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/tabs/RequestBodyTab.tsx
@@ -0,0 +1,61 @@
+"use client";
+
+import { useState } from "react";
+import type { InterceptedRequest } from "@/mitm/inspector/types";
+import { JsonViewer } from "../shared/JsonViewer";
+import { SecretMaskToggle } from "../shared/SecretMaskToggle";
+
+interface RequestBodyTabProps {
+ request: InterceptedRequest;
+}
+
+const MASK_PATTERNS = [/sk-[A-Za-z0-9]+/g, /Bearer [A-Za-z0-9._-]+/g, /eyJ[A-Za-z0-9._-]+/g];
+
+function maskSecrets(text: string): string {
+ let out = text;
+ for (const p of MASK_PATTERNS) {
+ out = out.replace(p, "••••");
+ }
+ return out;
+}
+
+export function RequestBodyTab({ request }: RequestBodyTabProps) {
+ const [masked, setMasked] = useState(true);
+ const [raw, setRaw] = useState(false);
+
+ const body = request.requestBody;
+ if (!body) {
+ return No request body.
;
+ }
+
+ const display = masked ? maskSecrets(body) : body;
+ let parsed: unknown = null;
+ try {
+ parsed = JSON.parse(display);
+ } catch {
+ // not JSON
+ }
+
+ return (
+
+
+ setMasked((m) => !m)} />
+
+ {request.requestSize} B
+
+
+ {raw || !parsed ? (
+
{display}
+ ) : (
+
+ )}
+
+
+ );
+}
diff --git a/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/tabs/ResponseBodyTab.tsx b/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/tabs/ResponseBodyTab.tsx
new file mode 100644
index 0000000000..c5fe9349f4
--- /dev/null
+++ b/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/tabs/ResponseBodyTab.tsx
@@ -0,0 +1,71 @@
+"use client";
+
+import { useState } from "react";
+import type { InterceptedRequest } from "@/mitm/inspector/types";
+import { parseSseStream, mergeStream } from "@/mitm/inspector/sseMerger";
+import { JsonViewer } from "../shared/JsonViewer";
+import { SseEventList } from "../shared/SseEventList";
+
+interface ResponseBodyTabProps {
+ request: InterceptedRequest;
+}
+
+export function ResponseBodyTab({ request }: ResponseBodyTabProps) {
+ const [showRaw, setShowRaw] = useState(false);
+
+ const body = request.responseBody;
+ if (!body) {
+ return No response body.
;
+ }
+
+ const isSSE = body.startsWith("data:") || body.includes("\ndata:");
+ const events = isSSE ? parseSseStream(body) : [];
+ const merged = isSSE && !showRaw ? mergeStream(events) : null;
+
+ let parsed: unknown = null;
+ if (!isSSE) {
+ try {
+ parsed = JSON.parse(body);
+ } catch {
+ // not JSON
+ }
+ }
+
+ return (
+
+
+ {isSSE && (
+
+ )}
+ {request.responseSize} B
+ {request.status === "in-flight" && (
+ streaming…
+ )}
+
+
+ {isSSE && showRaw ? (
+
+ ) : isSSE && merged ? (
+
+ {merged.text && (
+
{merged.text}
+ )}
+ {merged.toolCalls && merged.toolCalls.length > 0 && (
+
+ )}
+
+ ) : parsed ? (
+
+ ) : (
+
{body}
+ )}
+
+
+ );
+}
diff --git a/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/tabs/StatsTab.tsx b/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/tabs/StatsTab.tsx
new file mode 100644
index 0000000000..7e919d2360
--- /dev/null
+++ b/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/tabs/StatsTab.tsx
@@ -0,0 +1,136 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import dynamic from "next/dynamic";
+import type { InterceptedRequest } from "@/mitm/inspector/types";
+
+interface StatsTabProps {
+ requests: InterceptedRequest[];
+}
+
+// Recharts is lazy-loaded via dynamic() with ssr: false — avoids including the
+// full Recharts bundle in the initial page load.
+const _rechartsPreload = dynamic(() => import("recharts"), { ssr: false });
+void _rechartsPreload;
+
+// Using ComponentType instead of any to satisfy strict lint rules.
+type AnyComponent = React.ComponentType>;
+
+interface RechartsLib {
+ ResponsiveContainer: AnyComponent;
+ BarChart: AnyComponent;
+ Bar: AnyComponent;
+ XAxis: AnyComponent;
+ YAxis: AnyComponent;
+ Tooltip: AnyComponent;
+ LineChart: AnyComponent;
+ Line: AnyComponent;
+}
+
+function StatsCharts({ requests }: StatsTabProps) {
+ const [lib, setLib] = useState(null);
+
+ useEffect(() => {
+ import("recharts").then((mod) => {
+ setLib(mod as unknown as RechartsLib);
+ });
+ }, []);
+
+ if (!lib) {
+ return Loading charts…
;
+ }
+
+ const { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip, LineChart, Line } = lib;
+
+ const statusDist = requests.reduce>((acc, r) => {
+ const key =
+ typeof r.status === "number" ? `${Math.floor(r.status / 100)}xx` : String(r.status);
+ acc[key] = (acc[key] ?? 0) + 1;
+ return acc;
+ }, {});
+ const statusData = Object.entries(statusDist).map(([name, count]) => ({ name, count }));
+
+ const latencyData = requests
+ .filter((r) => r.totalLatencyMs != null)
+ .slice(-50)
+ .map((r, i) => ({ i, ms: r.totalLatencyMs }));
+
+ return (
+
+
+
+ Status distribution
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {latencyData.length > 1 && (
+
+
+ Latency (last 50 requests)
+
+
+
+
+
+
+ [`${String(v)}ms`, "latency"]} />
+
+
+
+
+
+ )}
+
+
+
+
{requests.length}
+
Total requests
+
+
+
+ {requests.filter((r) => typeof r.status === "number" && r.status < 400).length}
+
+
Successful
+
+
+
+ {
+ requests.filter(
+ (r) =>
+ r.status === "error" || (typeof r.status === "number" && r.status >= 400),
+ ).length
+ }
+
+
Errors
+
+
+
+ );
+}
+
+export function StatsTab({ requests }: StatsTabProps) {
+ if (requests.length === 0) {
+ return (
+
+ No requests yet. Start a session recording to capture data for stats.
+
+ );
+ }
+ return ;
+}
diff --git a/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/tabs/TimingTab.tsx b/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/tabs/TimingTab.tsx
new file mode 100644
index 0000000000..e62bd88ea8
--- /dev/null
+++ b/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/tabs/TimingTab.tsx
@@ -0,0 +1,38 @@
+"use client";
+
+import type { InterceptedRequest } from "@/mitm/inspector/types";
+import { TimingWaterfall } from "../shared/TimingWaterfall";
+
+interface TimingTabProps {
+ request: InterceptedRequest;
+}
+
+export function TimingTab({ request }: TimingTabProps) {
+ return (
+
+
+
+
+ Timestamp
+ {request.timestamp}
+
+
+ Method
+ {request.method}
+
+
+ Status
+ {String(request.status)}
+
+
+ Request size
+ {request.requestSize} B
+
+
+ Response size
+ {request.responseSize} B
+
+
+
+ );
+}