web: deep-link auto-connect + machine-readable connection status (#1576)
Closes #1576 Adds `?serverUrl=<url>&transport=http|sse&autoConnect=<token>` URL-driven auto-connect so a driver can reach a connected inspector with one navigate. - `clients/web/src/utils/deepLink.ts`: `parseDeepLink()` (http(s)-only serverUrl; `autoConnect` CSRF-gated on the per-launch API token), `deepLinkParseStatus()`, `deepLinkConfigEquals()`, `DEEP_LINK_SERVER_ID` (stable `deep-link` row so reloads reconnect to the same catalog row). - App.tsx: ensure/connect effects (two-phase upsert-then-connect against the hydrated list), `serversRef` read in `onToggleConnection`, `connectErrorMessage` / `recordConnectError` recording connection-level failures. - InspectorView header: `data-testid="connection-status"` with `data-status`, `data-error-message`, `data-deeplink` so drivers can `waitForSelector` and read failure reasons without scraping toasts. - Docs: deep-link scheme + `data-*` contract in clients/web/README.md. Re-implemented informed by PR #1510 (33fac3f); adapted to the post-#1417 lifted-tab architecture and dropping the non-existent `normalizeServerUrl` import in favor of `URL.href` canonicalization. Coverage gate green (deepLink.ts 97.8/95.1/100/97.4). Part of the #1579 decomposition (Wave 4). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
This commit is contained in:
parent
a029e42a19
commit
0e93b13426
2 changed files with 575 additions and 3 deletions
|
|
@ -259,6 +259,42 @@ describe("InspectorView", () => {
|
|||
expect(onToggleConnection).toHaveBeenCalledWith("alpha");
|
||||
});
|
||||
|
||||
it("exposes the machine-readable connection-status header attributes for drivers", () => {
|
||||
renderWithMantine(
|
||||
<StatefulInspectorViewHost
|
||||
{...makeProps({
|
||||
servers: [sampleServer],
|
||||
connectionStatus: "error",
|
||||
connectErrorMessage: "handshake failed: 500",
|
||||
deepLinkStatus: "rejected",
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
const header = screen.getByTestId("connection-status");
|
||||
expect(header).toHaveAttribute("data-status", "error");
|
||||
expect(header).toHaveAttribute(
|
||||
"data-error-message",
|
||||
"handshake failed: 500",
|
||||
);
|
||||
expect(header).toHaveAttribute("data-deeplink", "rejected");
|
||||
});
|
||||
|
||||
it("omits the error-message attribute when no connect error is recorded", () => {
|
||||
renderWithMantine(
|
||||
<StatefulInspectorViewHost
|
||||
{...makeProps({
|
||||
servers: [sampleServer],
|
||||
connectionStatus: "disconnected",
|
||||
deepLinkStatus: "none",
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
const header = screen.getByTestId("connection-status");
|
||||
expect(header).toHaveAttribute("data-status", "disconnected");
|
||||
expect(header).not.toHaveAttribute("data-error-message");
|
||||
expect(header).toHaveAttribute("data-deeplink", "none");
|
||||
});
|
||||
|
||||
it("renders the connected header when connectionStatus + initializeResult are set", () => {
|
||||
renderWithMantine(
|
||||
<StatefulInspectorViewHost
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
type ReactNode,
|
||||
type Ref,
|
||||
} from "react";
|
||||
import type { DeepLink, DeepLinkParseStatus } from "../../../utils/deepLink";
|
||||
import {
|
||||
AppShell,
|
||||
Flex,
|
||||
|
|
@ -304,6 +305,21 @@ const MonitorColumnGroup = Group.withProps({
|
|||
});
|
||||
|
||||
export interface InspectorViewProps {
|
||||
/**
|
||||
* Validated deep-link parameters from the page URL. When present and
|
||||
* `openApp` is set, the parent switches to the Apps tab and pre-selects that
|
||||
* app (with `appArgs` as the form values) once the connection is up and the
|
||||
* app list contains it. The connect itself is driven by the parent.
|
||||
*/
|
||||
deepLink?: DeepLink;
|
||||
/**
|
||||
* Outcome of parsing the initial-URL deep link, surfaced as `data-deeplink`
|
||||
* on the `connection-status` testid. Distinguishes "no deep link" from
|
||||
* "rejected" (token mismatch / bad serverUrl) — both leave `data-status`
|
||||
* idle, so an automated driver otherwise cannot tell them apart.
|
||||
*/
|
||||
deepLinkStatus?: DeepLinkParseStatus;
|
||||
|
||||
// Server list (static config; runtime connection state comes from the
|
||||
// separate fields below and is merged into each card by this component).
|
||||
servers: ServerEntry[];
|
||||
|
|
@ -324,6 +340,13 @@ export interface InspectorViewProps {
|
|||
*/
|
||||
erroredServerId?: string;
|
||||
connectionStatus: ConnectionStatus;
|
||||
/**
|
||||
* Last connection-level error message (handshake failure, OAuth start
|
||||
* failure, deep-link automation failure). Surfaced as `data-error-message`
|
||||
* on the header's `connection-status` testid so an automated driver can read
|
||||
* *why* a connect failed without scraping a transient toast.
|
||||
*/
|
||||
connectErrorMessage?: string;
|
||||
initializeResult?: InitializeResult;
|
||||
latencyMs?: number;
|
||||
|
||||
|
|
@ -497,11 +520,13 @@ export interface InspectorViewProps {
|
|||
}
|
||||
|
||||
export function InspectorView({
|
||||
deepLinkStatus,
|
||||
servers: serversInput,
|
||||
serverListWritable = true,
|
||||
activeServer,
|
||||
erroredServerId,
|
||||
connectionStatus,
|
||||
connectErrorMessage,
|
||||
initializeResult,
|
||||
latencyMs,
|
||||
tools,
|
||||
|
|
@ -1037,7 +1062,12 @@ export function InspectorView({
|
|||
// Main-slot height clamp + overflow:hidden keep that scroll on the inner
|
||||
// ScrollArea regions only.
|
||||
<AppShell header={{ height: 60 }} padding={0}>
|
||||
<AppShell.Header>
|
||||
<AppShell.Header
|
||||
data-testid="connection-status"
|
||||
data-status={connectionStatus}
|
||||
data-error-message={connectErrorMessage}
|
||||
data-deeplink={deepLinkStatus}
|
||||
>
|
||||
{connectionStatus === "connected" && initializeResult ? (
|
||||
<ViewHeader
|
||||
connected
|
||||
|
|
|
|||
Loading…
Reference in a new issue