feat(web): pin Logging/History/Network as a resizable column (#1616)

Add the ability to "pin" the Logging, History, and Network screens into a
second column on the right of the InspectorView, so a primary screen and a
monitor screen are visible at once (e.g. watch Logs while calling a Tool).

- A "pin as column" button on the Logs/History/Network panel toolbars opens
  the column (shown only when wide + connected).
- The column hosts a MonitoringControls tab row (available monitor tabs +
  close) over the selected embedded screen. Pinning removes the monitor group
  from the header tab bar; closing (x) returns it.
- The divider between the primary area and the column is draggable (pointer +
  keyboard); width is clamped and persisted.
- The split collapses to one column below ~1040px (the ServerListScreen
  single-card breakpoint) and while disconnected, without discarding the pin
  preference — it reopens when wide + connected again.

State (pinned/tab/width) is view-local and persisted via the existing
useLocalStorage pattern, so no new props thread through App and ViewHeader is
unchanged (it just receives a filtered tab list). Screens/panels gain an
`embedded` mode (fills the parent height, drops the 340px filter sidebar) via a
shared EmbeddableScrollArea element.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
cliffhall 2026-07-08 18:52:41 -04:00
commit 081fd789f1

View file

@ -1,13 +1,5 @@
import { useMemo } from "react";
import {
Button,
Group,
Paper,
ScrollArea,
Stack,
Text,
Title,
} from "@mantine/core";
import { Button, Group, Paper, Stack, Text, Title } from "@mantine/core";
import type {
FetchRequestCategory,
FetchRequestEntry,
@ -18,6 +10,8 @@ import {
SortToggle,
type SortDirection,
} from "../../elements/SortToggle/SortToggle";
import { PinColumnButton } from "../../elements/PinColumnButton/PinColumnButton";
import { EmbeddableScrollArea } from "../../elements/EmbeddableScrollArea/EmbeddableScrollArea";
import { useScrollMemory } from "../../../hooks/useScrollMemory";
export interface NetworkStreamPanelProps {
@ -30,6 +24,10 @@ export interface NetworkStreamPanelProps {
onSortChange: (next: SortDirection) => void;
compact: boolean;
onToggleCompact: () => void;
/** See LogStreamPanel: shows a "pin as column" button when set (#1616). */
onPin?: () => void;
/** See LogStreamPanel: fills the flex parent instead of the viewport calc. */
embedded?: boolean;
}
const PanelContainer = Paper.withProps({
@ -95,6 +93,8 @@ export function NetworkStreamPanel({
onSortChange,
compact,
onToggleCompact,
onPin,
embedded = false,
}: NetworkStreamPanelProps) {
const viewportRef = useScrollMemory("network-stream");
const filteredEntries = useMemo(() => {
@ -114,6 +114,7 @@ export function NetworkStreamPanel({
<Group justify="space-between" mb="sm">
<Title order={4}>{formatTitle(filteredEntries.length)}</Title>
<Group gap="xs">
{onPin ? <PinColumnButton onPin={onPin} /> : null}
{hasResults && (
<ListToggle compact={compact} onToggle={onToggleCompact} />
)}
@ -134,12 +135,7 @@ export function NetworkStreamPanel({
{!hasResults ? (
<EmptyState>No network requests</EmptyState>
) : (
<ScrollArea.Autosize
viewportRef={viewportRef}
mah="calc(100vh - var(--app-shell-header-height, 0px) - 150px)"
type="scroll"
offsetScrollbars
>
<EmbeddableScrollArea embedded={embedded} viewportRef={viewportRef}>
<Stack gap="md">
{filteredEntries.map((entry) => (
<NetworkEntry
@ -149,7 +145,7 @@ export function NetworkStreamPanel({
/>
))}
</Stack>
</ScrollArea.Autosize>
</EmbeddableScrollArea>
)}
</PanelContainer>
);