diff --git a/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/RequestRow.tsx b/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/RequestRow.tsx index 9fbcca5b95..1acf09e7a6 100644 --- a/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/RequestRow.tsx +++ b/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/RequestRow.tsx @@ -9,6 +9,7 @@ interface RequestRowProps { request: InterceptedRequest; selected: boolean; onClick: () => void; + onSameContext?: (contextKey: string) => void; style?: React.CSSProperties; } @@ -39,7 +40,7 @@ function formatTime(iso: string): string { } } -export function RequestRow({ request, selected, onClick, style }: RequestRowProps) { +export function RequestRow({ request, selected, onClick, onSameContext, style }: RequestRowProps) { const pathShort = request.path.length > 32 ? `…${request.path.slice(-30)}` : request.path; const sc = statusColor(request.status); @@ -74,9 +75,17 @@ export function RequestRow({ request, selected, onClick, style }: RequestRowProp {pathShort} {request.contextKey && ( -
+
+ )} diff --git a/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/RequestStreamingList.tsx b/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/RequestStreamingList.tsx index 112eb2ad93..dd68276783 100644 --- a/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/RequestStreamingList.tsx +++ b/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/RequestStreamingList.tsx @@ -10,6 +10,9 @@ interface RequestStreamingListProps { selectedId: string | null; onSelect: (req: InterceptedRequest) => void; containerHeight: number; + onSameContext?: (contextKey: string) => void; + sameContextKey?: string; + onClearContextFilter?: () => void; } export function RequestStreamingList({ @@ -17,6 +20,9 @@ export function RequestStreamingList({ selectedId, onSelect, containerHeight, + onSameContext, + sameContextKey, + onClearContextFilter, }: RequestStreamingListProps) { const { virtualItems, totalHeight, containerRef, rowRef } = useVirtualList( requests, @@ -25,44 +31,73 @@ export function RequestStreamingList({ if (requests.length === 0) { return ( -
-
- -

No requests captured yet.

-

Make sure AgentBridge is running or enable another capture mode.

+
+ {sameContextKey && ( +
+ Filtering: context {sameContextKey.slice(0, 6)} + +
+ )} +
+
+ +

No requests captured yet.

+

Make sure AgentBridge is running or enable another capture mode.

+
); } return ( -
} - className="h-full overflow-y-auto relative" - style={{ contain: "strict" }} - > -
- {virtualItems.map(({ index, item, top }) => ( -
+ {sameContextKey && ( +
+ Filtering: context {sameContextKey.slice(0, 6)} +
- ))} + [clear] + +
+ )} +
} + className="flex-1 overflow-y-auto relative" + style={{ contain: "strict" }} + > +
+ {virtualItems.map(({ index, item, top }) => ( +
+ onSelect(item)} + onSameContext={onSameContext} + /> +
+ ))} +
); diff --git a/src/app/(dashboard)/dashboard/tools/traffic-inspector/hooks/useTrafficStream.ts b/src/app/(dashboard)/dashboard/tools/traffic-inspector/hooks/useTrafficStream.ts index 2f6c8ac8f3..09112748e6 100644 --- a/src/app/(dashboard)/dashboard/tools/traffic-inspector/hooks/useTrafficStream.ts +++ b/src/app/(dashboard)/dashboard/tools/traffic-inspector/hooks/useTrafficStream.ts @@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import type { InterceptedRequest, ListFilters, WsEvent } from "@/mitm/inspector/types"; +import type { FiltersState } from "./useTrafficFilters"; const WS_PATH = "/api/tools/traffic-inspector/ws"; const INITIAL_BACKOFF_MS = 500; @@ -13,6 +14,7 @@ export interface TrafficStreamState { connected: boolean; paused: boolean; total: number; + pendingCount: number; } export interface TrafficStreamActions { @@ -22,11 +24,12 @@ export interface TrafficStreamActions { } export function useTrafficStream( - filters: ListFilters + filters: FiltersState | ListFilters ): [TrafficStreamState, TrafficStreamActions] { const [requests, setRequests] = useState([]); const [connected, setConnected] = useState(false); const [paused, setPaused] = useState(false); + const [pendingCount, setPendingCount] = useState(0); const wsRef = useRef(null); const backoffRef = useRef(INITIAL_BACKOFF_MS); @@ -44,13 +47,14 @@ export function useTrafficStream( }); const applyFilter = useCallback((req: InterceptedRequest): boolean => { - const f = filtersRef.current; + const f = filtersRef.current as FiltersState; if (f.profile === "llm" && req.detectedKind !== "llm") return false; if (f.profile === "custom" && req.source !== "custom-host") return false; if (f.host && !req.host.includes(f.host)) return false; if (f.agent && req.agent !== f.agent) return false; if (f.source && req.source !== f.source) return false; if (f.sessionId && req.sessionId !== f.sessionId) return false; + if (f.sameContextKey && req.contextKey !== f.sameContextKey) return false; if (f.status) { const s = req.status; if (typeof s === "number") { @@ -93,7 +97,10 @@ export function useTrafficStream( } if (pausedRef.current) { - if (event.type === "new") pendingRef.current.push(event.data); + if (event.type === "new") { + pendingRef.current.push(event.data); + setPendingCount(pendingRef.current.length); + } if (event.type === "update") { const idx = pendingRef.current.findIndex((r) => r.id === event.data.id); if (idx !== -1) pendingRef.current[idx] = event.data; @@ -157,6 +164,7 @@ export function useTrafficStream( if (pendingRef.current.length > 0) { const pending = pendingRef.current.filter(applyFilter); pendingRef.current = []; + setPendingCount(0); setRequests((prev) => [...pending, ...prev].slice(0, 1000)); } }, [applyFilter]); @@ -164,6 +172,7 @@ export function useTrafficStream( const clear = useCallback(() => { setRequests([]); pendingRef.current = []; + setPendingCount(0); }, []); const state: TrafficStreamState = { @@ -171,6 +180,7 @@ export function useTrafficStream( connected, paused, total: requests.length, + pendingCount, }; return [state, { pause, resume, clear }]; diff --git a/tests/unit/ui/same-context-filter.test.tsx b/tests/unit/ui/same-context-filter.test.tsx new file mode 100644 index 0000000000..ddc9aaef0b --- /dev/null +++ b/tests/unit/ui/same-context-filter.test.tsx @@ -0,0 +1,116 @@ +/** + * Tests for R5-4: same-context filter wired end-to-end + * + * Source-grep assertions that: + * - useTrafficStream.applyFilter branches on sameContextKey + * - RequestRow exports an onSameContext prop + * - useTrafficFilters.setSameContext is referenced from TrafficInspectorPageClient + */ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const ROOT = path.resolve( + __dirname, + "../../../src/app/(dashboard)/dashboard/tools/traffic-inspector" +); + +function read(rel: string): string { + return fs.readFileSync(path.join(ROOT, rel), "utf8"); +} + +describe("R5-4 same-context filter end-to-end", () => { + it("useTrafficStream.applyFilter has sameContextKey branch", () => { + const src = read("hooks/useTrafficStream.ts"); + assert.ok( + src.includes("sameContextKey") && src.includes("contextKey"), + "applyFilter should branch on sameContextKey / contextKey" + ); + // Must actually exclude requests where contextKey differs + assert.ok( + src.includes("req.contextKey !== f.sameContextKey"), + "should exclude when contextKey !== sameContextKey" + ); + }); + + it("RequestRow accepts onSameContext prop in interface", () => { + const src = read("components/RequestRow.tsx"); + assert.ok( + src.includes("onSameContext"), + "RequestRow interface should declare onSameContext prop" + ); + assert.ok( + src.includes("onSameContext?.("), + "RequestRow should call onSameContext on click" + ); + }); + + it("RequestRow ctx chip is a button element", () => { + const src = read("components/RequestRow.tsx"); + // The ctx chip should now be a button for keyboard/mouse click + const hasButton = src.includes(" that calls onSameContext"); + }); + + it("TrafficInspectorPageClient references setSameContext", () => { + const src = read("TrafficInspectorPageClient.tsx"); + assert.ok( + src.includes("setSameContext"), + "TrafficInspectorPageClient should destructure setSameContext from useTrafficFilters" + ); + }); + + it("RequestStreamingList passes onSameContext to RequestRow", () => { + const src = read("components/RequestStreamingList.tsx"); + assert.ok( + src.includes("onSameContext"), + "RequestStreamingList should accept and forward onSameContext prop" + ); + }); + + it("RequestStreamingList shows sameContextKey banner when active", () => { + const src = read("components/RequestStreamingList.tsx"); + assert.ok( + src.includes("sameContextKey") && src.includes("onClearContextFilter"), + "RequestStreamingList should show a clear-filter banner when sameContextKey is set" + ); + }); + + it("TrafficInspectorPageClient passes sameContextKey and onClearContextFilter to list", () => { + const src = read("TrafficInspectorPageClient.tsx"); + assert.ok( + src.includes("sameContextKey={filters.sameContextKey}"), + "should pass sameContextKey to RequestStreamingList" + ); + assert.ok( + src.includes("onClearContextFilter"), + "should pass onClearContextFilter to RequestStreamingList" + ); + }); + + it("useTrafficFilters exports setSameContext", () => { + const src = read("hooks/useTrafficFilters.ts"); + assert.ok( + src.includes("setSameContext"), + "useTrafficFilters should export setSameContext" + ); + }); + + describe("applyFilter sameContextKey logic (unit)", () => { + it("returns false when contextKey does not match filter", () => { + type Req = { contextKey?: string; detectedKind: string; source: string; host: string; agent?: string; sessionId?: string; status: number }; + const applyFilter = (req: Req, sameContextKey?: string): boolean => { + if (sameContextKey && req.contextKey !== sameContextKey) return false; + return true; + }; + + assert.equal(applyFilter({ contextKey: "abc123", detectedKind: "llm", source: "agent", host: "api.openai.com", status: 200 }, "abc123"), true); + assert.equal(applyFilter({ contextKey: "xyz456", detectedKind: "llm", source: "agent", host: "api.openai.com", status: 200 }, "abc123"), false); + assert.equal(applyFilter({ contextKey: undefined, detectedKind: "llm", source: "agent", host: "api.openai.com", status: 200 }, "abc123"), false); + assert.equal(applyFilter({ contextKey: "abc123", detectedKind: "llm", source: "agent", host: "api.openai.com", status: 200 }, undefined), true); + }); + }); +});