Address PR #1221 review round 3 feedback

- InspectorView: simulate the connect flow through "connecting" with a
  randomized 50-500ms handshake delay and an 85% success rate. On success,
  measure observed latency from connect start; on failure, set a plausible
  error message. Connection error message flows through to the active
  ServerCard's connection state. Real wiring (Phase 3) replaces this with
  `useConnection`.
- InspectorView: document that the view owns connection state (input
  `serversInput` connection fields are intentionally ignored), and mark
  `noop` callbacks as Phase-3 stubs.
- ServerStatusIndicator: add `showLabel?: boolean` escape hatch so consumers
  in constrained containers can override the default viewport-based
  visibility.
- ServerListScreen.stories: add `WithActiveServer` story exercising the
  dimming + `inert` behaviour in isolation.
- ViewHeader.stories: rename "Logging" → "Logs" to match `ALL_TABS` in
  InspectorView.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
cliffhall 2026-04-25 19:05:18 -04:00
commit 473bd95826
4 changed files with 93 additions and 21 deletions

View file

@ -6,6 +6,10 @@ export interface ServerStatusIndicatorProps {
status: ConnectionStatus;
latencyMs?: number;
retryCount?: number;
// Override for the default viewport-based label visibility. Useful when
// the indicator renders inside a constrained container (sidebar, card)
// where the global media query doesn't reflect available space.
showLabel?: boolean;
}
const statusColorVar: Record<ConnectionStatus, string> = {
@ -42,8 +46,10 @@ export function ServerStatusIndicator({
status,
latencyMs,
retryCount,
showLabel: showLabelProp,
}: ServerStatusIndicatorProps) {
const showLabel = useMediaQuery("(min-width: 1200px)");
const wideViewport = useMediaQuery("(min-width: 1200px)");
const showLabel = showLabelProp ?? wideViewport;
const label = getLabel(status, latencyMs, retryCount);
return (
<Group gap="xs">

View file

@ -32,7 +32,7 @@ export const Connected: Story = {
"Resources",
"Prompts",
"Tasks",
"Logging",
"Logs",
"History",
],
onTabChange: fn(),

View file

@ -98,3 +98,10 @@ export const MixedStates: Story = {
],
},
};
export const WithActiveServer: Story = {
args: {
servers: [connectedStdioServer, disconnectedStdioServer, failedHttpServer],
activeServer: connectedStdioServer.id,
},
};

View file

@ -40,8 +40,24 @@ const ALL_TABS: string[] = [
"History",
];
// Demo stub: every screen-action callback below resolves to noop. Phase 3
// wiring will replace each with its real `useManaged*` / `useConnection`
// hook call. Anything still pointing here in Phase 3 is unfinished.
const noop = () => undefined;
// Demo stub: simulated handshake delays and a sample of plausible failure
// reasons. Replace with real handshake telemetry once `useConnection` is wired.
const STUB_MIN_DELAY_MS = 50;
const STUB_MAX_DELAY_MS = 500;
const STUB_SUCCESS_RATE = 0.85;
const STUB_ERROR_MESSAGES = [
"Connection refused",
"Handshake timeout",
"Protocol version mismatch",
"Authentication required",
"Server returned invalid response",
];
export interface InspectorViewProps {
servers: ServerEntry[];
tools: Tool[];
@ -78,24 +94,36 @@ export function InspectorView({
const [connectionStatus, setConnectionStatus] =
useState<ConnectionStatus>("disconnected");
const [latencyMs, setLatencyMs] = useState<number | undefined>(undefined);
const [errorMessage, setErrorMessage] = useState<string | undefined>(
undefined,
);
const [activeTab, setActiveTab] = useState<string>(SERVERS_TAB);
const [availableTabs, setAvailableTabs] = useState<string[]>([SERVERS_TAB]);
const [logLevel, setLogLevel] = useState<LoggingLevel>("info");
const [autoScroll, setAutoScroll] = useState<boolean>(true);
// Per-card connection state derives from "is this the active server,
// and what's our current global connection status?". Cards that aren't
// the active server always render as disconnected.
// The view is the single source of truth for connection state. Any
// `connection` field on incoming `serversInput` items is intentionally
// ignored — cards mirror the global `connectionStatus` for the active
// server and render as `disconnected` otherwise.
const servers = useMemo<ServerEntry[]>(
() =>
serversInput.map((s) => ({
...s,
connection:
s.id === activeServer
? { status: connectionStatus }
: { status: "disconnected" },
})),
[serversInput, activeServer, connectionStatus],
serversInput.map((s) => {
if (s.id !== activeServer) {
return { ...s, connection: { status: "disconnected" } };
}
if (connectionStatus === "error" && errorMessage) {
return {
...s,
connection: {
status: "error",
error: { message: errorMessage },
},
};
}
return { ...s, connection: { status: connectionStatus } };
}),
[serversInput, activeServer, connectionStatus, errorMessage],
);
function disconnect() {
@ -103,10 +131,16 @@ export function InspectorView({
setConnectionStatus("disconnected");
setInitializeResult(undefined);
setLatencyMs(undefined);
setErrorMessage(undefined);
setAvailableTabs([SERVERS_TAB]);
setActiveTab(SERVERS_TAB);
}
// Demo stub: simulates the full connect → connecting → connected/error
// transition with a randomized handshake delay. Real wiring will dispatch
// `useConnection.connect(id)` and let the hook drive these state changes.
// The `protocolVersion`, `capabilities`, and `serverInfo` populated below
// are placeholders until a real `InitializeResult` arrives from the server.
function handleToggleConnection(id: string) {
if (id === activeServer && connectionStatus === "connected") {
disconnect();
@ -114,15 +148,40 @@ export function InspectorView({
}
const target = serversInput.find((s) => s.id === id);
if (!target) return;
// Capture `start` at the "connecting" edge and compute observed
// latency at the "connected" edge. Both edges are owned by this
// handler so the timing is deterministic — no useEffect chain needed
// (which would otherwise trip `react-hooks/set-state-in-effect`).
const start = Date.now();
setActiveServer(id);
setConnectionStatus("connected");
setInitializeResult({
protocolVersion: "2025-06-18",
capabilities: {},
serverInfo: target.info ?? { name: target.name, version: "0.0.0" },
});
setLatencyMs(42);
setAvailableTabs(ALL_TABS);
setLatencyMs(undefined);
setErrorMessage(undefined);
setConnectionStatus("connecting");
const delay =
STUB_MIN_DELAY_MS +
Math.floor(Math.random() * (STUB_MAX_DELAY_MS - STUB_MIN_DELAY_MS + 1));
window.setTimeout(() => {
if (Math.random() < STUB_SUCCESS_RATE) {
setInitializeResult({
protocolVersion: "2025-06-18",
capabilities: {},
serverInfo: target.info ?? { name: target.name, version: "0.0.0" },
});
setAvailableTabs(ALL_TABS);
setLatencyMs(Date.now() - start);
setConnectionStatus("connected");
} else {
setErrorMessage(
STUB_ERROR_MESSAGES[
Math.floor(Math.random() * STUB_ERROR_MESSAGES.length)
],
);
setConnectionStatus("error");
}
}, delay);
}
return (