diff --git a/clients/web/src/components/elements/ResizeHandle/ResizeHandle.test.tsx b/clients/web/src/components/elements/ResizeHandle/ResizeHandle.test.tsx
index 0cac37d2..5ba77b0b 100644
--- a/clients/web/src/components/elements/ResizeHandle/ResizeHandle.test.tsx
+++ b/clients/web/src/components/elements/ResizeHandle/ResizeHandle.test.tsx
@@ -86,6 +86,28 @@ describe("ResizeHandle", () => {
expect(document.body.classList.contains("resizing-col")).toBe(false);
});
+ it("clears the drag body class if it unmounts mid-drag", () => {
+ const onChange = vi.fn();
+ const onCommit = vi.fn();
+ const { unmount } = renderWithMantine(
+ ,
+ );
+ fireEvent.pointerDown(screen.getByRole("separator"), {
+ clientX: 500,
+ pointerId: 1,
+ });
+ expect(document.body.classList.contains("resizing-col")).toBe(true);
+ // No pointerup/cancel — the handle is removed from the tree first.
+ unmount();
+ expect(document.body.classList.contains("resizing-col")).toBe(false);
+ });
+
it("ignores pointer moves when no drag is active", () => {
const { handle, onChange, onCommit } = setup();
fireEvent.pointerMove(handle, { clientX: 460, pointerId: 1 });
diff --git a/clients/web/src/components/elements/ResizeHandle/ResizeHandle.tsx b/clients/web/src/components/elements/ResizeHandle/ResizeHandle.tsx
index a181e367..77545a5e 100644
--- a/clients/web/src/components/elements/ResizeHandle/ResizeHandle.tsx
+++ b/clients/web/src/components/elements/ResizeHandle/ResizeHandle.tsx
@@ -1,4 +1,9 @@
-import { useRef, type KeyboardEvent, type PointerEvent } from "react";
+import {
+ useEffect,
+ useRef,
+ type KeyboardEvent,
+ type PointerEvent,
+} from "react";
import { Box } from "@mantine/core";
export interface ResizeHandleProps {
@@ -45,6 +50,13 @@ export function ResizeHandle({
// Gesture origin, captured on pointer down. `null` while idle.
const drag = useRef<{ startX: number; startWidth: number } | null>(null);
+ // If the handle unmounts mid-drag — the window narrows past the split
+ // breakpoint or the server disconnects, either of which removes the handle
+ // from the tree before a pointerup/cancel fires — the drag body class would
+ // otherwise linger and leave `user-select: none` + `col-resize` applied
+ // page-wide. Clear it on unmount so the class is self-healing.
+ useEffect(() => () => document.body.classList.remove("resizing-col"), []);
+
function handlePointerDown(e: PointerEvent) {
drag.current = { startX: e.clientX, startWidth: value };
// Optional chaining: happy-dom (unit tests) doesn't implement pointer
diff --git a/clients/web/src/components/groups/HistoryListPanel/HistoryListPanel.tsx b/clients/web/src/components/groups/HistoryListPanel/HistoryListPanel.tsx
index be7840c3..9aaa8187 100644
--- a/clients/web/src/components/groups/HistoryListPanel/HistoryListPanel.tsx
+++ b/clients/web/src/components/groups/HistoryListPanel/HistoryListPanel.tsx
@@ -215,15 +215,26 @@ export function HistoryListPanel({
const [pinnedOpen, setPinnedOpen] = useState(true);
const [historyOpen, setHistoryOpen] = useState(true);
const filteredEntries = useMemo(() => {
- // `.filter()` returns a fresh array, so sorting in-place is safe.
+ // Embedded column: show the full stream (no filter sidebar to explain a
+ // mirrored filter). See LogStreamPanel (#1616). `.filter()` returns a fresh
+ // array, so sorting in-place is safe.
const sorted = entries
- .filter((e) =>
- matchesFilters(e, searchText, visibleDirections, methodFilter),
+ .filter(
+ (e) =>
+ embedded ||
+ matchesFilters(e, searchText, visibleDirections, methodFilter),
)
.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
if (sortDirection === "newest-first") sorted.reverse();
return sorted;
- }, [entries, searchText, visibleDirections, methodFilter, sortDirection]);
+ }, [
+ entries,
+ searchText,
+ visibleDirections,
+ methodFilter,
+ sortDirection,
+ embedded,
+ ]);
const pinnedEntries = useMemo(
() => filteredEntries.filter((e) => pinnedIds.has(e.id)),
diff --git a/clients/web/src/components/groups/LogStreamPanel/LogStreamPanel.tsx b/clients/web/src/components/groups/LogStreamPanel/LogStreamPanel.tsx
index 784faafe..a2ddd421 100644
--- a/clients/web/src/components/groups/LogStreamPanel/LogStreamPanel.tsx
+++ b/clients/web/src/components/groups/LogStreamPanel/LogStreamPanel.tsx
@@ -80,13 +80,16 @@ export function LogStreamPanel({
}: LogStreamPanelProps) {
const viewportRef = useScrollMemory("logs-stream");
const filteredEntries = useMemo(() => {
- // `.filter()` returns a fresh array, so sorting in-place is safe.
+ // The embedded column has no filter sidebar, so it shows the full stream
+ // rather than mirroring the full-size screen's live filter with no visible
+ // control to explain it (#1616). `.filter()` returns a fresh array, so
+ // sorting in-place is safe.
const sorted = entries
- .filter((e) => matchesFilters(e, filterText, visibleLevels))
+ .filter((e) => embedded || matchesFilters(e, filterText, visibleLevels))
.sort((a, b) => a.receivedAt.getTime() - b.receivedAt.getTime());
if (sortDirection === "newest-first") sorted.reverse();
return sorted;
- }, [entries, filterText, visibleLevels, sortDirection]);
+ }, [entries, filterText, visibleLevels, sortDirection, embedded]);
return (
diff --git a/clients/web/src/components/groups/MonitoringControls/MonitoringControls.test.tsx b/clients/web/src/components/groups/MonitoringControls/MonitoringControls.test.tsx
index 458d1cf4..8d6a0bca 100644
--- a/clients/web/src/components/groups/MonitoringControls/MonitoringControls.test.tsx
+++ b/clients/web/src/components/groups/MonitoringControls/MonitoringControls.test.tsx
@@ -33,6 +33,20 @@ describe("MonitoringControls", () => {
expect(screen.queryByRole("radio", { name: "Network" })).toBeNull();
});
+ it("gives the tab switcher an accessible name", () => {
+ const { container } = renderWithMantine(
+ ,
+ );
+ expect(
+ container.querySelector('[aria-label="Monitoring screen"]'),
+ ).not.toBeNull();
+ });
+
it("marks the active tab as selected", () => {
renderWithMantine(
diff --git a/clients/web/src/components/groups/NetworkStreamPanel/NetworkStreamPanel.tsx b/clients/web/src/components/groups/NetworkStreamPanel/NetworkStreamPanel.tsx
index 9d66367d..b1f58bba 100644
--- a/clients/web/src/components/groups/NetworkStreamPanel/NetworkStreamPanel.tsx
+++ b/clients/web/src/components/groups/NetworkStreamPanel/NetworkStreamPanel.tsx
@@ -98,13 +98,17 @@ export function NetworkStreamPanel({
}: NetworkStreamPanelProps) {
const viewportRef = useScrollMemory("network-stream");
const filteredEntries = useMemo(() => {
- // `.filter()` returns a fresh array, so sorting in-place is safe.
+ // Embedded column: show the full stream (no filter sidebar to explain a
+ // mirrored filter). See LogStreamPanel (#1616). `.filter()` returns a fresh
+ // array, so sorting in-place is safe.
const sorted = entries
- .filter((e) => matchesFilters(e, filterText, visibleCategories))
+ .filter(
+ (e) => embedded || matchesFilters(e, filterText, visibleCategories),
+ )
.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
if (sortDirection === "newest-first") sorted.reverse();
return sorted;
- }, [entries, filterText, visibleCategories, sortDirection]);
+ }, [entries, filterText, visibleCategories, sortDirection, embedded]);
const hasEntries = entries.length > 0;
const hasResults = filteredEntries.length > 0;
diff --git a/clients/web/src/components/screens/HistoryScreen/HistoryScreen.test.tsx b/clients/web/src/components/screens/HistoryScreen/HistoryScreen.test.tsx
index aad3f5da..c4e23a02 100644
--- a/clients/web/src/components/screens/HistoryScreen/HistoryScreen.test.tsx
+++ b/clients/web/src/components/screens/HistoryScreen/HistoryScreen.test.tsx
@@ -126,4 +126,17 @@ describe("HistoryScreen", () => {
// The sidebar (HistoryControls, with its Search box) is not rendered.
expect(screen.queryByPlaceholderText("Search...")).toBeNull();
});
+
+ it("shows the full stream when embedded, ignoring the live filter", () => {
+ // A search that matches nothing would empty the full-size screen...
+ renderWithMantine(
+ ,
+ );
+ // ...but the embedded column shows the full stream (not the empty state).
+ expect(screen.queryByText("No request history")).toBeNull();
+ });
});
diff --git a/clients/web/src/components/screens/LoggingScreen/LoggingScreen.test.tsx b/clients/web/src/components/screens/LoggingScreen/LoggingScreen.test.tsx
index 11e1e84a..b5df10ab 100644
--- a/clients/web/src/components/screens/LoggingScreen/LoggingScreen.test.tsx
+++ b/clients/web/src/components/screens/LoggingScreen/LoggingScreen.test.tsx
@@ -146,4 +146,24 @@ describe("LoggingScreen", () => {
// The sidebar (LogControls, with its Set button) is not rendered.
expect(screen.queryByRole("button", { name: "Set" })).toBeNull();
});
+
+ it("shows the full stream when embedded, ignoring the live filter", () => {
+ const entries = [
+ {
+ receivedAt: new Date(),
+ params: { level: "info" as const, data: "hello" },
+ },
+ ];
+ // A filter that would hide the entry on the full-size screen...
+ renderWithMantine(
+ ,
+ );
+ // ...is ignored in the embedded, sidebar-less column.
+ expect(screen.getByText("hello")).toBeInTheDocument();
+ });
});
diff --git a/clients/web/src/components/screens/NetworkScreen/NetworkScreen.test.tsx b/clients/web/src/components/screens/NetworkScreen/NetworkScreen.test.tsx
index a093d39b..0059a794 100644
--- a/clients/web/src/components/screens/NetworkScreen/NetworkScreen.test.tsx
+++ b/clients/web/src/components/screens/NetworkScreen/NetworkScreen.test.tsx
@@ -179,4 +179,17 @@ describe("NetworkScreen", () => {
// The stream toolbar (Export) is still present.
expect(screen.getByRole("button", { name: "Export" })).toBeInTheDocument();
});
+
+ it("shows the full stream when embedded, ignoring the live filter", () => {
+ // A filter that matches nothing would empty the full-size screen...
+ renderWithMantine(
+ ,
+ );
+ // ...but the embedded column shows the full stream (not the empty state).
+ expect(screen.queryByText("No network requests")).toBeNull();
+ });
});