Skip to content

Chrome DevTools Protocol server - #572

Open
nicoburns wants to merge 25 commits into
mainfrom
devin/1785390103-cdp-server
Open

nicoburns wants to merge 25 commits into
mainfrom
devin/1785390103-cdp-server

Conversation

@nicoburns

@nicoburns nicoburns commented Jul 30, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds a Chrome DevTools Protocol (CDP) server so Chrome DevTools' Elements panel works against Blitz documents: DOM tree, Styles (matched rules), Computed, Box Model, highlight overlays and the element picker (inspect mode). Read-only: no rule editing, no multi-frame, no JS runtime/console (Runtime/Page/Target and other irrelevant domains are stubbed just enough for the frontend to boot).

Based on main (uses the DevtoolSettings.highlight_node / element_picker fields and paint overlay from #574, and comment contents from #576, both now merged). This branch does not include the Firefox devtools server work (#557/#558); the shell plumbing they share (the DocumentProvider adapter, DevtoolsPoll wake event, and picker input interception incl. swallowing the matching release of a picking click) is included here directly.

New crate packages/blitz-cdp-server:

  • Transport: std::net + blocking tungstenite (no async runtime — nothing here needs one): a listener thread accepts connections; each connection gets a thread serving both HTTP discovery (/json/list, /json/version — one page target per document at /devtools/page/{doc_id}) and the WebSocket CDP endpoint (plain JSON {id, method, params} / results / events, echoing flat-mode sessionId), alternating between reads (short socket read timeout) and draining the outgoing message queue. Messages are queued and processed synchronously on the document-owning thread via process_messages(&mut dyn DocumentProvider).
  • DOM: getDocument, requestChildNodes (+ DOM.setChildNodes events), querySelector, pushNodesByBackendIdsToFrontend, getBoxModel, getOuterHTML. Node ids are NodeId::as_u64() + 1 (CDP node ids must be non-zero); nodeId == backendNodeId. Anonymous layout nodes and whitespace-only text nodes are filtered from the exposed tree. Comment nodes report their contents as nodeValue.
  • CSS: getMatchedStylesForNode (Stylo rule tree via styles.rules.self_and_ancestors(), selector text/origin recovered by mapping declaration-block pointers to rules found by walking the document's stylesheets; inline style attribute reported as inlineStyle; inherited entries per ancestor with inherited-only declarations), getComputedStyleForNode (all longhands via NonCustomPropertyId::iter(), with used post-layout px values substituted for width/height, margin, padding, border-width, and insets of positioned elements), getInlineStylesForNode.
  • Overlay: highlightNode/hideHighlight drive DevtoolSettings.highlight_node (per-fragment overlay for inline elements from Inline fragment rects: highlight inline spans per line box #569); setInspectMode toggles DevtoolSettings.element_picker. Embedder picker input arrives via notify_picker_event() and emits Overlay.nodeHighlightRequested (hover, preceded by the node's ancestor path via DOM.setChildNodes so the frontend reveals it in the tree in realtime) / Overlay.inspectNodeRequested (click), after promotion to the nearest non-anonymous element.
  • Box model geometry uses get_client_bounding_rect() for the border box (bounding rect of line-box fragments for non-atomic inlines, which report zero box insets) and Taffy final_layout() insets otherwise.

blitz-shell integration behind the devtools feature: opt-in via BLITZ_CDP_PORT; the server is woken via a DevtoolsPoll shell event and processes messages synchronously on the event-loop thread through a DocumentProvider adapter over the application's windows. The browser, rdme and dioxus-native crates gain forwarding devtools features (e.g. cargo run -p rdme --features devtools).

Testing

  • packages/blitz-cdp-server/tests/elements_panel.rs: real WebSocket client drives the Elements-panel sequence (discovery → enables → getDocument → requestChildNodes → querySelector → matched/computed/inline styles → getBoxModel incl. wrapped inline span → highlightNode/hideHighlight → setInspectMode + simulated picker hover/pick → pushNodesByBackendIdsToFrontend) asserting on responses and events.
  • cargo fmt, cargo clippy --workspace --features blitz-shell/devtools, cargo test -p blitz-cdp-server pass.
  • Verified end-to-end with real Chrome DevTools (evidence in PR comments), including a smoke test of the blocking transport.

WPT results

No changes in test results compared to main.

Generated by the WPT workflow.

Link to Devin session: https://dioxus.staging.devinenterprise.com/sessions/f46cbcd2db8548f5a54c1fe02a0e7eea
Requested by: @nicoburns

@nicoburns nicoburns self-assigned this Jul 30, 2026
@staging-devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@staging-devin-ai-integration

Copy link
Copy Markdown
Contributor

Tested end-to-end with real Chrome DevTools (Chrome's bundled inspector connected to ws://127.0.0.1:9222/devtools/page/1 from /json/list), against BLITZ_CDP_PORT=9222 cargo run -r --example url --features blitz/devtools -- https://example.com.

All four flows work:

  • Elements panel renders the DOM tree; expanding nodes (DOM.requestChildNodes) reveals children
  • Selecting an element populates Styles (authored + user-agent + inline), Computed, and Box Model margins
  • Hovering tree nodes draws the highlight overlay in the Blitz window (Overlay.highlightNode) and clears on mouse-out
  • Inspect picker: hover highlights in the Blitz window with live DevTools feedback; click selects the node and exits inspect mode

Hover highlight overlay drawn in Blitz window

Styles / Computed / Box Model evidence

Styles pane for h1 with user-agent rules
Computed + box model

Minor observations (non-blocking)
  • Box Model content area shows auto × auto instead of used pixel dims (computed style returns specified values; DOM.getBoxModel geometry is correct).
  • Picking a collapsed node with the picker updates breadcrumbs/Styles/Computed but the tree row selection doesn't move/reveal it; picking visible nodes works fully.
  • Overlay.highlightNode is only exercised with DevTools' screencast pane toggled off (otherwise DevTools self-draws highlights over the screencast) — standard remote-DevTools behavior.

Written by Devin

@staging-devin-ai-integration

Copy link
Copy Markdown
Contributor

Re-verified the computed-style used-value fixes (0182075, e186e63) with real Chrome DevTools:

  • Box Model content area now shows used pixel dimensions instead of auto × auto — <h1>: 354 × 28.800, <body>: 354 × 137.680, <html>: 590 × 407.680, with margins/paddings still numeric.
  • Insets resolve to used px for positioned elements (tested against a local page): #rel (relative): top: 10px / left: 15px, #abs (absolute): top: 20px / left: 30px (bottom/right also resolved in the Box Model position row), while static <body> still reports top: auto like browsers.

h1 Box Model with used px dims

More evidence (body/html box model, inset resolution)

body box model 354 × 137.680
html box model 590 × 407.680
#abs computed insets: top 20px / left 30px
#rel top 10px
static body top: auto

Written by Devin

@staging-devin-ai-integration
staging-devin-ai-integration Bot force-pushed the devin/1785376701-element-picker branch from 3dd2492 to 525afa5 Compare July 30, 2026 16:52
@nicoburns
nicoburns force-pushed the devin/1785390103-cdp-server branch from e186e63 to a97827c Compare July 30, 2026 18:32
@staging-devin-ai-integration
staging-devin-ai-integration Bot force-pushed the devin/1785390103-cdp-server branch from a97827c to 19e8e20 Compare July 30, 2026 18:43
@staging-devin-ai-integration
staging-devin-ai-integration Bot changed the base branch from devin/1785376701-element-picker to devin/1785430271-devtools-dom-support July 30, 2026 18:43
@staging-devin-ai-integration

Copy link
Copy Markdown
Contributor

Re-tested the rebased branch (onto devin/1785430271-devtools-dom-support) end-to-end with real Chrome DevTools, this time on the vello_cpu backend via cargo run -p rdme --no-default-features --features cpu,comrak,blitz-shell/devtools with BLITZ_CDP_PORT=9222. The re-resolved shell plumbing (DocumentProvider / DevtoolsPoll / picker input interception) works.

  • DOM tree renders + expands (DOM.requestChildNodes)
  • Styles/Computed/Box Model with used px values (h1: 480 × 28.800, margins 16.080)
  • Tree-hover overlay in the Blitz window draws + clears (Overlay.highlightNode/hideHighlight)
  • Picker: hover feedback, click selects + exits inspect mode, Escape cancels (Overlay.inspectModeCanceled), and no stuck-button/text-selection drag after picking

h1 Box Model with used px dims on CPU backend

Overlay + picker evidence

Tree-hover overlay drawn on p in the Blitz window
Picker selected p: breadcrumbs + styles switch
No text-selection drag after picking

Non-blocking observations
  • Resizing the Blitz window externally (wmctrl -e) can hang the app inside lavapipe Vulkan (pixels presentation path on the CPU renderer), which freezes the event loop and CDP — pre-existing renderer/environment behavior, not the CDP code.
  • Picking a collapsed node still doesn't reveal/move the visible tree row (breadcrumbs/Styles update correctly) — same as before the rebase.

Written by Devin

@staging-devin-ai-integration

Copy link
Copy Markdown
Contributor

Fixed the "picker doesn't expand/reveal the picked node in the tree" issue (7fff47c): the session now tracks which nodes' children were already sent and skips resending DOM.setChildNodes for them — resending made the frontend replace its node objects, detaching the tree's selection/expansion state.

Verified with real Chrome DevTools on the CPU backend (cargo run -p rdme --no-default-features --features cpu,comrak,blitz-shell/devtools, BLITZ_CDP_PORT=9222): from a fresh connection with body collapsed, picking a paragraph via inspect mode now auto-expands the Elements tree and visibly selects the picked <p> row (breadcrumbs html body div p) — previously only breadcrumbs/Styles updated while the tree stayed collapsed.

After pick: tree auto-expanded, p row selected

Precondition + regression evidence

Precondition (fresh connect, body collapsed):
Fresh connect, body collapsed

Regression — arrow expand/collapse, hover highlight overlay, Styles/Box Model still work:
h1 hover overlay in Blitz window
h1 Styles + used-px Box Model 480×28.800

Written by Devin

@staging-devin-ai-integration
staging-devin-ai-integration Bot force-pushed the devin/1785390103-cdp-server branch from 7fff47c to 829e833 Compare July 30, 2026 19:51
@staging-devin-ai-integration

Copy link
Copy Markdown
Contributor

Fixed comments showing empty in devtools (62cd6fe): NodeData::Comment now stores the comment text (populated by the HTML parser), and the CDP server reports it as the node's nodeValue (and in getOuterHTML).

Verified with real Chrome DevTools on the CPU backend against a page containing <!-- hello from a comment -->: the Elements tree renders the comment with its text instead of an empty <!---->. Quick regression: tree expansion and Styles (inline + user-agent rules, numeric Box Model) still work.

Comment text visible in Elements tree

Regression evidence

body expanded with comment between h1 and p
h1 selected: inline color darkblue + UA rules, box model 784 × 38.400

Written by Devin

@staging-devin-ai-integration

Copy link
Copy Markdown
Contributor

Fixed inspect-mode hover not revealing nodes in the tree (1e37614): hovering now emits the node's ancestor path via DOM.setChildNodes before each Overlay.nodeHighlightRequested, so the frontend can expand/highlight the hovered row in realtime like against real Chrome.

Verified with real Chrome DevTools on the CPU backend against a nested page, from a fresh connection with the tree collapsed: merely hovering elements in the Blitz window during inspect mode expands and highlights the hovered row live, updating as the hover moves between elements. Picking and Escape-cancel still work.

Hover-only: tree auto-expanded to span#deep

Live-update + pick/Escape evidence

Hover moved: b row revealed under ul#list > li
Picked b: selected row, breadcrumbs, styles
After Escape: canceled, b still selected

Written by Devin

@staging-devin-ai-integration
staging-devin-ai-integration Bot force-pushed the devin/1785390103-cdp-server branch from 1e37614 to 61d98fb Compare July 30, 2026 20:26
@nicoburns
nicoburns force-pushed the devin/1785430271-devtools-dom-support branch from 1d125db to b646f5a Compare July 30, 2026 20:37
@staging-devin-ai-integration
staging-devin-ai-integration Bot force-pushed the devin/1785390103-cdp-server branch 2 times, most recently from 5044eb2 to c9641b9 Compare July 30, 2026 20:59
@staging-devin-ai-integration
staging-devin-ai-integration Bot changed the base branch from devin/1785430271-devtools-dom-support to main July 30, 2026 21:00
@staging-devin-ai-integration
staging-devin-ai-integration Bot force-pushed the devin/1785390103-cdp-server branch from c9641b9 to 1464222 Compare July 30, 2026 21:05
@staging-devin-ai-integration

Copy link
Copy Markdown
Contributor

Migrated the transport to std::net + blocking tungstenite (995703a): nothing here needs async — all real work already happens synchronously on the event-loop thread via process_messages() — so the tokio runtime is replaced by a listener thread plus one thread per connection (alternating between reads with a short socket timeout and draining the outgoing queue). Drops the tokio, tokio-tungstenite and futures-util dependencies. Also added devtools feature flags to browser, rdme and dioxus-native (67477e5).

Smoke-tested end-to-end with real Chrome DevTools on the CPU backend (cargo run -p rdme --no-default-features --features cpu,comrak,devtools, BLITZ_CDP_PORT=9222) against a nested local page: discovery + fresh WebSocket handshake, tree expansion, Styles + numeric Box Model, live inspect-mode hover-reveal, pick, and Escape-cancel all work over the new blocking transport with no noticeable latency (/json/list answered in <1 ms after the session).

Fresh connection over new transport, tree rendered

Styles/Box Model + picker evidence

h1 UA rules + numeric box model
Hover-only revealed span#deep
Picked b: selection + breadcrumbs

Written by Devin

@staging-devin-ai-integration
staging-devin-ai-integration Bot force-pushed the devin/1785390103-cdp-server branch from d7c18f7 to 1f6494c Compare July 31, 2026 00:05
@staging-devin-ai-integration

Copy link
Copy Markdown
Contributor

Verified the live-editing feature (11d1f5e) end-to-end with real Chrome DevTools on the CPU backend (cargo run -p rdme --no-default-features --features cpu,comrak,devtools, BLITZ_CDP_PORT=9222) against a local HTML page: all four mutation flows work from the Elements panel and take effect in the actual Blitz window.

  • ✅ Double-click style attribute, lightblue → orange: Blitz repaints instantly, tree row updates and stays editable (no stale row)
  • ✅ Add attribute data-foo="bar": appears in the tree and persists on the node
  • ✅ Delete attribute (edit class="plain" to empty): removed from row, breadcrumb drops .plain
  • ✅ Double-click text node → "Edited via DevTools!": Blitz window re-renders the new text
  • ✅ Regression: tree browsing, Styles/Box Model, and inspect-mode picker (hover overlay + click select) all still work

Style edit applied — Blitz window repainted orange with updated tree row:
After style edit: Blitz box orange

Add / delete attribute + text edit + picker evidence

data-foo="bar" added to div#box
class attribute removed after editing to empty
Text node edit re-rendered in Blitz window
Picker regression: hover overlay on h1 in Blitz

Written by Devin

@staging-devin-ai-integration

Copy link
Copy Markdown
Contributor

Verified the sub-document targets feature (b09eefe) end-to-end with real Chrome DevTools against the actual browser app (cargo run -r -p browser --no-default-features --features cpu,devtools, BLITZ_CDP_PORT=9222), where each tab is a sub-document inside the chrome UI document.

  • ✅ /json/list lists the chrome document + one target per tab (title/URL); a newly opened tab appears as a new target after navigating to a page
  • ✅ Connecting to a tab target shows that tab's page DOM (example.org), not the chrome UI
  • ✅ Overlay.highlightNode from the tab session draws the highlight at the correct offset inside the tab (below tab strip + toolbar)
  • ✅ Inspect mode on the tab session: hover coordinates translate correctly past the toolbar (right element highlighted + tree live-revealed), click selects, Escape cancels
  • ✅ Regression: chrome-document target still inspectable (picked the urlbar input via picker), and single-document rdme is unaffected

Picker on the tab target — hover hit the exact "Learn more" link with the tree revealed live:
Tab-session inspect mode: correct element under cursor

Target enumeration + offset/regression evidence

3 targets after opening a 2nd tab
Overlay drawn inside the tab area below the toolbar
Chrome-doc regression: urlbar input picked
rdme single-document unaffected

Note: about: pages (e.g. a fresh about:newtab tab) render inside the chrome document and only appear as separate targets once navigated to a real page.

Written by Devin

@staging-devin-ai-integration

Copy link
Copy Markdown
Contributor

Verified inline style editing end-to-end with real Chrome DevTools on the CPU backend (cargo run -r -p rdme --no-default-features --features cpu,comrak,devtools, BLITZ_CDP_PORT=9222), typing all edits in the Styles pane. Two rounds of DevTools-frontend quirks were found and fixed during testing (commits 163d704b, 905d88e9, 1bfafcf3); final state passes everything:

  • ✅ element.style declarations are editable; value edit (lightblue → orange) repaints the Blitz window instantly, pane + Elements-tree style attribute update
  • ✅ Typed shorthand outline: 3px solid blue paints and stays authored (no longhand expansion); unrelated declarations preserved
  • ✅ Typed padding: 5px 30px on a fresh element: Box Model shows 5/30/5/30
  • ✅ Adding a declaration to an element with no style attribute creates it and renders
  • ✅ Checkbox toggle round-trips: unchecking comments the declaration out (/* color: green; */, shown struck-through with an unchecked box), re-checking restores it with no comment residue, repeatedly
  • ✅ Regression: author/UA rules stay read-only; Computed/Box Model unaffected

Key behaviors the implementation needed for real DevTools (beyond a naive setStyleTexts):

  • edits are sub-range splices into the sheet text, not whole-attribute replacement (DevTools edits a single declaration's range)
  • the inline sheet text must be the authored attribute text verbatim — DevTools live-commits on every keystroke and adopts the echoed text as its editing model, so longhand-expanding shorthands corrupts the edit mid-typing
  • /* name: value; */ comments must be reported as disabled: true properties so the checkbox can re-enable them

Typed outline shorthand paints, stays authored, green preserved

More evidence

Value edit applied: Blitz box repainted orange
Box Model 5/30/5/30 from typed padding: 5px 30px
Uncheck: struck-through declaration with unchecked checkbox
Re-checked: restored, attribute comment-free, outline still painted

Written by Devin

@staging-devin-ai-integration
staging-devin-ai-integration Bot force-pushed the devin/1785390103-cdp-server branch from 58d0c59 to f0d1bc0 Compare August 13, 2026 17:31
Implements HTTP discovery (/json/list, /json/version) plus a WebSocket
endpoint speaking plain-JSON CDP, with the DOM, CSS and Overlay domains
needed for Chrome DevTools' Elements panel: DOM tree inspection (with
anonymous/whitespace nodes filtered), matched/computed/inline styles from
Stylo, box model geometry from Taffy (incl. inline fragment rects),
node highlight overlays and the element picker (inspect mode).

Integrated into blitz-shell behind the devtools feature, opt-in via the
BLITZ_CDP_PORT env var, using synchronous document access on the event
loop thread via a DocumentProvider adapter.
Chrome DevTools builds the Box Model diagram from computed style, and
browsers report used (post-layout) values there: width/height, margins,
paddings and border widths now come from layout instead of the specified
values, so the content area no longer shows 'auto x auto'.
Resending DOM.setChildNodes for a parent whose children were already sent
makes the devtools frontend replace its node objects, detaching the tree's
selection/expansion state — so picking an element updated breadcrumbs and
styles but didn't expand/reveal the node in the Elements tree. Track which
nodes have had their children sent per session and skip them when emitting
ancestor paths for querySelector / pushNodesByBackendIdsToFrontend.
Chrome reveals the node under the cursor in the Elements tree in realtime
while inspect mode is active, which requires the frontend to know the node
before the Overlay.nodeHighlightRequested event: send the (not yet sent
parts of the) node's ancestor path via DOM.setChildNodes first.
Forward to blitz-shell/devtools so the CDP server can be enabled with
e.g. 'cargo run -p rdme --features devtools' + BLITZ_CDP_PORT.
Nothing in the server needs async: all real work happens synchronously
on the event-loop thread via process_messages(), and connection counts
are tiny. Replace the tokio runtime with a listener thread and one
thread per connection, which alternates between reading (with a short
socket read timeout) and draining the outgoing message queue. Drops the
tokio, tokio-tungstenite and futures-util dependencies.
DOM.setAttributeValue, DOM.setAttributesAsText, DOM.removeAttribute
(with DOM.attributeModified/attributeRemoved events) and
DOM.setNodeValue for text and comment nodes (with
DOM.characterDataModified). Mutations go through DocumentMutator so
snapshots/restyle/damage are handled, and request a redraw.
Each sub-document (e.g. a browser tab) is enumerated recursively as an
inspectable target of its own. While picking on a sub-document, window
coordinates are translated into the sub-document's coordinate space by
the host element's position and scroll offset (mirroring pointer event
forwarding), and shell redraw requests resolve windows through their
sub-documents.
Inline styles now carry a synthetic per-element style sheet id and source
ranges into their serialized cssText, which makes the Styles pane treat
them as editable. CSS.getStyleSheetText serves that text and
CSS.setStyleTexts replaces the element's style attribute (via the
document mutator), returning the re-serialized style and emitting
CSS.styleSheetChanged / DOM.attributeModified.
The frontend edits a sub-range of the sheet text (a single declaration's
range when edited inline), so replacing the whole style attribute with
the edit's text dropped the other declarations and corrupted multi-token
values as the frontend's text model drifted. Each edit's text is now
spliced into the current sheet text at the given range.
DevTools live-commits setStyleTexts on every keystroke and adopts the
text the server echoes back as its editing model. Reporting a longhand-
expanded re-serialization therefore corrupted shorthand edits mid-typing.
The inline style sheet's text is now the authored style attribute value,
with declarations and ranges scanned from it verbatim.
DevTools disables a declaration by commenting it out; reporting the
comment as plain declaration text left the frontend without a disabled
property to re-enable, so clicking the checkbox again nested a second
comment and invalidated the following declarations. Declarations wrapped
in /* name: value; */ comments are now reported with disabled: true and
the comment as their text/range, so the checkbox round-trips.
…text

The frontend computes all of a batch's ranges against the same snapshot
of the sheet text, so applying edits sequentially against the mutated
text misaligns later edits' offsets. Resolve every range against the
original text up front and apply the splices back-to-front.
Closing tags on void elements (<img ...></img>, <br></br>) are invalid
markup that re-parses into a different document. Non-void empty elements
keep their explicit closing tag, matching Chrome's serializer (omitting
it would make the element swallow following content when re-parsed).
@staging-devin-ai-integration
staging-devin-ai-integration Bot force-pushed the devin/1785390103-cdp-server branch 2 times, most recently from d1ed18f to 17a545f Compare August 19, 2026 13:22

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant