Skip to content
21 changes: 21 additions & 0 deletions docs/browser-modes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Browser Modes

React-Sentinel supports three browser modes. Choose the one that matches the kind of runtime evidence you need.

| Mode | Best for | User state | Consent | Main tradeoff |
| --- | --- | --- | --- | --- |
| **User Chrome attach** | Inspecting a real tab that already contains the user's authenticated or manually prepared state | Reuses the user's existing browser profile and selected tab | **Required** via `select_attach_tab` with `confirm: true` | Highest power, but depends on an external Chrome CDP endpoint |
| **Managed Chrome** | Reducing CDP setup friction while keeping a visible isolated browser that React-Sentinel can drive | Uses a temporary isolated profile created by React-Sentinel | Not needed for the managed profile itself | Easier than manual CDP, but still separate from the user's personal Chrome session |
| **Replay sandbox** | Deterministic reproduction, assertions, and runtime patch validation in an isolated environment | Fresh isolated Playwright context | Not needed | Lowest friction, but it does not reuse existing user session state |

## Recommended order

1. Use **user Chrome attach** when the bug depends on a real logged-in or user-prepared tab.
2. Use **managed Chrome** when attach mode is useful but the user does not want to launch Chrome with `--remote-debugging-port` manually.
3. Use **replay sandbox** when you only need deterministic reproduction, assertions, or patch verification.

## Safety notes

