* refactor(web): sharpen src/lib vs src/utils rule and move misfiled modules (#1776)
Codify the empirical rule the web client already followed loosely — utils =
pure functions that compute; lib = things that instantiate, adapt, or touch
the environment — in AGENTS.md and clients/web/README.md, and move the modules
that sat on the wrong side of it.
Moves (source + co-located test):
- lib/sandbox-csp -> utils (pure CSP-string construction; type-only import)
- utils/browserTabVisibility -> lib (registers visibilitychange listeners)
- utils/oauthResume -> lib (reads/writes sessionStorage)
- utils/clearServerOAuthState -> lib (drives live InspectorClient / OAuthStorage)
Borderline audits (kept in place): pendingReauth (pure type declarations),
deepLink / correlateTransportErrors / mcpNetworkHeaders / oauthUx / oauthFlow /
oauthNetworkPhase (pure transforms / type-only core imports) stay in utils;
downloadFile stays whole in lib (dominant side is DOM side effects).
Also relocate the lone src/test/lib/downloadFile.test.ts straggler beside its
source at src/lib/downloadFile.test.ts, and document the test-placement rule
(side-by-side by default; src/test/ only for core/ tests, the integration
project, and shared scaffolding).
Pure move — no behavior change; no build/coverage config keys off the boundary.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* refactor(web): address review — fix stale paths, move customHeaders, one-way arrows (#1776)
Responds to the @claude review on PR #1784:
1. Stale path references (fixed): update the four in-repo pointers left at
pre-move paths — specification/v2_auth_mid_session.md (oauthResume,
browserTabVisibility → lib/), and the two load-bearing sandbox security
comments (sandbox_proxy.html, sandbox-controller.ts → src/utils/sandbox-csp).
2. lib/types/customHeaders (moved): it's pure (types + pure transforms), the
rule's clearest counterexample. Moved lib/types/customHeaders.{ts,test.ts} →
utils/ (no external importers; only its own test) and removed the
undocumented lib/types/ subdir. Documented that utils/ is the home for pure
shared domain types + their constructors — no separate types/ bucket.
3. One-way arrows (item 3): declare OAuthResumeAuthKind in utils/pendingReauth
and re-export it from lib/oauthResume, so the only cross-directory arrow now
points lib → utils. Codified that invariant in AGENTS.md.
Wording nit: AGENTS.md + README now state diagnostic logging
(console.warn/error) doesn't count as a side effect for the utils rule.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* docs: address review round 2 — stale customHeaders refs, rule wording (#1776)
Doc-only follow-up to the round-2 @claude review on PR #1784 (no code changed):
1. Stale paths (same class as round-1 item 1): the customHeaders move left
three references to the deleted lib/types/customHeaders.ts path in
specification/ (v2_ux_interfaces_phase_0_4_inventory.md,
v2_ux_interfaces_plan.md ×2) — updated to utils/customHeaders.ts.
2. customHeaders anchor: note in AGENTS.md that its shape is staged for
ServerSettingsForm (per v2_ux_interfaces_plan.md), so its zero-importer
state reads as intentional rather than rot; kept as the no-types/-bucket
illustration and led the anchor list with load-bearing modules.
3. Type-only-import clause: split the conflated clause — a type-only import
(pendingReauth) and re-exporting core's pure functions/constants
(oauthUx/oauthFlow) are distinct reasons to stay in utils; what makes a
module lib is wrapping core's stateful runtime. Fixed in both AGENTS.md
and the web README.
Cosmetic: re-padded the v2_auth_mid_session.md table row to column width.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* docs: address review round 3 — scope the "no types/ bucket" claim (#1776)
Doc-only follow-up to the round-3 @claude review on PR #1784 (no code changed):
1. "no separate types/ bucket" overreached: clients/web/src/types/ does exist
(navigation.ts + react-syntax-highlighter .d.ts ambient stubs wired via
tsconfig.app.json paths). Scoped the claim in both AGENTS.md and the web
README to "no types/ sub-bucket INSIDE lib/utils", and noted src/types/ is
for ambient .d.ts stubs, not domain types.
2. README example list drifted from AGENTS.md's round-2 load-bearing anchors —
realigned it (inspectorTabs/mcpNetworkHeaders, dropped the zero-importer
customHeaders from the headline list).
3. Clarified v2_ux_interfaces_plan.md:185 so the v2 path reads as the copy's
destination, not as the v1.5 source location.
Follow-up filed as #1785 (board Todo): src/types/navigation.ts InspectorTab is
dead and duplicates utils/inspectorTabs.ts — kept out of this pure-move PR.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* docs: address review round 4 — reconcile src/types/ claim with #1785 (#1776)
Doc-only follow-up to the round-4 @claude review on PR #1784 (no code changed):
The round-3 scope fix overshot: it asserted src/types/ holds "not domain types
at all", but the dead navigation.ts InspectorTab is a plain-.ts domain type
still living there, and specification/v2_ux_interfaces_plan.md:181-182 actively
prescribes placing InspectorTab there — so AGENTS.md and that spec doc gave
opposite guidance about the same directory.
- AGENTS.md + web README: softened to "not the place for new domain types"
and added a parenthetical that the one lingering domain type (dead
navigation.ts) is tracked for removal in #1785; until then a pure domain
type belongs in utils/, not src/types/.
- v2_ux_interfaces_plan.md:181: marked the tab-enum placement guidance as
superseded by #1785 (live type is InspectorTabId in utils/inspectorTabs.ts).
Self-heals on merge of #1785.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* docs: address review round 5 — fix garbled #1785 clause, restructure utils rule (#1776)
Doc-only follow-up to the round-5 @claude review on PR #1784 (no code changed):
1. Grammar (item 1): the round-4 #1785 clause read "superseding [#1785] will
remove it" — a participle with no subject. Reworded to "the dead
navigation.ts InspectorTab, which #1785 will remove".
2. Readability (item 2): the src/utils/ bullet had accreted into a single
~1,400-char blob across five review rounds. Promoted the carve-outs to
scannable sub-bullets (Domain types / Diagnostic logging / Importing from
core) and lifted the src/types/ note out to its own line, since it's a
statement about a sibling directory, not about utils.
AGENTS.md only; the README's parallel sentence was already clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* docs+config: address review round 6 — correct the coverage-glob claim (#1776)
Responds to the round-6 @claude review on PR #1784:
1. "no build/coverage glob" was factually wrong (should-fix): the web coverage
include in clients/web/vite.config.ts DOES enumerate src/lib/** and
src/utils/**. The conclusion (a move between them is coverage-neutral)
survives, but the reason was wrong. Reworded both AGENTS.md and the web
README to say nothing *enforces* the boundary — no path alias, and the
include lists both dirs so a move is coverage-neutral — plus the important
whitelist caveat: a module outside components/lib/utils/server falls out of
the ≥90 gate entirely.
2. Removed a pre-existing duplicate glob in vite.config.ts (src/lib/**/*.{ts,tsx}
appeared twice, lines 161 and 163). Inert (vitest de-dupes) but it sat three
lines from the config the doc now describes.
Nits: gave the README bullet the same round-5 sub-bullet restructure as
AGENTS.md, and made its #1785 reference a real markdown link (bare #N doesn't
autolink in rendered repo markdown).
npm run ci green (config change re-verified through coverage + smokes + Storybook).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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
Extend the browser download library beyond JSON so the Apps host's
ui/download-file support can download arbitrary embedded resources and
open http(s) links safely, with no UI wiring in this change:
- downloadBlob(): temp-anchor download core with a setTimeout(...,0)-
deferred revokeObjectURL so a synchronous revoke can't abort the
scheduled download (Firefox/Safari, intermittently Chrome).
- downloadJsonFile(): refactored on top of downloadBlob; callers unchanged.
- fileNameFromUri(): last path segment sanitized (control/format chars
stripped, disallowed filename chars -> _, capped at 255, "download"
fallback).
- isHttpUrl(): parse-and-allowlist http(s) only, else null.
Re-implementation of the downloadFile.ts slice of PR #1510 as Wave 1 of
the #1579 decomposition.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
* feat(export): wire History + Logs export JSON handlers (#1354)
Replaces the `todoNoop` placeholders in App.tsx with real handlers that
serialize the current entries and trigger a browser download via the
existing `downloadJsonFile` helper.
- Adds `buildExportFilename(kind, serverId, now?)` next to the download
helper. Produces sortable, filesystem-safe names like
`inspector-history-alpha-2026-03-17T10-00-42.123Z.json` (URL-encodes
server ids that contain slashes / spaces / colons, swaps `:` for `-`
in the ISO timestamp so the file is safe on Windows).
- Wires `onExportHistory` (full MessageEntry list) and `onExportLogs`
(the LogEntryData list the screen renders).
- Routes the existing Network export through the same filename helper
for consistency.
Unit tests cover the filename builder, including the no-server-id
fallback, special-character encoding, and the default-`now` shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(export): tighten buildExportFilename signature + docs
- Introduce an `ExportKind = "history" | "logs" | "network"` union and
use it as the type of the `kind` parameter. Typos like "hisotry" now
fail at compile time and the supported export kinds are documented
inline.
- Clarify the JSDoc: the segment is omitted when `serverId` is falsy
(undefined OR empty string), matching the existing `serverId ? ... :
undefined` check. New test pins the empty-string branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(logging): remove Copy All button from LogStreamPanel
Export now produces the same JSON the Copy All button used to dump to
the clipboard, so the button is redundant. History and Network panels
have no equivalent — dropping Copy All also brings Logs into parity.
Removes the prop from LogStreamPanel + LoggingScreen + InspectorView +
App.tsx, and updates the matching tests/stories. Tests now assert the
button is *not* in the document so a future re-introduction would have
to update the assertion deliberately.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(servers): export server list as mcp.json download (#1346)
Adds an Export button to the Servers screen, next to "Add Servers".
Clicking it downloads the current server list as a canonical mcp.json
file (2-space indent, same shape serializeStore writes), so the user
can hand it off, sync it to another machine, or symlink into Claude
Desktop / Cursor / Cline.
Wiring:
- ServerListControls: new Export Button (variant=default), disabled
when serverCount === 0 (nothing to download).
- ServerListScreen / InspectorView: thread `onExport` / `onServerExport`
through.
- App.tsx: onServerExport runs serverEntriesToMcpConfig on the in-memory
servers list, JSON.stringify with 2-space indent, and triggers a Blob
download via a temporary anchor (appended to body for Firefox
compatibility, then removed; object URL revoked).
Tests:
- ServerListControls unit tests cover button-count (toggle hidden vs
shown, Export always present), disabled-on-empty, click fires onExport.
- ServerListControls.stories grows play functions for both stories —
WithServers asserts Export is enabled + click fires the spy in real
Chromium, WithoutServers asserts Export is disabled.
- Existing screen + view tests / stories updated for the new prop.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(servers): address PR #1351 review (extract helpers + exception safety + guard)
Substantive items from the review:
1. Serialization format parity (review #1): added
`serializeMcpConfig(entries)` to core/mcp/serverList.ts — a
browser-safe helper that runs `serverEntriesToMcpConfig` then
stringifies with 2-space indent. Single source of truth for export
formatting; can't drift from serializeStore's on-disk shape.
2. Exception safety on the anchor cleanup (review #2) + a direct test
for the App.tsx download wiring (review #6): extracted
`downloadJsonFile(filename, json)` to clients/web/src/lib/
downloadFile.ts. removeChild + revokeObjectURL run in a `finally`
so a thrown click() doesn't leak the DOM node or the object URL.
Unit-tested under happy-dom with URL.createObjectURL mocked
(happy-dom doesn't ship it).
3. Empty-list guard (review #5): App.tsx onServerExport bails when
`servers.length === 0`. The button's disabled prop already prevents
the UI path, but the handler is now locally correct against any
future programmatic caller (storybook, keyboard shortcut, etc.).
Acknowledged but not changed:
- Tooltip on the disabled Export button (review #3) — Mantine's
disabled-button + Tooltip wart wants a span wrapper + custom events
config. Optional per the reviewer; deferred.
- Repeated exports → `mcp (1).json` browser suffix (review #4) — a
deliberate trade-off since `mcp.json` is the conventional name a user
would expect to symlink into Claude Desktop / Cursor / etc.
Tests: 1647 unit (+6) + 410 integration + 306 storybook = 2363 green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>