Merge pull request #1634 from modelcontextprotocol/1623-rename-history-to-protocol

refactor(web): rename "History" tab/feature to "Protocol" (#1623)
This commit is contained in:
Cliff Hall 2026-07-09 21:49:01 -04:00 • committed by GitHub
commit 50e780ff71
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
52 changed files with 524 additions and 429 deletions

View file

@ -309,10 +309,10 @@ vi.mock("./components/views/InspectorView/InspectorView", () => ({
onClearCompletedTasks: () => void; onClearCompletedTasks: () => void;
onRefreshTasks: () => void; onRefreshTasks: () => void;
onServerSettings: (id: string) => void; onServerSettings: (id: string) => void;
onClearHistory: () => void; onClearProtocol: () => void;
onReplayHistory: (id: string) => void; onReplayProtocol: (id: string) => void;
onTogglePinHistory: (id: string) => void; onTogglePinProtocol: (id: string) => void;
pinnedHistoryIds?: Set<string>; pinnedProtocolIds?: Set<string>;
}) => ( }) => (
<div> <div>
<span data-testid="tool-status"> <span data-testid="tool-status">
@ -419,15 +419,15 @@ vi.mock("./components/views/InspectorView/InspectorView", () => ({
<button onClick={() => props.onSetLogLevel("debug")}>set-level</button> <button onClick={() => props.onSetLogLevel("debug")}>set-level</button>
<button onClick={() => props.onServerSettings("A")}>open-settings</button> <button onClick={() => props.onServerSettings("A")}>open-settings</button>
<span data-testid="pinned-history"> <span data-testid="pinned-history">
{Array.from(props.pinnedHistoryIds ?? []).join(",")} {Array.from(props.pinnedProtocolIds ?? []).join(",")}
</span> </span>
<button onClick={() => props.onTogglePinHistory("hist-1")}> <button onClick={() => props.onTogglePinProtocol("hist-1")}>
toggle-pin toggle-pin
</button> </button>
<button onClick={() => props.onReplayHistory("hist-1")}> <button onClick={() => props.onReplayProtocol("hist-1")}>
replay-history replay-history
</button> </button>
<button onClick={() => props.onClearHistory()}>clear-history</button> <button onClick={() => props.onClearProtocol()}>clear-history</button>
</div> </div>
), ),
})); }));

View file

