* 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
68 lines
1.8 KiB
Rust
68 lines
1.8 KiB
Rust
//! React component tree snapshot and formatter.
|
|
|
|
use serde::Deserialize;
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct TreeNode {
|
|
pub id: i64,
|
|
#[serde(rename = "type")]
|
|
pub node_type: i64,
|
|
pub name: Option<String>,
|
|
pub key: Option<String>,
|
|
pub parent: i64,
|
|
}
|
|
|
|
const HEADER: &str = "# React component tree\n# Columns: depth id parent name [key=...]\n# Use `react inspect <id>` for props/hooks/state. IDs valid until next navigation.";
|
|
|
|
pub fn format_tree(nodes: &[TreeNode]) -> String {
|
|
use std::collections::HashMap;
|
|
let mut children: HashMap<i64, Vec<&TreeNode>> = HashMap::new();
|
|
for n in nodes {
|
|
children.entry(n.parent).or_default().push(n);
|
|
}
|
|
|
|
let mut lines: Vec<String> = vec![HEADER.to_string()];
|
|
if let Some(roots) = children.get(&0) {
|
|
for root in roots {
|
|
walk(root, 0, &children, &mut lines);
|
|
}
|
|
}
|
|
lines.join("\n")
|
|
}
|
|
|
|
fn walk<'a>(
|
|
node: &'a TreeNode,
|
|
depth: usize,
|
|
children: &std::collections::HashMap<i64, Vec<&'a TreeNode>>,
|
|
lines: &mut Vec<String>,
|
|
) {
|
|
let name = node
|
|
.name
|
|
.clone()
|
|
.unwrap_or_else(|| type_name(node.node_type));
|
|
let key = match &node.key {
|
|
Some(k) => format!(" key={:?}", k),
|
|
None => String::new(),
|
|
};
|
|
let parent = if node.parent == 0 {
|
|
"-".to_string()
|
|
} else {
|
|
node.parent.to_string()
|
|
};
|
|
lines.push(format!("{} {} {} {}{}", depth, node.id, parent, name, key));
|
|
if let Some(cs) = children.get(&node.id) {
|
|
for c in cs {
|
|
walk(c, depth + 1, children, lines);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn type_name(t: i64) -> String {
|
|
match t {
|
|
11 => "Root".to_string(),
|
|
12 => "Suspense".to_string(),
|
|
13 => "SuspenseList".to_string(),
|
|
_ => format!("({})", t),
|
|
}
|
|
}
|