- **User Chrome attach** is explicit and consent-based because React-Sentinel can inspect and interact with the selected live tab.
- **Managed Chrome** uses a temporary profile directory so it does not silently reuse the user's personal browser data.
- **Replay sandbox** is the safest default when no live state is required.
8 changes: 8 additions & 0 deletions docs/local-diagnostics-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ Fix:
google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp/react-sentinel-cdp
```

Or let React-Sentinel launch an isolated managed Chromium for you:

```bash
react-sentinel mcp --browser-mode managed --headed
```

Then retry:

1. `get_attach_status`
Expand All @@ -36,6 +42,8 @@ Then retry:

If you do not need the live browser, skip attach mode and stay in replay mode with `browser_ping` or `navigate_replay`.

See [browser-modes.md](browser-modes.md) for the differences between user Chrome attach, managed Chrome, and replay sandbox.

## 3. If no live browser tab is selected

Symptoms:
Expand Down
20 changes: 20 additions & 0 deletions docs/tool-selection-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# MCP Tool Selection Guide

Use React-Sentinel when the answer depends on **runtime evidence in the browser**, not just source code structure.

| If you need to... | Start with | Why this beats grep/read | Good follow-up |
| --- | --- | --- | --- |
| Triage a vague runtime bug fast | `diagnose_runtime_bug` | It correlates console, hydration, async, and render signals into one verdict-first answer. | `attribute_render`, `get_runtime_timeline` |
| Explain why a component rerendered | `diagnose_excess_renders`, `attribute_render` | It inspects live render churn, hooks, props, and context instead of guessing from component code. | `find_memo_breaks`, `inspect_component` |
| Reproduce a bug in a deterministic browser | `navigate_replay` or `start_debug_replay` | It creates a clean replay session that can be rerun exactly. | `replay_interactions`, `validate_scenario` |
| Validate a user flow or invariant | `validate_scenario` or `validate_user_flow` | It executes real browser actions and returns pass/fail assertions with traces. | `find_race_conditions` |
| Catch intermittent timing bugs | `find_race_conditions` | It perturbs action timing across multiple iterations and shrinks failing flows. | `verify_hypothesis`, `verify_fix` |
| Test a runtime hypothesis before editing code | `verify_hypothesis` or `test_runtime_hypothesis` | It proves or refutes the idea against browser behavior instead of relying on intuition. | `attribute_render` |
| Try a fix without touching repository files | `apply_patch_then_replay`, `patch_and_validate`, `verify_fix`, or `verify_runtime_fix` | It validates an ephemeral runtime patch in the sandbox before a source change exists. | `reset_runtime_patches` |
| Reuse a real logged-in browser tab | `get_attach_status`, `get_attach_tabs`, `select_attach_tab` | It lets React-Sentinel inspect the exact user-prepared session that static analysis cannot recreate. | `get_runtime_status` |

## Quick heuristics

- If the bug depends on **current props, state, context, network, console, or timing**, prefer React-Sentinel.
- If you only need to understand **static source structure**, grep/read is still cheaper.
- Prefer **verdict-first tools** (`diagnose_*`, `attribute_render`, `verify_*`) before low-level atomic tools unless you already know the exact signal you need.
75 changes: 45 additions & 30 deletions scripts/e2e-smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ const expectedTools = [
"reset_runtime_patches",
];

function readVerdictRawData<T>(value: unknown): T {
if (value && typeof value === "object" && "raw_data" in value) {
return (value as { raw_data: T }).raw_data;
}
return value as T;
}

async function main(): Promise<void> {
const managedProcesses: ManagedProcess[] = [];
const checks: string[] = [];
Expand Down Expand Up @@ -265,26 +272,34 @@ async function main(): Promise<void> {
);
checks.push("get_render_counts:ok");

const renderHotspots = expectToolSuccess(
const renderHotspots = readVerdictRawData<{
hotspots: { componentName: string; probableCause: { type: string; summary: string } }[];
}>(expectToolSuccess(
await callTool(client, "get_render_hotspots", {
url: demoUrl,
threshold: 4,
windowMs: 2000,
limit: 10,
}),
"get_render_hotspots"
) as {
hotspots: { componentName: string; probableCause: { type: string; summary: string } }[];
};
));
const infiniteLoopHotspot = renderHotspots.hotspots.find((entry) => entry.componentName === "InfiniteLoopScenario");
const nonUnknownProbableCauses = new Set([
"state_change",
"hook_instability",
"provider_value_recreated",
"context_change",
"prop_diff",
"parent_render",
]);
assert(renderHotspots.hotspots.length >= 1, "get_render_hotspots returned no hotspots.");
assert(
renderHotspots.hotspots.some(
(entry) =>
entry.componentName === "InfiniteLoopScenario" &&
["unstable_state", "unstable_hook_value", "unstable_props", "repeated_effect"].includes(
entry.probableCause.type
)
),
"get_render_hotspots did not flag InfiniteLoopScenario with a probable cause."
Boolean(infiniteLoopHotspot),
"get_render_hotspots did not include InfiniteLoopScenario as a hotspot."
);
assert(
infiniteLoopHotspot ? nonUnknownProbableCauses.has(infiniteLoopHotspot.probableCause.type) : false,
"get_render_hotspots classified InfiniteLoopScenario with an unexpected or unknown probable cause type."
);
Comment on lines +286 to 303
checks.push("get_render_hotspots:ok");

Expand Down Expand Up @@ -364,13 +379,13 @@ async function main(): Promise<void> {
) as { success: boolean };
assert(asyncTraceReplay.success === true, "async trace replay failed.");

const asyncTimeline = expectToolSuccess(
await callTool(client, "get_async_timeline", { url: demoUrl, limit: 10 }),
"get_async_timeline"
) as {
const asyncTimeline = readVerdictRawData<{
events: { phase: string; groupKey: string }[];
summary: { totalRequests: number; invertedGroups: { groupKey: string }[]; slowRequests: { durationMs: number }[] };
};
}>(expectToolSuccess(
await callTool(client, "get_async_timeline", { url: demoUrl, limit: 10 }),
"get_async_timeline"
));
assert(asyncTimeline.summary.totalRequests >= 2, "get_async_timeline reported fewer than two requests.");
assert(
asyncTimeline.events.some((event) => event.phase === "request_start") &&
Expand Down Expand Up @@ -400,20 +415,20 @@ async function main(): Promise<void> {
) as { success: boolean };
assert(raceConditionReplay.success === true, "race condition replay failed.");

const raceDiagnosis = expectToolSuccess(
const raceDiagnosis = readVerdictRawData<{
suspected: boolean;
diagnosis: string;
finalStateText: string | null;
latestIntent: { query: string | null } | null;
finalStateRequest: { query: string | null } | null;
}>(expectToolSuccess(
await callTool(client, "get_race_condition_diagnosis", {
url: demoUrl,
stateSelector: "#race-condition-visible-result",
limit: 10,
}),
"get_race_condition_diagnosis"
) as {
suspected: boolean;
diagnosis: string;
finalStateText: string | null;
latestIntent: { query: string | null } | null;
finalStateRequest: { query: string | null } | null;
};
));
assert(raceDiagnosis.suspected === true, "get_race_condition_diagnosis did not flag the stale overwrite.");
assert(
raceDiagnosis.finalStateText?.toLowerCase().includes("slow") === true,
Expand Down Expand Up @@ -548,13 +563,13 @@ async function main(): Promise<void> {
);
await new Promise((resolve) => setTimeout(resolve, 600));

const hydrationIssues = expectToolSuccess(
await callTool(client, "get_hydration_issues", { url: hydrationDemoUrl, limit: 20 }),
"get_hydration_issues"
) as {
const hydrationIssues = readVerdictRawData<{
issues: { tag: string; kind: string; framework: string; message: string }[];
summary: { total: number };
};
}>(expectToolSuccess(
await callTool(client, "get_hydration_issues", { url: hydrationDemoUrl, limit: 20 }),
"get_hydration_issues"
));
assert(
hydrationIssues.summary.total >= 1,
"get_hydration_issues returned no hydration issue for the mismatch demo."
Expand Down
Loading