@ -114,7 +114,7 @@ import {
EMPTY_APPS_UI, EMPTY_APPS_UI,
EMPTY_TASKS_UI, EMPTY_TASKS_UI,
EMPTY_LOGS_UI, EMPTY_LOGS_UI,
EMPTY_HISTORY_UI, EMPTY_PROTOCOL_UI,
EMPTY_NETWORK_UI, EMPTY_NETWORK_UI,
EMPTY_CONSOLE_UI, EMPTY_CONSOLE_UI,
} from "./components/screens/screenUiState"; } from "./components/screens/screenUiState";
@ -140,7 +140,7 @@ import { ConnectionInfoModal } from "./components/groups/ConnectionInfoModal/Con
import { oauthDetailsFromConnectionState } from "./components/groups/ConnectionInfoContent/oauthDetailsFromConnectionState"; import { oauthDetailsFromConnectionState } from "./components/groups/ConnectionInfoContent/oauthDetailsFromConnectionState";
import { OutputValidationModal } from "./components/groups/OutputValidationModal/OutputValidationModal"; import { OutputValidationModal } from "./components/groups/OutputValidationModal/OutputValidationModal";
import { UrlElicitationErrorModal } from "./components/groups/UrlElicitationErrorModal/UrlElicitationErrorModal"; import { UrlElicitationErrorModal } from "./components/groups/UrlElicitationErrorModal/UrlElicitationErrorModal";
import { isReplayableHistoryMethod } from "./components/groups/historyUtils.js"; import { isReplayableProtocolMethod } from "./components/groups/protocolUtils.js";
import type { OAuthDetails } from "./components/groups/ConnectionInfoContent/ConnectionInfoContent"; import type { OAuthDetails } from "./components/groups/ConnectionInfoContent/ConnectionInfoContent";
import { ServerRemoveConfirmModal } from "./components/groups/ServerRemoveConfirmModal/ServerRemoveConfirmModal"; import { ServerRemoveConfirmModal } from "./components/groups/ServerRemoveConfirmModal/ServerRemoveConfirmModal";
import { StepUpAuthModal } from "./components/groups/StepUpAuthModal/StepUpAuthModal"; import { StepUpAuthModal } from "./components/groups/StepUpAuthModal/StepUpAuthModal";
@ -267,21 +267,21 @@ function messagesToLogEntries(messages: MessageEntry[]): LogEntryData[] {
return out; return out;
} }
// Re-issue the original request behind a History entry. The call goes through // Re-issue the original request behind a Protocol entry. The call goes through
// InspectorClient → tracked transport → message log, so the replayed // InspectorClient → tracked transport → message log, so the replayed
// request+response surface as a fresh History entry (history-local) — it // request+response surface as a fresh Protocol entry (protocol-local) — it
// intentionally does NOT touch the Tools/Prompts/Resources panels. Returns a // intentionally does NOT touch the Tools/Prompts/Resources panels. Returns a
// human-readable reason when the entry can't be replayed (unsupported method, // human-readable reason when the entry can't be replayed (unsupported method,
// or a tool that's no longer present), or null on a dispatched replay. // or a tool that's no longer present), or null on a dispatched replay.
async function replayHistoryRequest( async function replayProtocolRequest(
client: InspectorClient, client: InspectorClient,
method: string, method: string,
params: Record<string, unknown> | undefined, params: Record<string, unknown> | undefined,
tools: Tool[], tools: Tool[],
): Promise<string | null> { ): Promise<string | null> {
// Gate on the shared replayable-method set (the same one HistoryEntry uses to // Gate on the shared replayable-method set (the same one ProtocolEntry uses to
// show/hide the Replay button) so the two can't drift. // show/hide the Replay button) so the two can't drift.
if (!isReplayableHistoryMethod(method)) { if (!isReplayableProtocolMethod(method)) {
return `Replay isn't supported for "${method}".`; return `Replay isn't supported for "${method}".`;
} }
// Pagination cursor carried by the */list requests; replaying the same page // Pagination cursor carried by the */list requests; replaying the same page
@ -755,11 +755,11 @@ function App() {
const [appsUi, setAppsUi] = useState(EMPTY_APPS_UI); const [appsUi, setAppsUi] = useState(EMPTY_APPS_UI);
const [tasksUi, setTasksUi] = useState(EMPTY_TASKS_UI); const [tasksUi, setTasksUi] = useState(EMPTY_TASKS_UI);
const [logsUi, setLogsUi] = useState(EMPTY_LOGS_UI); const [logsUi, setLogsUi] = useState(EMPTY_LOGS_UI);
const [historyUi, setHistoryUi] = useState(EMPTY_HISTORY_UI); const [protocolUi, setProtocolUi] = useState(EMPTY_PROTOCOL_UI);
// History entries the user pinned (by entry id). Session-scoped — the ids // Protocol entries the user pinned (by entry id). Session-scoped — the ids
// reference message-log entries, which clear on disconnect, so this resets // reference message-log entries, which clear on disconnect, so this resets
// with the rest of the per-screen state. // with the rest of the per-screen state.
const [pinnedHistoryIds, setPinnedHistoryIds] = useState<Set<string>>( const [pinnedProtocolIds, setPinnedProtocolIds] = useState<Set<string>>(
() => new Set(), () => new Set(),
); );
const [networkUi, setNetworkUi] = useState(EMPTY_NETWORK_UI); const [networkUi, setNetworkUi] = useState(EMPTY_NETWORK_UI);
@ -1014,8 +1014,8 @@ function App() {
setAppsUi(EMPTY_APPS_UI); setAppsUi(EMPTY_APPS_UI);
setTasksUi(EMPTY_TASKS_UI); setTasksUi(EMPTY_TASKS_UI);
setLogsUi(EMPTY_LOGS_UI); setLogsUi(EMPTY_LOGS_UI);
setHistoryUi(EMPTY_HISTORY_UI); setProtocolUi(EMPTY_PROTOCOL_UI);
setPinnedHistoryIds(new Set()); setPinnedProtocolIds(new Set());
setNetworkUi(EMPTY_NETWORK_UI); setNetworkUi(EMPTY_NETWORK_UI);
// Only the search filter resets here; the stderr entries themselves live in // Only the search filter resets here; the stderr entries themselves live in
// StderrLogState, which deliberately survives connect/disconnect so a failed // StderrLogState, which deliberately survives connect/disconnect so a failed
@ -1059,7 +1059,7 @@ function App() {
// Surface incoming `notifications/progress` as toasts so the user can watch a // Surface incoming `notifications/progress` as toasts so the user can watch a
// long-running tool's progress while staying on the tool view — the v2 // long-running tool's progress while staying on the tool view — the v2
// replacement for v1's always-visible "Server Notifications" shelf (#1414). // replacement for v1's always-visible "Server Notifications" shelf (#1414).
// The full notification history still lives in the History tab; these toasts // The full notification history still lives in the Protocol tab; these toasts
// are the at-a-glance, in-context signal. Toasts are keyed by progress stream // are the at-a-glance, in-context signal. Toasts are keyed by progress stream
// (see `progressToastId`) and replaced per tick so a chatty server updates one // (see `progressToastId`) and replaced per tick so a chatty server updates one
// toast rather than stacking one per tick. // toast rather than stacking one per tick.
@ -1171,7 +1171,7 @@ function App() {
// with `requestorTaskUpdated` so we skip it to avoid double-firing. One toast per // with `requestorTaskUpdated` so we skip it to avoid double-firing. One toast per
// taskId, replaced per tick, dismissed on terminal status (which also prunes the // taskId, replaced per tick, dismissed on terminal status (which also prunes the
// task's progress entry) and on client teardown. The full status history still // task's progress entry) and on client teardown. The full status history still
// lives in the History view. // lives in the Protocol view.
useEffect(() => { useEffect(() => {
if (!inspectorClient) return; if (!inspectorClient) return;
const liveToastIds = taskToastIdsRef.current; const liveToastIds = taskToastIdsRef.current;
@ -1560,7 +1560,7 @@ function App() {
appsUi, appsUi,
tasksUi, tasksUi,
logsUi, logsUi,
historyUi, protocolUi,
networkUi, networkUi,
}), }),
...(remoteSessionId && { remoteSessionId }), ...(remoteSessionId && { remoteSessionId }),
@ -1578,7 +1578,7 @@ function App() {
appsUi, appsUi,
tasksUi, tasksUi,
logsUi, logsUi,
historyUi, protocolUi,
networkUi, networkUi,
onBeforeOAuthRedirect, onBeforeOAuthRedirect,
], ],
@ -2189,7 +2189,7 @@ function App() {
setAppsUi, setAppsUi,
setTasksUi, setTasksUi,
setLogsUi, setLogsUi,
setHistoryUi, setProtocolUi,
setNetworkUi, setNetworkUi,
setActiveTab, setActiveTab,
clearToolCallState: () => setToolCallState(undefined), clearToolCallState: () => setToolCallState(undefined),
@ -2987,7 +2987,7 @@ function App() {
const onClearLogs = useCallback(() => { const onClearLogs = useCallback(() => {
if (!messageLogState) return; if (!messageLogState) return;
// Clear only the log notifications, not the entire request/response // Clear only the log notifications, not the entire request/response
// history (which the History screen renders from the same source). // history (which the Protocol screen renders from the same source).
messageLogState.clearMessages( messageLogState.clearMessages(
(m) => (m) =>
m.direction === "notification" && m.direction === "notification" &&
@ -2999,10 +2999,10 @@ function App() {
// Panel-level Clear clears the (unpinned) history and keeps pinned entries — // Panel-level Clear clears the (unpinned) history and keeps pinned entries —
// pinning is the way to protect an entry from Clear. This matches the button's // pinning is the way to protect an entry from Clear. This matches the button's
// `disabled={unpinnedEntries.length === 0}` gating and the per-section model, // `disabled={unpinnedEntries.length === 0}` gating and the per-section model,
// and leaves pinnedHistoryIds valid (the pins it references still exist). // and leaves pinnedProtocolIds valid (the pins it references still exist).
const onClearHistory = useCallback(() => { const onClearProtocol = useCallback(() => {
messageLogState?.clearMessages((m) => !pinnedHistoryIds.has(m.id)); messageLogState?.clearMessages((m) => !pinnedProtocolIds.has(m.id));
}, [messageLogState, pinnedHistoryIds]); }, [messageLogState, pinnedProtocolIds]);
const onClearNetwork = useCallback(() => { const onClearNetwork = useCallback(() => {
fetchRequestLogState?.clearFetchRequests(); fetchRequestLogState?.clearFetchRequests();
@ -3016,50 +3016,50 @@ function App() {
); );
}, [fetchRequests, activeServerId]); }, [fetchRequests, activeServerId]);
const onExportHistory = useCallback(() => { const onExportProtocol = useCallback(() => {
if (messages.length === 0) return; if (messages.length === 0) return;
downloadJsonFile( downloadJsonFile(
buildExportFilename("history", activeServerId), buildExportFilename("protocol", activeServerId),
JSON.stringify(messages, null, 2), JSON.stringify(messages, null, 2),
); );
}, [messages, activeServerId]); }, [messages, activeServerId]);
// Clear just one section: remove its entries from the log by pin membership. // Clear just one section: remove its entries from the log by pin membership.
// Clearing the pinned section also drops the (now-stale) pinned id set. // Clearing the pinned section also drops the (now-stale) pinned id set.
const onClearHistorySection = useCallback( const onClearProtocolSection = useCallback(
(section: "pinned" | "history") => { (section: "pinned" | "history") => {
const isPinned = section === "pinned"; const isPinned = section === "pinned";
messageLogState?.clearMessages((m) => messageLogState?.clearMessages((m) =>
isPinned ? pinnedHistoryIds.has(m.id) : !pinnedHistoryIds.has(m.id), isPinned ? pinnedProtocolIds.has(m.id) : !pinnedProtocolIds.has(m.id),
); );
if (isPinned) setPinnedHistoryIds(new Set()); if (isPinned) setPinnedProtocolIds(new Set());
}, },
[messageLogState, pinnedHistoryIds], [messageLogState, pinnedProtocolIds],
); );
// Export just one section's entries (by pin membership) to a JSON file. // Export just one section's entries (by pin membership) to a JSON file.
const onExportHistorySection = useCallback( const onExportProtocolSection = useCallback(
(section: "pinned" | "history") => { (section: "pinned" | "history") => {
const isPinned = section === "pinned"; const isPinned = section === "pinned";
const subset = messages.filter((m) => const subset = messages.filter((m) =>
isPinned ? pinnedHistoryIds.has(m.id) : !pinnedHistoryIds.has(m.id), isPinned ? pinnedProtocolIds.has(m.id) : !pinnedProtocolIds.has(m.id),
); );
if (subset.length === 0) return; if (subset.length === 0) return;
downloadJsonFile( downloadJsonFile(
buildExportFilename( buildExportFilename(
isPinned ? "history-pinned" : "history-unpinned", isPinned ? "protocol-pinned" : "protocol-unpinned",
activeServerId, activeServerId,
), ),
JSON.stringify(subset, null, 2), JSON.stringify(subset, null, 2),
); );
}, },
[messages, pinnedHistoryIds, activeServerId], [messages, pinnedProtocolIds, activeServerId],
); );
// Pin/unpin a history entry by id. HistoryListPanel sorts pinned entries to // Pin/unpin a Protocol entry by id. ProtocolListPanel sorts pinned entries to
// the top; the set is session-scoped (see resetSessionScopedUiState). // the top; the set is session-scoped (see resetSessionScopedUiState).
const onTogglePinHistory = useCallback((id: string) => { const onTogglePinProtocol = useCallback((id: string) => {
setPinnedHistoryIds((prev) => { setPinnedProtocolIds((prev) => {
const next = new Set(prev); const next = new Set(prev);
if (next.has(id)) { if (next.has(id)) {
next.delete(id); next.delete(id);
@ -3070,12 +3070,12 @@ function App() {
}); });
}, []); }, []);
// Replay a history entry: re-issue its original request so the fresh // Replay a Protocol entry: re-issue its original request so the fresh
// request+response appear as a new History entry (history-local). A reason // request+response appear as a new Protocol entry (protocol-local). A reason
// string (unsupported method / missing tool) surfaces as a toast; a genuine // string (unsupported method / missing tool) surfaces as a toast; a genuine
// call error already shows up as the replayed entry's Error status, so only a // call error already shows up as the replayed entry's Error status, so only a
// pre-flight failure (nothing logged) needs the fallback toast. // pre-flight failure (nothing logged) needs the fallback toast.
const onReplayHistory = useCallback( const onReplayProtocol = useCallback(
(id: string) => { (id: string) => {
if (!inspectorClient) return; if (!inspectorClient) return;
const entry = messages.find((m) => m.id === id); const entry = messages.find((m) => m.id === id);
@ -3085,7 +3085,7 @@ function App() {
"params" in entry.message "params" in entry.message
? (entry.message.params as Record<string, unknown> | undefined) ? (entry.message.params as Record<string, unknown> | undefined)
: undefined; : undefined;
void replayHistoryRequest(inspectorClient, method, params, tools) void replayProtocolRequest(inspectorClient, method, params, tools)
.then((reason) => { .then((reason) => {
if (reason) { if (reason) {
notifications.show({ notifications.show({
@ -3687,7 +3687,7 @@ function App() {
logs={logs} logs={logs}
tasks={tasks} tasks={tasks}
progressByTaskId={progressByTaskId} progressByTaskId={progressByTaskId}
history={messages} protocol={messages}
network={fetchRequests} network={fetchRequests}
stderrLogs={stderrLogs} stderrLogs={stderrLogs}
toolCallState={toolCallState} toolCallState={toolCallState}
@ -3699,7 +3699,7 @@ function App() {
appsUi={appsUi} appsUi={appsUi}
tasksUi={tasksUi} tasksUi={tasksUi}
logsUi={logsUi} logsUi={logsUi}
historyUi={historyUi} protocolUi={protocolUi}
networkUi={networkUi} networkUi={networkUi}
consoleUi={consoleUi} consoleUi={consoleUi}
activeTab={activeTab} activeTab={activeTab}
@ -3792,14 +3792,14 @@ function App() {
onLogsUiChange={setLogsUi} onLogsUiChange={setLogsUi}
onClearLogs={onClearLogs} onClearLogs={onClearLogs}
onExportLogs={onExportLogs} onExportLogs={onExportLogs}
onHistoryUiChange={setHistoryUi} onProtocolUiChange={setProtocolUi}
onClearHistory={onClearHistory} onClearProtocol={onClearProtocol}
onExportHistory={onExportHistory} onExportProtocol={onExportProtocol}
onClearHistorySection={onClearHistorySection} onClearProtocolSection={onClearProtocolSection}
onExportHistorySection={onExportHistorySection} onExportProtocolSection={onExportProtocolSection}
onReplayHistory={onReplayHistory} onReplayProtocol={onReplayProtocol}
onTogglePinHistory={onTogglePinHistory} onTogglePinProtocol={onTogglePinProtocol}
pinnedHistoryIds={pinnedHistoryIds} pinnedProtocolIds={pinnedProtocolIds}
onNetworkUiChange={setNetworkUi} onNetworkUiChange={setNetworkUi}
onClearNetwork={onClearNetwork} onClearNetwork={onClearNetwork}
onExportNetwork={onExportNetwork} onExportNetwork={onExportNetwork}

View file

@ -31,4 +31,27 @@ describe("EmbeddableScrollArea", () => {
); );
expect(ref.current).toBeInstanceOf(HTMLElement); expect(ref.current).toBeInstanceOf(HTMLElement);
}); });
it.each([false, true])(
"constrains the content width when asked (embedded=%s)",
(embedded) => {
const ref = createRef<HTMLDivElement>();
renderWithMantine(
<EmbeddableScrollArea
embedded={embedded}
viewportRef={ref}
constrainContentWidth
>
<div>constrained content</div>
</EmbeddableScrollArea>,
);
expect(screen.getByText("constrained content")).toBeInTheDocument();
// The Mantine ScrollArea `content` slot has its default
// `min-width: max-content` relaxed to 0 so a long non-wrapping row
// (e.g. a network URL) can't stretch the list past its viewport (#1623).
const content = document.querySelector(".mantine-ScrollArea-content");
expect(content).not.toBeNull();
expect((content as HTMLElement).style.minWidth).toBe("0");
},
);
}); });

View file

@ -17,10 +17,25 @@ export interface EmbeddableScrollAreaProps {
embedded: boolean; embedded: boolean;
viewportRef: Ref<HTMLDivElement>; viewportRef: Ref<HTMLDivElement>;
children: ReactNode; children: ReactNode;
/**
* Constrain the scrolled content to the viewport width instead of letting it
* grow to its own `max-content`. Mantine's ScrollArea `content` slot defaults
* to `min-width: max-content`, so a row with non-wrapping content (e.g. a long
* network URL) stretches every card past the column and it bleeds out (#1623).
* When true, the content can shrink to the viewport and each row must manage
* its own overflow (the Network URL scrolls inside its own inner ScrollArea).
* Left off for panels whose rows already wrap/truncate (Logs, Protocol), where
* the default lets a long line scroll the list horizontally instead.
*/
constrainContentWidth?: boolean;
} }
// Relax the `content` slot's default `min-width: max-content` so the list can't
// grow wider than its viewport; see `constrainContentWidth`.
const CONSTRAIN_CONTENT_STYLES = { content: { minWidth: 0 } } as const;
/** /**
* The scroll region shared by the Logs / History / Network stream panels, which * The scroll region shared by the Logs / Protocol / Network stream panels, which
* render both full-size (their own tab) and embedded (the monitoring column). * render both full-size (their own tab) and embedded (the monitoring column).
* Centralizes the one layout difference between those two hosts. * Centralizes the one layout difference between those two hosts.
*/ */
@ -28,7 +43,9 @@ export function EmbeddableScrollArea({
embedded, embedded,
viewportRef, viewportRef,
children, children,
constrainContentWidth = false,
}: EmbeddableScrollAreaProps) { }: EmbeddableScrollAreaProps) {
const styles = constrainContentWidth ? CONSTRAIN_CONTENT_STYLES : undefined;
if (embedded) { if (embedded) {
return ( return (
<Stack flex={1} mih={0} gap={0}> <Stack flex={1} mih={0} gap={0}>
@ -37,6 +54,7 @@ export function EmbeddableScrollArea({
mah="100%" mah="100%"
type="scroll" type="scroll"
offsetScrollbars offsetScrollbars
styles={styles}
> >
{children} {children}
</ScrollArea.Autosize> </ScrollArea.Autosize>
@ -49,6 +67,7 @@ export function EmbeddableScrollArea({
mah={FULLSIZE_MAH} mah={FULLSIZE_MAH}
type="scroll" type="scroll"
offsetScrollbars offsetScrollbars
styles={styles}
> >
{children} {children}
</ScrollArea.Autosize> </ScrollArea.Autosize>

View file

@ -8,7 +8,7 @@ export interface ExpandToggleProps {
} }
/** /**
* Icon toggle for a per-entry expand/collapse control (History, Network, and * Icon toggle for a per-entry expand/collapse control (Protocol, Network, and
* Task cards). Uses the same expand/collapse-vertical icons as the list-level * Task cards). Uses the same expand/collapse-vertical icons as the list-level
* ListToggle: collapsed shows the expand icon, expanded shows the collapse * ListToggle: collapsed shows the expand icon, expanded shows the collapse
* icon. The aria-label stays "Expand"/"Collapse" so it reads the same as the * icon. The aria-label stays "Expand"/"Collapse" so it reads the same as the

View file

@ -17,7 +17,7 @@ const ToggleLabel = Text.withProps({
}); });
/** /**
* A single full-width filter toggle used by the Logging, History, and Network * A single full-width filter toggle used by the Logging, Protocol, and Network
* controls. The `filterToggle` theme variant + `.filter-toggle` rules own the * controls. The `filterToggle` theme variant + `.filter-toggle` rules own the
* styling: hover shows a thin border, the active (`aria-pressed`) state shows a * styling: hover shows a thin border, the active (`aria-pressed`) state shows a
* filled background. Keeping hover as a border (not a fill) means toggling a * filled background. Keeping hover as a border (not a fill) means toggling a

View file

@ -22,6 +22,6 @@ export const Incoming: Story = {
args: { direction: "incoming" }, args: { direction: "incoming" },
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(canvas.getByText("client ← server")).toBeInTheDocument(); expect(canvas.getByText("server → client")).toBeInTheDocument();
}, },
}; };

View file

@ -8,8 +8,8 @@ describe("MessageDirectionBadge", () => {
expect(screen.getByText("client → server")).toBeInTheDocument(); expect(screen.getByText("client → server")).toBeInTheDocument();
}); });
it("renders client ← server for incoming", () => { it("renders server → client for incoming", () => {
renderWithMantine(<MessageDirectionBadge direction="incoming" />); renderWithMantine(<MessageDirectionBadge direction="incoming" />);
expect(screen.getByText("client ← server")).toBeInTheDocument(); expect(screen.getByText("server → client")).toBeInTheDocument();
}); });
}); });

View file

@ -4,21 +4,21 @@ export interface MessageDirectionBadgeProps {
/** /**
* Direction of travel for the entry: "outgoing" = the inspector sent it to * Direction of travel for the entry: "outgoing" = the inspector sent it to
* the server (client → server); "incoming" = the server sent it to the * the server (client → server); "incoming" = the server sent it to the
* inspector (client ← server). * inspector (server → client).
*/ */
direction: "outgoing" | "incoming"; direction: "outgoing" | "incoming";
} }
const LABEL: Record<MessageDirectionBadgeProps["direction"], string> = { const LABEL: Record<MessageDirectionBadgeProps["direction"], string> = {
outgoing: "client → server", outgoing: "client → server",
incoming: "client ← server", incoming: "server → client",
}; };
/** /**
* Dual-state badge showing which way a History/Network entry traveled. Outgoing * Dual-state badge showing which way a Protocol/Network entry traveled. Outgoing
* (client → server) is green; incoming (client ← server) is purple — not yellow, * (client → server) is green; incoming (server → client) is purple — not yellow,
* which (paired with green) reads as caution/ok status rather than direction. * which (paired with green) reads as caution/ok status rather than direction.
* Shared by `HistoryEntry` and `NetworkEntry`. * Shared by `ProtocolEntry` and `NetworkEntry`.
*/ */
export function MessageDirectionBadge({ export function MessageDirectionBadge({
direction, direction,

View file

@ -7,7 +7,7 @@ export interface PinColumnButtonProps {
} }
/** /**
* Toolbar button that pins the owning monitor screen (Logs / History / Network) * Toolbar button that pins the owning monitor screen (Logs / Protocol / Network)
* into the resizable column on the right of the InspectorView (#1616). Distinct * into the resizable column on the right of the InspectorView (#1616). Distinct
* from `PinToggle` (which pins individual history entries) — this one opens a * from `PinToggle` (which pins individual history entries) — this one opens a
* side column, so it uses a right-sidebar glyph and an "as column" label. Styled * side column, so it uses a right-sidebar glyph and an "as column" label. Styled

View file

@ -7,7 +7,7 @@ export interface ReplayButtonProps {
} }
/** /**
* Icon form of the "Replay" action, used in the compact (column) HistoryEntry * Icon form of the "Replay" action, used in the compact (column) ProtocolEntry
* layout where the text button is replaced by a replay icon sitting next to the * layout where the text button is replaced by a replay icon sitting next to the
* pin toggle (#1616). Matches PinToggle's subtle gray icon-button styling. * pin toggle (#1616). Matches PinToggle's subtle gray icon-button styling.
*/ */

View file

@ -17,7 +17,7 @@ describe("MessageDirectionFilter", () => {
screen.getByRole("button", { name: "client → server" }), screen.getByRole("button", { name: "client → server" }),
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
screen.getByRole("button", { name: "client ← server" }), screen.getByRole("button", { name: "server → client" }),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
@ -30,7 +30,7 @@ describe("MessageDirectionFilter", () => {
onToggleDirection={onToggleDirection} onToggleDirection={onToggleDirection}
/>, />,
); );
await user.click(screen.getByRole("button", { name: "client ← server" })); await user.click(screen.getByRole("button", { name: "server → client" }));
expect(onToggleDirection).toHaveBeenCalledWith("server", false); expect(onToggleDirection).toHaveBeenCalledWith("server", false);
}); });

View file

@ -9,14 +9,14 @@ const SubtleButton = Button.withProps({
// The two message directions, in display order. Label + color mirror the // The two message directions, in display order. Label + color mirror the
// MessageDirectionBadge: outgoing (client → server) is green, incoming // MessageDirectionBadge: outgoing (client → server) is green, incoming
// (client ← server) is violet. // (server → client) is violet.
const MESSAGE_DIRECTIONS: { const MESSAGE_DIRECTIONS: {
origin: MessageOrigin; origin: MessageOrigin;
label: string; label: string;
color: string; color: string;
}[] = [ }[] = [
{ origin: "client", label: "client → server", color: "green" }, { origin: "client", label: "client → server", color: "green" },
{ origin: "server", label: "client ← server", color: "violet" }, { origin: "server", label: "server → client", color: "violet" },
]; ];
export interface MessageDirectionFilterProps { export interface MessageDirectionFilterProps {
@ -27,8 +27,8 @@ export interface MessageDirectionFilterProps {
/** /**
* "Filter by Message Direction" section — a Select/Deselect All control plus a * "Filter by Message Direction" section — a Select/Deselect All control plus a
* FilterToggleButton per direction (client → server / client ← server). Used by * FilterToggleButton per direction (client → server / server → client). Used by
* the History controls. (Kept as its own component so the section is testable in * the Protocol controls. (Kept as its own component so the section is testable in
* isolation and reusable if another screen ever needs a direction filter.) * isolation and reusable if another screen ever needs a direction filter.)
*/ */
export function MessageDirectionFilter({ export function MessageDirectionFilter({

View file

@ -6,7 +6,7 @@ const meta: Meta<typeof MonitoringControls> = {
title: "Groups/MonitoringControls", title: "Groups/MonitoringControls",
component: MonitoringControls, component: MonitoringControls,
args: { args: {
tabs: ["Logs", "History", "Network"], tabs: ["Logs", "Protocol", "Network"],
value: "Logs", value: "Logs",
onChange: fn(), onChange: fn(),
searchValue: "", searchValue: "",
@ -29,7 +29,7 @@ export const AllTabs: Story = {
}; };
export const StdioServer: Story = { export const StdioServer: Story = {
args: { tabs: ["Logs", "History"], value: "History" }, args: { tabs: ["Logs", "Protocol"], value: "Protocol" },
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(canvas.queryByRole("radio", { name: "Network" })).toBeNull(); expect(canvas.queryByRole("radio", { name: "Network" })).toBeNull();

View file

@ -6,7 +6,7 @@ import {
type MonitoringControlsProps, type MonitoringControlsProps,
} from "./MonitoringControls"; } from "./MonitoringControls";
const TABS = ["Logs", "History", "Network"]; const TABS = ["Logs", "Protocol", "Network"];
function renderControls(overrides: Partial<MonitoringControlsProps> = {}) { function renderControls(overrides: Partial<MonitoringControlsProps> = {}) {
const props: MonitoringControlsProps = { const props: MonitoringControlsProps = {
@ -31,7 +31,7 @@ describe("MonitoringControls", () => {
}); });
it("renders only the tabs it is given", () => { it("renders only the tabs it is given", () => {
renderControls({ tabs: ["Logs", "History"] }); renderControls({ tabs: ["Logs", "Protocol"] });
expect(screen.getAllByRole("radio")).toHaveLength(2); expect(screen.getAllByRole("radio")).toHaveLength(2);
expect(screen.queryByRole("radio", { name: "Network" })).toBeNull(); expect(screen.queryByRole("radio", { name: "Network" })).toBeNull();
}); });
@ -53,8 +53,8 @@ describe("MonitoringControls", () => {
}); });
it("marks the active tab as selected", () => { it("marks the active tab as selected", () => {
renderControls({ value: "History" }); renderControls({ value: "Protocol" });
expect(screen.getByRole("radio", { name: "History" })).toBeChecked(); expect(screen.getByRole("radio", { name: "Protocol" })).toBeChecked();
}); });
it("calls onChange when a different tab is chosen", async () => { it("calls onChange when a different tab is chosen", async () => {

View file

@ -3,7 +3,7 @@ import { TbLayoutSidebarRightCollapse } from "react-icons/tb";
import { ClearButton } from "../../elements/ClearButton/ClearButton"; import { ClearButton } from "../../elements/ClearButton/ClearButton";
export interface MonitoringControlsProps { export interface MonitoringControlsProps {
/** The monitor screens currently available to pin (e.g. Logs/History/Network). */ /** The monitor screens currently available to pin (e.g. Logs/Protocol/Network). */
tabs: string[]; tabs: string[];
/** The active monitor tab shown in the column below. */ /** The active monitor tab shown in the column below. */
value: string; value: string;

View file

@ -9,7 +9,7 @@ const demoScreens = {
<Text>Log stream</Text> <Text>Log stream</Text>
</Box> </Box>
), ),
History: ( Protocol: (
<Box p="md"> <Box p="md">
<Text>Request history</Text> <Text>Request history</Text>
</Box> </Box>
@ -32,7 +32,7 @@ const meta: Meta<typeof MonitoringScreen> = {
), ),
], ],
args: { args: {
tabs: ["Logs", "History", "Network"], tabs: ["Logs", "Protocol", "Network"],
value: "Logs", value: "Logs",
onChange: fn(), onChange: fn(),
searchValue: "", searchValue: "",
@ -52,8 +52,8 @@ export const Logs: Story = {
}, },
}; };
export const History: Story = { export const Protocol: Story = {
args: { value: "History" }, args: { value: "Protocol" },
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(canvas.getByText("Request history")).toBeInTheDocument(); expect(canvas.getByText("Request history")).toBeInTheDocument();

View file

@ -6,12 +6,12 @@ import {
type MonitoringScreenProps, type MonitoringScreenProps,
} from "./MonitoringScreen"; } from "./MonitoringScreen";
const TABS = ["Logs", "History", "Network"]; const TABS = ["Logs", "Protocol", "Network"];
function screens() { function screens() {
return { return {
Logs: <div>logs-body</div>, Logs: <div>logs-body</div>,
History: <div>history-body</div>, Protocol: <div>history-body</div>,
Network: <div>network-body</div>, Network: <div>network-body</div>,
}; };
} }
@ -33,7 +33,7 @@ function renderScreen(overrides: Partial<MonitoringScreenProps> = {}) {
describe("MonitoringScreen", () => { describe("MonitoringScreen", () => {
it("renders the screen for the active tab", () => { it("renders the screen for the active tab", () => {
renderScreen({ value: "History" }); renderScreen({ value: "Protocol" });
expect(screen.getByText("history-body")).toBeInTheDocument(); expect(screen.getByText("history-body")).toBeInTheDocument();
expect(screen.queryByText("logs-body")).toBeNull(); expect(screen.queryByText("logs-body")).toBeNull();
expect(screen.queryByText("network-body")).toBeNull(); expect(screen.queryByText("network-body")).toBeNull();

View file

@ -3,7 +3,7 @@ import { Divider, Stack } from "@mantine/core";
import { MonitoringControls } from "../MonitoringControls/MonitoringControls"; import { MonitoringControls } from "../MonitoringControls/MonitoringControls";
export interface MonitoringScreenProps { export interface MonitoringScreenProps {
/** Available monitor tabs (Logs/History/Network, filtered by capability). */ /** Available monitor tabs (Logs/Protocol/Network, filtered by capability). */
tabs: string[]; tabs: string[];
/** Active monitor tab; keys into `screens` to pick what renders below. */ /** Active monitor tab; keys into `screens` to pick what renders below. */
value: string; value: string;

View file

@ -58,7 +58,7 @@ const UrlScroll = Text.withProps({
variant: "nowrap", variant: "nowrap",
}); });
// Left / right clusters for a compact header line (mirrors HistoryEntry). // Left / right clusters for a compact header line (mirrors ProtocolEntry).
const HeaderCluster = Group.withProps({ const HeaderCluster = Group.withProps({
gap: "sm", gap: "sm",
wrap: "nowrap", wrap: "nowrap",
@ -240,7 +240,7 @@ export function NetworkEntry({
// The list-level Expand/Collapse toggle is authoritative: each time the // The list-level Expand/Collapse toggle is authoritative: each time the
// parent changes `isListExpanded`, every entry snaps to that state and // parent changes `isListExpanded`, every entry snaps to that state and
// any per-entry override is intentionally discarded. Mirrors // any per-entry override is intentionally discarded. Mirrors
// HistoryEntry; do not change without aligning both. This re-runs on // ProtocolEntry; do not change without aligning both. This re-runs on
// re-render when `isListExpanded` keeps its reference, but the setter // re-render when `isListExpanded` keeps its reference, but the setter
// is a no-op when the next value equals the current one. // is a no-op when the next value equals the current one.
useEffect(() => { useEffect(() => {

Some files were not shown because too many files have changed in this diff Show more