diff --git a/src-web/components/HttpResponseTimeline.tsx b/src-web/components/HttpResponseTimeline.tsx
index be459181..a9946703 100644
--- a/src-web/components/HttpResponseTimeline.tsx
+++ b/src-web/components/HttpResponseTimeline.tsx
@@ -81,13 +81,13 @@ function EventDetails({
const rawText = formatEventRaw(event.event);
return (
-
-
+
);
}
@@ -114,7 +114,12 @@ function EventDetails({
if (e.type === 'send_url') {
return (
-
+
@@ -129,7 +134,12 @@ function EventDetails({
if (e.type === 'receive_url') {
return (
-
+
{e.version}
@@ -144,7 +154,12 @@ function EventDetails({
if (e.type === 'redirect') {
return (
-
+
@@ -162,7 +177,12 @@ function EventDetails({
if (e.type === 'setting') {
return (
-
+
{e.name}
{e.value}
@@ -176,7 +196,12 @@ function EventDetails({
const direction = e.type === 'chunk_sent' ? 'Sent' : 'Received';
return (
-
+
{formatBytes(e.bytes)}
);
diff --git a/src-web/components/WebsocketResponsePane.tsx b/src-web/components/WebsocketResponsePane.tsx
index 041fda32..76a704d6 100644
--- a/src-web/components/WebsocketResponsePane.tsx
+++ b/src-web/components/WebsocketResponsePane.tsx
@@ -31,7 +31,6 @@ interface Props {
}
export function WebsocketResponsePane({ activeRequest }: Props) {
- const [activeEventIndex, setActiveEventIndex] = useState(null);
const [showLarge, setShowLarge] = useStateWithDeps(false, [activeRequest.id]);
const [showingLarge, setShowingLarge] = useState(false);
const [hexDumps, setHexDumps] = useState>({});
@@ -40,26 +39,6 @@ export function WebsocketResponsePane({ activeRequest }: Props) {
const connections = useAtomValue(activeWebsocketConnectionsAtom);
const events = useWebsocketEvents(activeConnection?.id ?? null);
- const activeEvent = useMemo(
- () => (activeEventIndex != null ? events[activeEventIndex] : null),
- [activeEventIndex, events],
- );
-
- const hexDump =
- hexDumps[activeEventIndex ?? -1] ?? activeEvent?.messageType === 'binary';
-
- const message = useMemo(() => {
- if (hexDump) {
- return activeEvent?.message ? hexy(activeEvent?.message) : '';
- }
- return activeEvent?.message
- ? new TextDecoder('utf-8').decode(Uint8Array.from(activeEvent.message))
- : '';
- }, [activeEvent?.message, hexDump]);
-
- const language = languageFromContentType(null, message);
- const formattedMessage = useFormatText({ language, text: message, pretty: true });
-
if (activeConnection == null) {
return (
@@ -101,12 +80,8 @@ export function WebsocketResponsePane({ activeRequest }: Props) {
renderDetail={({ event, index }) => (
setHexDumps({ ...hexDumps, [index]: v })}
- message={message}
- formattedMessage={formattedMessage}
- language={language}
showLarge={showLarge}
showingLarge={showingLarge}
setShowLarge={setShowLarge}
@@ -133,11 +108,7 @@ function WebsocketEventRow({
: '';
const iconColor =
- messageType === 'close' || messageType === 'open'
- ? 'secondary'
- : isServer
- ? 'info'
- : 'primary';
+ messageType === 'close' || messageType === 'open' ? 'secondary' : isServer ? 'info' : 'primary';
const icon =
messageType === 'close' || messageType === 'open'
@@ -170,29 +141,31 @@ function WebsocketEventRow({
function WebsocketEventDetail({
event,
- index,
hexDump,
setHexDump,
- message,
- formattedMessage,
- language,
showLarge,
showingLarge,
setShowLarge,
setShowingLarge,
}: {
event: WebsocketEvent;
- index: number;
hexDump: boolean;
setHexDump: (v: boolean) => void;
- message: string;
- formattedMessage: string | null;
- language: string;
showLarge: boolean;
showingLarge: boolean;
setShowLarge: (v: boolean) => void;
setShowingLarge: (v: boolean) => void;
}) {
+ const message = useMemo(() => {
+ if (hexDump) {
+ return event.message ? hexy(event.message) : '';
+ }
+ return event.message ? new TextDecoder('utf-8').decode(Uint8Array.from(event.message)) : '';
+ }, [event.message, hexDump]);
+
+ const language = languageFromContentType(null, message);
+ const formattedMessage = useFormatText({ language, text: message, pretty: true });
+
const title =
event.messageType === 'close'
? 'Connection Closed'
diff --git a/src-web/components/core/AutoScroller.tsx b/src-web/components/core/AutoScroller.tsx
index 017ebd48..f0688192 100644
--- a/src-web/components/core/AutoScroller.tsx
+++ b/src-web/components/core/AutoScroller.tsx
@@ -1,4 +1,4 @@
-import { type Virtualizer, useVirtualizer } from '@tanstack/react-virtual';
+import { useVirtualizer, type Virtualizer } from '@tanstack/react-virtual';
import type { ReactElement, ReactNode, UIEvent } from 'react';
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
import { IconButton } from './IconButton';
diff --git a/src-web/components/core/EventViewer.tsx b/src-web/components/core/EventViewer.tsx
index 3053069e..ff3e8102 100644
--- a/src-web/components/core/EventViewer.tsx
+++ b/src-web/components/core/EventViewer.tsx
@@ -48,6 +48,9 @@ interface EventViewerProps {
/** Message to show when no events */
emptyMessage?: string;
+
+ /** Callback when active index changes (for controlled state in parent) */
+ onActiveIndexChange?: (index: number | null) => void;
}
export function EventViewer({
@@ -63,8 +66,22 @@ export function EventViewer({
isLoading = false,
loadingMessage = 'Loading events...',
emptyMessage = 'No events recorded',
+ onActiveIndexChange,
}: EventViewerProps) {
- const [activeIndex, setActiveIndex] = useState(null);
+ const [activeIndex, setActiveIndexInternal] = useState(null);
+
+ // Wrap setActiveIndex to notify parent
+ const setActiveIndex = useCallback(
+ (indexOrUpdater: number | null | ((prev: number | null) => number | null)) => {
+ setActiveIndexInternal((prev) => {
+ const newIndex =
+ typeof indexOrUpdater === 'function' ? indexOrUpdater(prev) : indexOrUpdater;
+ onActiveIndexChange?.(newIndex);
+ return newIndex;
+ });
+ },
+ [onActiveIndexChange],
+ );
const containerRef = useRef(null);
const virtualizerRef = useRef | null>(null);
@@ -101,7 +118,7 @@ export function EventViewer({
(index: number) => {
setActiveIndex((prev) => (prev === index ? null : index));
},
- [],
+ [setActiveIndex],
);
if (isLoading) {
@@ -154,7 +171,7 @@ export function EventViewer({
- {renderDetail({ event: activeEvent, index: activeIndex! })}
+ {renderDetail({ event: activeEvent, index: activeIndex ?? 0 })}
)