feat(react): React introspection, Web Vitals, and SPA primitives (#1257)

* feat(react): first-class React introspection, Web Vitals, and nextjs skill

Add React-general and web-universal features as first-class agent-browser verbs
(react tree/inspect/renders/suspense, vitals, pushstate). Genuinely Next.js-specific
workflows (PPR cookie protocol, /_next/mcp bridge, dev-server endpoints) ship as
a new `nextjs` skill that composes the primitives. No new runtime dependencies -
the React DevTools installHook.js is vendored (MIT) and include_str!'d into the
binary.

New commands:
  react tree                  Full React component tree (depth id parent name)
  react inspect <fiberId>     Props, hooks, state, source for one fiber
  react renders start|stop    Fiber profiler with Insts/Mounts/Re-renders/Self/DOM
                              + prev->next change details
  react suspense              Suspense boundaries + classifier (client-hook,
                              request-api, server-fetch, cache, stream, framework)
                              + root-cause grouping + recommendations
  vitals [url]                LCP/CLS/TTFB/FCP/INP + React hydration phases
  pushstate <url>             Generic SPA client-side navigation
  removeinitscript <id>       Remove a script registered via addinitscript

New launch flags:
  --init-script <path>        Register init scripts before first navigation
                              (repeatable; env AGENT_BROWSER_INIT_SCRIPTS)
  --enable <feature>          Built-in init scripts; currently react-devtools
                              (repeatable; env AGENT_BROWSER_ENABLE)

Other primitives:
  network route ... --resource-type <csv>  Filter by CDP resource type
  cookies set --curl <file>                Auto-detects JSON/cURL/Cookie-header

* fixes

* fixes

* fixes
This commit is contained in:
Chris Tate
2026-04-20 16:12:47 -05:00
committed by GitHub
parent cff12598bf
commit 57405f9361
22 changed files with 3438 additions and 18 deletions
+81
View File
@@ -60,6 +60,8 @@ pub struct Config {
pub session_name: Option<String>,
pub executable_path: Option<String>,
pub extensions: Option<Vec<String>>,
pub init_scripts: Option<Vec<String>>,
pub enable: Option<Vec<String>>,
pub profile: Option<String>,
pub state: Option<String>,
pub proxy: Option<String>,
@@ -107,6 +109,20 @@ impl Config {
}
(a, b) => b.or(a),
},
init_scripts: match (self.init_scripts, other.init_scripts) {
(Some(mut a), Some(b)) => {
a.extend(b);
Some(a)
}
(a, b) => b.or(a),
},
enable: match (self.enable, other.enable) {
(Some(mut a), Some(b)) => {
a.extend(b);
Some(a)
}
(a, b) => b.or(a),
},
profile: other.profile.or(self.profile),
state: other.state.or(self.state),
proxy: other.proxy.or(self.proxy),
@@ -200,6 +216,8 @@ fn extract_config_path(args: &[String]) -> Option<Option<String>> {
"--executable-path",
"--cdp",
"--extension",
"--init-script",
"--enable",
"--profile",
"--state",
"--proxy",
@@ -277,6 +295,8 @@ pub struct Flags {
pub executable_path: Option<String>,
pub cdp: Option<String>,
pub extensions: Vec<String>,
pub init_scripts: Vec<String>,
pub enable: Vec<String>,
pub profile: Option<String>,
pub state: Option<String>,
pub proxy: Option<String>,
@@ -313,6 +333,8 @@ pub struct Flags {
// (as opposed to being set only via environment variables)
pub cli_executable_path: bool,
pub cli_extensions: bool,
pub cli_init_scripts: bool,
pub cli_enable: bool,
pub cli_profile: bool,
pub cli_state: bool,
pub cli_args: bool,
@@ -347,6 +369,38 @@ pub fn parse_flags(args: &[String]) -> Flags {
config.extensions.unwrap_or_default()
};
let init_scripts_env = env::var("AGENT_BROWSER_INIT_SCRIPTS")
.ok()
.map(|s| {
s.split(',')
.map(|p| p.trim().to_string())
.filter(|p| !p.is_empty())
.collect::<Vec<_>>()
})
.unwrap_or_default();
let init_scripts = if !init_scripts_env.is_empty() {
init_scripts_env
} else {
config.init_scripts.unwrap_or_default()
};
let enable_env = env::var("AGENT_BROWSER_ENABLE")
.ok()
.map(|s| {
s.split(',')
.map(|p| p.trim().to_string())
.filter(|p| !p.is_empty())
.collect::<Vec<_>>()
})
.unwrap_or_default();
let enable = if !enable_env.is_empty() {
enable_env
} else {
config.enable.unwrap_or_default()
};
let mut flags = Flags {
json: env_var_is_truthy("AGENT_BROWSER_JSON") || config.json.unwrap_or(false),
headed: env_var_is_truthy("AGENT_BROWSER_HEADED") || config.headed.unwrap_or(false),
@@ -361,6 +415,8 @@ pub fn parse_flags(args: &[String]) -> Flags {
.or(config.executable_path),
cdp: config.cdp,
extensions,
init_scripts,
enable,
profile: env::var("AGENT_BROWSER_PROFILE").ok().or(config.profile),
state: env::var("AGENT_BROWSER_STATE").ok().or(config.state),
proxy: env::var("AGENT_BROWSER_PROXY")
@@ -449,6 +505,8 @@ pub fn parse_flags(args: &[String]) -> Flags {
quiet: false,
cli_executable_path: false,
cli_extensions: false,
cli_init_scripts: false,
cli_enable: false,
cli_profile: false,
cli_state: false,
cli_args: false,
@@ -525,6 +583,27 @@ pub fn parse_flags(args: &[String]) -> Flags {
i += 1;
}
}
"--init-script" => {
if let Some(s) = args.get(i + 1) {
flags.init_scripts.push(s.clone());
flags.cli_init_scripts = true;
i += 1;
}
}
"--enable" => {
if let Some(s) = args.get(i + 1) {
// Allow either repeated --enable foo --enable bar, or
// a single --enable foo,bar comma-list for convenience.
for item in s.split(',') {
let trimmed = item.trim();
if !trimmed.is_empty() {
flags.enable.push(trimmed.to_string());
}
}
flags.cli_enable = true;
i += 1;
}
}
"--cdp" => {
if let Some(s) = args.get(i + 1) {
flags.cdp = Some(s.clone());
@@ -783,6 +862,8 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
"--executable-path",
"--cdp",
"--extension",
"--init-script",
"--enable",
"--profile",
"--state",
"--proxy",