CI failed with an unhandled `ReferenceError: window is not defined` thrown from
a Mantine `Transition` `setTimeout` (`use-transition.mjs`) firing after the test
environment was torn down — every test passed, but the leaked timer failed the
run. happy-dom has no `matchMedia`, so `useReducedMotion` resolves to
"motion allowed" and every Transition (ScreenStage, Modal, …) schedules real
enter/exit timers; a timer that outlives its test throws post-teardown. This
surfaced now because MonitoringScreen started wrapping its screens in
ScreenStage (a Transition) where it previously rendered them directly.
Mock `window.matchMedia` in the test setup to report
`prefers-reduced-motion: reduce`, so Mantine transitions render instantly (no
timers). All other queries return `false`, matching the prior absent-matchMedia
behavior, so `useMediaQuery`-driven layout is unchanged. Full unit suite passes
with no unhandled errors across repeated runs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
Address PR #1641 review feedback:
- Add readInspectorVersionSafe() (non-throwing) in core/node/version.ts and use
it in the web backend, so a failure to resolve the root manifest hides the
(cosmetic) version badge instead of crashing the backend at startup. Covered
by tests for both the resolved and unresolvable paths.
- Update MonitoringScreen's docstrings: it now wraps every supplied screen in a
ScreenStage (only the active tab mounts, cross-fading) rather than rendering
just the active tab's node.
The third fetch-consolidation suggestion (one useInitialConfig instead of three
GET /api/config hooks) is tracked as a follow-up in #1643.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
Make the server-list toolbar's expand/collapse toggle and open/close-monitoring
icons the same width and spacing as the header's theme / client-settings icons:
- ListToggle (default variant), PinColumnButton, and the MonitoringControls
close button now render as `ActionIcon variant="subtle" size={36}` (glyph 20),
matching the header's 36px ActionIcons, instead of wider `Button`s.
- ServerListControls uses `gap="sm"` so its buttons sit the same 12px apart as
the header's RightSection controls.
Update a TaskListPanel test that found the toggle by a brittle
`mantine-Button-root` class to select it by its accessible name instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
When a server is connected, has monitor screens to show, and the monitoring
column isn't open, show an "open monitoring column" button to the right of the
list expand/collapse toggle on the server list. It reuses PinColumnButton (the
same right-sidebar affordance the monitor screens use, now with an optional
label), so opening the column is reachable from the server list — not only from
a monitor screen's pin button. InspectorView gates it on
`canPin && monitorAvailable.length > 0 && !effectivePinned`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
Extract the primary pane's per-screen enter/exit transition into a shared
`ScreenStage` element (the `fade-up` Mantine Transition: incoming screen slides
up + fades in while the outgoing fades down + out, both absolutely positioned).
InspectorView now imports it (behavior unchanged), and the pinned monitoring
column uses it too — via a new `fill` variant that stretches the stage to the
column's height — so switching Logs/Protocol/Network animates the same way as
switching the main screens.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
Display the single-source version (root package.json) in the web UI's
lower-right corner, in grey — matching the CLI and TUI. The browser can't read
the filesystem, so the version flows backend → browser via GET /api/config:
- Add `version?: string` to InitialConfigPayload; the web backend
(webServerConfigToInitialPayload) reads it once via the shared
readInspectorVersion() and tags it onto the payload.
- Add a `useInspectorVersion` hook mirroring useSandboxUrl/useServerListWritable.
- Add a presentational `VersionBadge` element rendered fixed lower-right, grey,
non-interactive; styling lives in a `versionBadge` Text theme variant (no
inline styles, --inspector-* tokens).
- Wire it into App.tsx.
Verified live: /api/config serves `version: "2.0.0"` and the badge renders
`v2.0.0` fixed at the lower-right in grey. Tests: useInspectorVersion (full
branch coverage), VersionBadge (unit + Storybook), web-server-config payload.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
The Inspector ships as one package with one version, but four client
package.jsons each carried their own `version` and were read at runtime
(cli.ts, tui App.tsx), and core hardcoded a third, stale value (0.20.0) that
the TUI and web actually reported to servers. Consolidate onto the root
package.json as the single source of truth.
- Add core/node/version.ts: shared, Node-only readInspectorVersion() that walks
up to the root package.json (always shipped in the tarball). 100% covered by a
new integration test.
- CLI and TUI read the version via the shared reader instead of their own
manifest; TUI now also passes a real clientIdentity (was falling through to
the stale core default).
- Drop the `version` field from all four client package.jsons; the root keeps
the only version. TUI header name/description become local display constants.
- core default client identity: replace the stale 0.20.0 hardcode with a neutral
0.0.0 placeholder (real clients pass clientIdentity; web will source its
version from GET /api/config, see #1639).
- Stop shipping clients/cli/package.json and clients/tui/package.json (no longer
read at runtime) — supersedes the earlier "ship them" fix; update pack:verify
and the root README packaging invariants accordingly.
- Add core/node/** to the web coverage include so the new module is gated.
- Add the shared-code architecture diagram to the root README.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
Long request URLs stretched every Network card to their own max-content
width, bleeding the cards (and their status badges) past the pinned
monitoring column. Mantine's ScrollArea `content` slot defaults to
`min-width: max-content`, so a non-wrapping URL forced the whole list wider
than its viewport.
Add an opt-in `constrainContentWidth` prop to EmbeddableScrollArea that
relaxes that slot to `min-width: 0`, so the list can't grow past its
viewport and each row manages its own overflow — the URL already lives in a
horizontal ScrollArea, which now actually scrolls. Scoped to the Network
stream panel (Logs/Protocol rows wrap or truncate, so they keep the default
where a long line scrolls the list).
Verified in Storybook (embedded 380px column): cards clamp to the viewport,
no list/page horizontal overflow, and the URL scrolls within its own area.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
- Direction labels now both point right so the arrow reads clearly at
badge size: incoming "client ← server" becomes "server → client"
(from → to), in MessageDirectionBadge and MessageDirectionFilter
(shared by the Protocol and Network views).
- The Protocol list's pinned section header now reads "Pinned Messages"
(was "Pinned Requests"), matching the "Messages" panel title.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
Rename the web client's History tab and its supporting code to Protocol
so the feature reads as the MCP "Protocol" view.
- Tab renders as "Protocol"; INSPECTOR_TAB_IDS, InspectorView tab
constants/MonitorTab, TAB_UI_REGISTRY key, and navigation.ts tab id all
updated to Protocol.
- Rename components + folders: HistoryScreen -> ProtocolScreen,
HistoryListPanel -> ProtocolListPanel, HistoryEntry -> ProtocolEntry,
HistoryControls -> ProtocolControls, historyUtils -> protocolUtils
(plus their tests and stories, and exported types).
- Rename App/InspectorView plumbing: historyUi, pinnedHistoryIds,
on*History* handlers/props, replayHistoryRequest,
isReplayableHistoryMethod, sort/compact scopes, etc. -> Protocol names.
- Export filenames use the "protocol" slug (buildExportFilename +
ExportKind).
Preserved (per issue #1623 decisions):
- The in-panel section discriminator keeps its "pinned" | "history"
values, and the sub-sections stay labeled Pinned / History.
- window.history and core message-log/replay concepts are untouched.
npm run ci passes (format, lint, build, unit tests, >=90 per-file
coverage gate, smoke, Storybook).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
Re-review follow-up (#1622):
- Observation A: a returning user whose last-pinned monitor tab was
History would land on the (empty) History tab on a stdio failure instead
of the Console diagnostic. Root cause: History is *always* empty on a
connect failure (the message log clears on the error transition), yet it
was pushed unconditionally, so it could precede/hide the real diagnostic.
Fix: content-gate History like Console/Network — only offer it when it
has entries. On a fresh failure the diagnostic (Console/Network) is the
only tab, so it always leads, without touching tab-switching or adding
state. This also resolves the earlier "opens onto an empty column" note:
when nothing has been captured yet, monitorAvailable is empty and the
column stays closed, opening reactively once stderr/fetch content lands.
Tests: stdio/HTTP failure now assert the diagnostic leads and History is
absent; added "leads with Console even when stored tab was History",
"keeps the column closed until a diagnostic has content", and "offers
History when it has captured content". Verified live (time server →
Console-only column). Full `npm run ci` passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
Review feedback on #1622:
- #1 (medium): `connectionStatus === "error"` also covers a mid-session
crash of a previously-connected server, not just a failed connect
attempt. Scope the auto-open + failure tab set to connect attempts only:
the auto-open effect's error arm now requires the previous status to not
be `connected` (connecting/disconnected → error), and `failed` (render)
requires `erroredServerId`, which the parent sets only for connect-
attempt failures. A mid-session crash now closes the column like any
teardown instead of reopening a user-closed column / dropping the live
Logs tab. This also aligns the column behavior with the red border.
- #3 (minor): extract the column slide duration into a shared
`monitorColumnAnimation.ts` (MONITOR_COLUMN_ANIM_MS +
FAILED_CARD_SCROLL_DELAY_MS) so InspectorView's Transition and
ServerCard's scroll delay can't drift.
- #4 (nit): composite key for ConsoleScreen entry rows instead of the
array index.
Added a mid-session-crash test (connected → error with no erroredServerId
closes the column). Verified live: a connect failure still opens the
column to Console. Full `npm run ci` passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
Opening the monitoring column on a failed connection reflows the server
grid to fewer/narrower columns, which can push the failed card below the
fold. Scroll it into view so the red-bordered card is visible without
hunting for it.
- ServerCard scrolls itself into view on the disconnected→errored
transition (mirrors the freshly-added highlight scroll), deferred
~320ms so the column open + grid reflow settle first — otherwise
scrollIntoView targets the card's pre-reflow position. Guarded by a ref
so it fires once per failure and won't fight a user who scrolls away.
Verified live: the failed `time` card scrolls fully into view after the
column opens. Full `npm run ci` passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
Flags the server whose last connection attempt failed with a red card
border, cleared when another server is connected or a new connection is
attempted — a persistent, at-a-glance companion to the failure toast and
the auto-opened monitoring column.
- App tracks `failedServerId`: set in onToggleConnection's non-auth
handshake catch, cleared at the start of any new connect attempt. It is
deliberately kept out of resetSessionScopedUiState, since the failure's
own `disconnect` event would otherwise clear it the same tick (that same
event clears `activeServer`, which is why the flag is tracked separately).
- New `errored` Card theme variant (red border via a new
`--inspector-error-border` token), mirroring the `highlighted` green
border. Precedence: disabled (dimmed) > errored > highlighted.
- Plumbed `erroredServerId` through InspectorView → ServerListScreen →
ServerCard (`errored` prop).
Full `npm run ci` passes (validate + coverage + smoke + storybook).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
The InspectorView story args predate the new required Console props, so the
"Connection Error" play story crashed rendering (`stderrLogs` undefined →
`.length` throw) — caught by the Storybook step of `npm run ci`, which I
should have run before the previous push.
- Add `stderrLogs` / `consoleUi` / `onConsoleUiChange` / `onClearConsole` /
`onExportConsole` to the story args, with a `demoStderr` fixture.
- Make the Connection Error story a stdio-failure narrative: it now opens
the column to Console showing the captured startup error, with `network:
[]` so Network isn't offered.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
A failed stdio launch had no useful diagnostic surface: the message log
(History) is cleared on the error transition and there are no HTTP
requests (Network), so the auto-opened monitoring column was empty. The
process's stderr — which explains the failure (e.g. "No module named
mcp_server_time") — was captured in StderrLogState but never displayed.
- New ConsoleScreen renders the captured stderr stream (search / sort /
clear / export; embeddable in the monitoring column), wired through App
via the existing useStderrLog hook + StderrLogState.
- Console is a pinnable monitor tab like Logs/History/Network, gated to
stdio servers (HTTP has no child process). It lives in the header menu
when the column isn't pinned, and moves into the column when it is.
- The failure column now picks its tabs by which diagnostic actually
captured content (stderr → Console, fetch requests → Network), not by
the active server's transport: a connect failure fires the client
`disconnect` event, which clears `activeServer`, so transport can't be
read back there. The memo re-runs as stderr/fetch entries stream in, so
the diagnostic tab appears as soon as the failing process prints.
Verified live against a failing `python -m mcp_server_time` stdio server:
the column auto-opens to Console showing the real ModuleNotFoundError.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
On a connection failure the user was left without an obvious way to see
why. Open the monitoring column automatically on the transition into the
`error` status (mirroring the connect-success behavior from #1616) so the
diagnostics surface right when they're needed.
- Broaden the auto-open effect to fire on the transition into `error` as
well as `connected`, tracked via a general previous-status ref so it
fires once per transition and never on a mount that starts errored.
- A failed connect never negotiated capabilities, so the failure column
offers History (the local client-side message log) and — for non-stdio
transports — Network (HTTP requests), the entries that explain the
failure. Logs is omitted (it needs the server `logging` capability).
- `effectivePinned` now allows the column while `error`, not only while
`connected`; it still closes on a clean disconnect or narrowing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
Adds a test that clicks Expand/Collapse in the embedded (compact)
layout, exercising the ExpandToggle onToggle callback that was
uncovered (line 193). Restores HistoryEntry.tsx to 100% coverage,
fixing the CI per-file function-coverage gate failure (was 85.71%).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
Auto-open the monitoring column when a connection is established. Gated on the
disconnected → connected transition (a ref-tracked edge), so it fires on an
actual connect rather than on every render while connected or on a mount that
starts already-connected (which would fight a user who closed it). The column
still only appears when wide and a monitor tab is available.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
At the 320px column minimum the full ISO timestamp ate most of line 1. Use an
HH:MM:SS (UTC) form in the embedded HistoryEntry/NetworkEntry headers only; the
full-size layouts keep the full ISO string.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In the pinned monitoring column (embedded), the single-line entry headers wrap
badly. Add compact two-line variants gated on `embedded`:
- HistoryEntry: line 1 = time + client/server badge + elapsed + status; line 2 =
method (and target, truncated) on the left with the controls on the right,
where Replay becomes an icon (new ReplayButton element) next to Pin/Expand.
- NetworkEntry: line 1 = time + HTTP method + category + elapsed + status; line 2
= the URL in a horizontal ScrollArea (new Text `nowrap` variant) with the
expand toggle on the right.
The full-size (non-embedded) layouts are unchanged. `embedded` is threaded from
the History/Network panels into the entries.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the Client/Server direction badge to the right of the timestamp so the
monospace timestamp is leftmost, matching the NetworkEntry header layout.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Fix the resize handle disappearing: the animation wrapper is a Mantine Group
(default align: center), and the full-height handle has no intrinsic height, so
it collapsed to 0. Set align: stretch on the wrapper.
- Closing the column now leaves the primary on the screen currently selected in
the header (commits the active tab first), instead of swapping in the column's
screen.
- Keep the primary screen's standard ScreenStage transition; only the column
(handle + column) slides open/closed from the right with a fade.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add a search box to MonitoringControls (right of the tabs). It's a single
shared search kept across column tab switches, filtering whichever screen is
shown. Embedded panels apply this text but still ignore the sidebar-only
filters (levels/categories/directions/method), which have no control there.
- Close button now uses the "collapse sidebar" icon (TbLayoutSidebarRightCollapse),
the mirror of the pin button's expand icon.
- Closing the column carries its selection to the primary view: the screen the
user was watching becomes the active header tab (rather than snapping back to
the pre-pin selection).
- Animate the column open/close: the handle + column slide in from the right and
fade (reversing on close) via a Mantine Transition.
- HistoryListPanel: drop the redundant "History (N)" header when there are no
pinned requests (lone unpinned section renders headerless).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ResizeHandle: clear the `resizing-col` body class on unmount so a drag
interrupted by the handle unmounting (narrow past the breakpoint / disconnect)
can't leave page-wide user-select:none / col-resize applied.
- Embedded monitor column now shows the full unfiltered stream instead of
silently mirroring the full-size screen's live filter (the column has no
filter control to explain an empty/partial list).
- MonitoringControls: give the SegmentedControl an aria-label ("Monitoring
screen") so the tab group has an accessible name.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Render PinColumnButton as a subtle size-sm Button with a size-20 icon, the same
style as the panel's ListToggle, instead of a default ActionIcon.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On the History and Network panels, place the ListToggle (expand/collapse)
after Export and before the pin-as-column button. (Logs has no such toggle.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Place the "pin as column" icon after Clear/Export (rightmost) on the Logs,
History, and Network panel toolbars instead of at the front.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
`tsc -b` (run in CI's validate step, but not by vitest locally) rejected
`ok.mock.calls[0][1].body` — under the project's strict indexing the
untyped mock's call tuple is `[]`, so `[0]` is possibly-undefined and
`[1]` is out of range. Capture the request body inside a `vi.fn<typeof
fetch>` implementation instead of indexing into `mock.calls`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend the Node-free guard beyond oauth-persist.ts to every module
reachable from the browser's RemoteOAuthStorage (storage-remote,
oauth-storage, store, oauth-persist, store-serialize), so a forbidden
store-io/node:/atomically import added one hop away is caught too — not
just a direct import in oauth-persist.ts. Addresses the non-blocking
hardening note on Claude review of #1612.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The refactor routed the browser's RemoteOAuthStorage through
`oauth-persist.ts`, which value-imported the Node-only `store-io.js`
(`atomically` -> `stubborn-fs` -> `node:process.getuid`, plus
`node:fs/promises`). That dragged Node built-ins into the browser bundle
and the web app crashed to a blank page at runtime — a regression from
v2/main, where browser OAuth storage used a Zustand JSON adapter with no
Node imports. Unit/integration tests run in node/happy-dom and
`smoke:web` only checks the served HTML, so none of them execute the app
in a real browser and the break slipped through.
Fix, following the existing `store-id.ts` precedent:
- Split the pure JSON (de)serializers into Node-free
`core/storage/store-serialize.ts`; `store-io.ts` re-exports them so
existing importers are unaffected.
- Move the Node-only `createFileOAuthPersistBackend` into
`core/auth/node/oauth-persist-file.ts`; update `storage-node.ts` and
the storage adapters test to import from there.
- `oauth-persist.ts` now imports only the Node-free serializers, so the
browser bundle no longer pulls in `node:fs`/`atomically`.
Tests:
- oauth-persist-browser-safe.test.ts: source-level guard that
`oauth-persist.ts` never imports store-io/node:/atomically.
- Direct unit tests for the isomorphic remote + session backends
(restoring oauth-persist.ts coverage after the file backend moved out).
- store.test.ts: OAuthMemoryStore unit coverage incl. replace() fallbacks.
- storage.test.ts: clearAllOAuthClientState null-snapshot (no file) path.
Verified end-to-end in a real browser (Playwright): DCR OAuth flow
completes, tokens persist via the remote store, and after a full page
reload the server reconnects with no re-authentication.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document the contract the remote read() path leans on — the server's
missing-file response is `c.json({}, 200)`, and the backend now passes
that empty object straight to parseOAuthPersistBlob. Addresses the
optional note on Claude review of #1612.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review follow-ups on the RemoteOAuthStorage web migration:
- useEmaIdpLoginState.logout(): add a .catch to the now-async
clearEmaIdpSession call so a storage failure no longer surfaces as an
unhandled rejection; leave loginState unchanged on failure (the session
was not actually cleared). Add a test asserting no unhandled rejection.
- createOAuthStore: attach a no-op catch to the eager persistLoadPromise
so a hydration read that fails before any load() awaits it does not
become an unhandled rejection; real awaiters still observe the rejection
via the shared reference.
- storage-remote.test: replace `as unknown as typeof fetch` double-casts
with the typed vi.fn<typeof fetch>() pattern the same PR established.
- remoteOAuthStorage: correct the doc comment to describe the cache-of-one
accurately instead of implying a keyed map.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Serialize persist() in OAuthStorageBase so overlapping remote POSTs cannot
reorder and clobber state. Regenerate launcher package-lock.json to drop
stale zustand entries. Let parseOAuthPersistBlob accept parsed objects
from res.json() without a redundant JSON round-trip.
- Add a dedicated `inspectorTabs.test.ts` (was relying only on incidental
coverage from App.test.tsx) — covers the tab-id constants and both branches
of the `isInspectorTabId` type guard; 100% per-file.
- AGENTS.md: extend the `core/auth/` structure-tree entry to describe the new
mid-session recovery modules (challenge parsing, scope union, shared OAuth UX
copy, force-reauthorization) and the node loopback callback flow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DZyeWManYTvcXxjcrhxSQc
- App.tsx: replace the inline `style={{}}` on the re-auth banner wrapper with
Mantine style props (pos/top/bg) plus a `.reauth-banner-bar` class for the
z-index/box-shadow that have no Mantine shorthand.
- App.tsx: extract the ~100-line inline `onAuthorize` and the inline `onCancel`
step-up handlers into named `handleStepUpAuthorize`/`handleStepUpCancel`.
- ReAuthBanner: move the flat `styles={{ root }}` (background/border) into a
`reauth` Alert theme variant; replace the raw `<span>` with `<Text>`.
- StepUpAuthModal: extract the inline `onClick` wrapper into `handleAuthorize`.
No behavior change; validate + integration green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DZyeWManYTvcXxjcrhxSQc
The mid-session auth work passed `validate`/CI but failed the local-only
`npm run test:coverage` per-file gate (≥90% lines/statements/functions/
branches) on 12 files. Add targeted unit/integration tests so every changed
file clears the gate; annotate provably-dead defensive branches with justified
`/* v8 ignore */` comments rather than relaxing the gate.
Files brought to ≥90 on all four dimensions:
- core/auth: mcpAuth, oauthUx, utils, challenge, node/runner-interactive-oauth
- core/mcp: oauthManager, inspectorClient, node/authChallengeFetch,
remote/remoteClientTransport, remote/node/{remote-session,server}
- clients/web/src/utils: oauthResume, browserTabVisibility
`npm run test:coverage` now passes (3575 tests, 0 threshold failures);
`npm run validate` green across all four clients.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DZyeWManYTvcXxjcrhxSQc