Repository navigation
Bring main up to date: CDN probe, console.table, shell fixes, element split - #1
Merged
Merged
Conversation
jerrychan7
commented
Nov 2, 2026
Owner
- refactor(cdp-agent): move snapDOM rendering out of the screencast into a shared render
- feat(cdp-agent): take screenshots with Page.captureScreenshot and report Page.getLayoutMetrics
- docs: describe screenshots and their limits
- fix(cdp-agent): scale screenshots down only when the canvas cannot hold them
- feat(cdp-agent): answer the emulation commands device mode sends
- feat(shell): device mode in shell mode, with DevTools laying out the page as in Chrome
- docs: describe device mode and the frontend's layout in shell mode
- ci(release): require npm 11.21.0, which npm dist-tag needs under trusted publishing
- fix(shell): clear device mode with the frontend and tidy the shell layout
- fix(cdp-agent): retry a screenshot smaller only when drawing fails
- fix(cdp-agent): report the deprecated layout metrics in device pixels as Chrome does
- test(cdp-agent): cover the screenshot retry and the layout metrics on a scaled screen
- test(e2e): make the device mode tests check what they claim
- docs: describe the branch model and the development and release flow
- docs: move the design out of the README into docs/design.md
- feat(shell): resize docked DevTools with a grip, shared by the panel and the shell
- docs: a short README to start with, and a usage guide to look things up in
- fix(runtime): preview console.table's rows so the frontend can draw the table
- feat(probe): one script tag from jsDelivr or unpkg, shell mode included
- docs(changelog): note the console.table fix
- fix(shell): restore the page when the shell's element goes, and narrow device mode's page
- test: restore what the tests patch even when they fail, and prove the CDN probe ran
- refactor(element): split element.ts into an element/ directory
…o a shared render The screencast loaded snapDOM, picked the page background, hid DevTools' own elements and encoded the canvas itself. Move that into page/render.ts as createRender and encode, so that Page.captureScreenshot can render with the same loaded snapDOM. The render also takes a page-coordinate clip besides 'viewport'. createCdpSession builds the render once from loadSnapdom; the screencast keeps its own error when snapDOM is not configured.
…ort Page.getLayoutMetrics Tools and agents that drive the page over CDP can already read the DOM, but had no image of it. Page.captureScreenshot renders with snapDOM, like the screencast, and follows Chrome's parameters as checked against real Chrome: clip is in page coordinates with or without captureBeyondViewport, the image has device pixels (devicePixelRatio times clip.scale), and captureBeyondViewport without a clip takes the whole page. The response adds approximate: true, a field Chrome does not send, since the image is the DOM redrawn rather than painted pixels. Images over 4096 x 4096 pixels, Safari's canvas limit, are scaled down to fit. Page.getLayoutMetrics returns the viewport, visual viewport and content size in CSS pixels in both sets of fields, as Chrome does on an unzoomed page; DevTools uses it for node screenshots. The conformance tests compare image sizes and the clipped content with Chrome's, at device scale factor 2 too, and getLayoutMetrics field by field.
README lists screenshots under the limitations next to the screencast: rendered by snapDOM, marked approximate, Chrome's parameters otherwise, scaled down past Safari's canvas limit, PNG where WebP cannot be encoded. The changelog notes the two new methods under Unreleased.
…ld them A screenshot was capped at 4096 x 4096 pixels, the smallest canvas limit of any browser, so a long page came out scaled down in Chrome too. Pass the size asked for to snapDOM instead: it already scales an image past the browser's canvas limits down to fit (32767 pixels a side, 16384 on Safari, 16384 x 16384 in area). Should drawing still fail, by throwing or by encoding an empty data URL, the screenshot is halved and tried again; once it fits in 4096 x 4096 the failure is reported. The conformance tests check that a 2000 x 20000 page is taken at full size and that a 2000 x 200000 page is scaled down to the longest side Chrome allows. README states the limits.
Device mode sends Emulation.setSafeAreaInsetsOverride, resetPageScaleFactor and setUserAgentOverride, Network.setUserAgentOverride, and Overlay.setShowHinge and setShowDisplayCutout. A page cannot do any of them, and an error from one stops the frontend's device mode from updating, so they are answered with empty results like the other commands without an in-page equivalent.
…page as in Chrome In Chrome the device toolbar is part of DevTools' window: its app (AdvancedApp) lays out the page and DevTools, and tells the browser where the page goes (setInspectedPageBounds). In shell mode the frontend now does the same. The shell passes #?shell=1, the entrypoint sets can_emulate_device and loads the emulation module, and the frontend fills the shell. Its setInspectedPageBounds goes to the shell (page-bounds), which places the page iframe there on top, and the shell answers Emulation.setDeviceMetricsOverride and clearDeviceMetricsOverride by sizing and scaling the iframe (shell-layout.ts). The frontend fills the shell from the start: device mode does not recover from laying out in a small frame first. Hidden, the page fills the shell at its own size; shown again, device mode is back. On touch screens a 28px handle straddles the edge between the page and DevTools (the frontend reports its split size), and dragging it sets DevTools' size (sidebar-size notice). Undocked, Chrome draws device mode into device_mode_emulation_frame.html in the tab. The frontend asks the shell to show that page (toolbox) instead of opening a window; the shell shows it in an iframe under the floating panel, and the frontend finds it among its parent's frames and hands its document to AdvancedApp. The build keeps the page, bundled as one file. patches/device-mode-in-shell.patch adds the can_emulate_device condition to device mode, keeps the screencast out of shell mode, opens device mode's page next to anywhere_app.html, and drops the device pixel ratio and device type from the toolbar's menu, since a page cannot change either. Outside shell mode nothing changes: the simple app and the screencast stay. device-mode.test.ts runs the built frontend in shell mode: the page goes where the frontend's placeholder is, a device preset sizes and scales it, the screenshot command saves it at the device's size, hiding and showing, undocking into device mode's page, and the touch handle.
README: in shell mode DevTools lays out the page as in Chrome, with device mode on the toolbar's device button, undocked too; device mode sets the page's size only, not its pixel ratio, user agent or touch input. DEV: how the entrypoint, the shell and the patch fit together, and device mode's page bundled as one file. The changelog notes it under Unreleased.
…ted publishing npm dist-tag works with trusted publishing once each package's trusted publisher allows it, from npm 11.21.0. The publish job installed npm@11, which happens to resolve to a later release today; it now asks for ^11.21.0 so that the Chrome milestone tag cannot fall back to an older npm that has no credentials for it.
…yout - A frontend loaded in place of another (its URL changed) no longer inherits the previous one's device metrics: the page iframe is no longer sized, scaled and clipped for a device mode the new frontend never turned on. Chrome clears the override with the frontend too. - The shell layout is disposed when its element leaves the shell: its resize listener, touch handle and device mode page are removed, so that an element replaced and connected again does not stack them up. - A toolbox request whose URL does not parse is ignored instead of throwing in the message handler. - The frontend looks for device mode's page for its latest request only, so undocking twice in a row no longer hands the page over twice; it waits up to 60 seconds instead of 10 for a slow load, and says in the console when the page never loaded.
Page.captureScreenshot halved the image and tried again on any error, until it was small enough to report the failure, so a snapDOM that failed to load was loaded again several times before the error came back. Failures before drawing (loading snapDOM, reading the page) are now a CaptureError and reported at once; only a failure to draw the canvas is tried again at half the size.
… as Chrome does Page.getLayoutMetrics returned CSS pixels in the deprecated layoutViewport, visualViewport and contentSize as well as in the css* fields, which only matches Chrome on a screen with a scale factor of 1. Measured in Chrome on a screen scaled by 2: the layout viewport's position and size, the visual viewport's size and the content size are in device pixels; the visual viewport's position stays in CSS pixels. Ours now multiplies the same fields by devicePixelRatio. The conformance tests compare both on a screen scaled by 2, for which launch() can now leave pages Chrome's own viewport (defaultViewport: null) rather than Puppeteer's 800 x 600 at a scale of 1.
… a scaled screen - captureScreenshot with a stand-in snapDOM that fails to draw until a quarter of the size: it draws at 1, 0.5 and 0.25; and with a snapDOM that fails to load: it loads once and reports that error. - getLayoutMetrics matches Chrome on a screen with a scale factor of 2, scrolled.
- Picking a device that is not in the list now fails: the undocked test picked "iPhone SE" again in the toolbox (the page kept its size from the previous test, so a broken toolbox toolbar would have passed); it now picks "iPhone 16", which only that toolbar can set. - The stand-ins for convertToBlob, anchors' click and matchMedia are restored once used. - waitFor says what it waited for when it times out. - New: docked to the right, the page goes where the frontend says; leaving device mode gives the page the room the frontend lays out for it. - The file says that its tests run in order, from where the previous one left the page.
DEV.md and DEV.zh-CN.md now say how work reaches main and how releases are cut from it: - main is the only long-lived branch, changed through pull requests with CI passing; there is no develop branch, since merging releases nothing and main itself holds what is merged and not yet released. - Short-lived branches are named after the commit type (feat/..., fix/..., ...), and a change users notice adds its changelog line in the same pull request. - A release is prepared on release/x.y.z through a pull request, then tagged on main; releases are cut when there is something worth releasing, not on every merge. A new feature is a minor. - An older minor is patched on release/x.y, branched from its tag, with fixes merged into main first and cherry-picked; what that still needs (CI on release/** and publishing without moving latest) is listed as not set up yet. - ci is listed among the commit types. - The API report paragraph no longer says the repository has no CI: GitHub Actions sets CI=true, so CI fails on a report left uncommitted.
The README spent most of its length on how the project works before saying how to use it. It now answers what the project is, why, how to use it, what it cannot do and which packages there are, in that order; the design moves to docs/design.md and docs/design.zh-CN.md: - the technology choices and the relation to Chii and chobitsu; - the architecture: the probe, shell mode and injection; - why the packages are split as they are (the package table stays in the README). The text is moved, not rewritten. Links into the moved sections (shell mode, injection) now point at the design document, and its links to DEV.md and experiments/ are relative to docs/. DEV.md and DEV.zh-CN.md point to the design document, list docs/ in the repository layout and ask for it to be kept up to date with the README.
…and the shell A docked panel was resized by a bar across its whole edge, and in shell mode by the frontend's own resizer, a few pixels wide and invisible (with a wider handle on touch screens only, which covered the top of DevTools). Both now get the same grip: a small pill with three dots against the middle of DevTools' edge, on the page's side, like a tab of the panel. It takes input only where it is seen, so it covers none of DevTools and only as much of the page as it shows; it is larger on touch screens. - grip.ts is the reusable piece: a Grip placed against an edge of its parent, across it, inside or outside, with its size for a mouse and for a finger, drag callbacks, and colors from --grip-* custom properties. - The panel puts it outside its inner edge; the bar now only shows on the undocked window, which it moves. - The shell puts it inside the page's area, against its edge next to DevTools, as the frontend lays the page out; it takes DevTools' toolbar colors like the panel's. - The device mode tests find the grip by its title, check its size and place, and wait for the undocked page's width as well as its position, which raced. - In the shell the grip sits on the page's side of the split, which the frontend now reports (`split`, its rect) in place of the sidebar size: the page bounds are only the device's screen in device mode, which put the grip on the screen's edge, away from DevTools.
…up in The README spent its length on long sentences that readers had to dig through for the one option they needed. It now says what devtools-anywhere is, how it compares, how to start (Vite, a CDN script tag, a phone from a computer), the common options, what works, and the packages, with two screenshots of WebMC (https://github.com/jerrychan7/WebMC): device mode in landscape on a computer, and a phone, the Elements panel dark next to the light page. The images are lossless WebP. docs/usage.md and docs/usage.zh-CN.md are the reference, in tables and short lines: which setup for which scenario, the script tag and its attributes with their Vite options and defaults, the Vite plugin, an element placed by the page, the panel's controls and togglers, shell mode (what it changes, when it is off, its caveats, the Service Worker), remote debugging (CLI options, behavior, Node.js API and endpoints), the limitations, the screenshots' parameters, and the packages. Everything the README said before is kept, and corrected where it was wrong: shell mode needs the bundle and frontend on the probe's origin (not the page's), which is why a CDN setup turns it off; the Vite plugin defaults to the installed frontend; its serviceWorker option is on / off only. The design documents link to the usage guide, the Chinese one gains the half sentence it was missing on the network buffers and says DevTools throughout, and two long English sentences are split. DEV.md, the packages' READMEs and a comment in screenshot.ts point to the usage guide.
…he table console.table of an array of objects showed a single "Value" column, since the rows' previews had no fields of their own. Like V8, the data is now previewed as a table: up to 1000 rows, each with a preview one level deep and no limit on its columns. Columns given as an array filter the rows, and only the data is sent, as Chrome does. The conformance tests compare the whole arguments with Chrome's, for rows of objects and arrays, an object of objects, columns, many columns and rows, and a primitive.
From an npm CDN the probe needed the frontend's URL, and shell mode had to be turned off: shell configs were trusted on the probe's origin, which on a CDN is everyone's package, so a page's script could point the shell to any frontend published there. The URL of the script that reads a shell config is now its trust root (trust.ts). On jsDelivr or unpkg it trusts only this package and the frontend's, at its own version; URLs are compared once parsed, so dot segments are resolved, and escaped slashes or backslashes are refused. Elsewhere it still trusts its origin. With that, the probe from such a CDN loads the frontend of its version there by default, and shell mode works, without the Service Worker, which must be on the page's origin. data-no-panel goes without the panel and shell mode, now that a frontend may be found without data-devtools-frontend-url: a page from such a CDN only debugged remotely needs it, or it gets a panel too. Unit tests cover the default frontend and the trust rules, path tricks included; e2e tests play the CDN through request interception: one tag enters shell mode with the frontend of the same version, a config pointing to another package on the CDN is dropped (it was trusted before), data-no-panel mounts nothing, and the built frontend from the CDN lays out the page. The README, usage guide, design notes and changelogs say so.
66a2a30 fixed console.table of objects, which showed a single "Value" column, but did not say so in the changelogs.
…w device mode's page Found in review: - Removed from the shell for good, the element left the page iframe as the layout placed it (fixed, maybe at a device's size, scaled and clipped). ShellLayout.dispose now clears that, so the page fills the document again. - Device mode's page (the toolbox) was accepted from anywhere on the frontend's origin, which on an npm CDN is every package's. It must now sit next to the frontend's entry page, compared as trust.ts compares shell configs. - A grip's drag ends on lostpointercapture, which follows pointerup and pointercancel and also fires when the grip is removed mid-drag, so the drag always ends once. - The probe's and bundle's comments said shell configs must come from the script's origin, no longer the rule on a CDN. Device mode's e2e tests check that the element removed from the shell leaves the page unplaced, and that a page of the frontend's origin but not next to it is refused as the toolbox; both fail without the fix.
… CDN probe ran Found in review: - Device mode's tests patched OffscreenCanvas and anchor clicks, the shell's matchMedia and the dock side, and restored them only once their checks passed: a failure left the later tests on a patched page. They restore in finally now. - The CDN tests that check nothing happens (a config to another package dropped, data-no-panel) also passed when the probe never ran. They now wait for the probe's element, or for the bundle with data-remote, instead of a fixed delay. A CDN file that fails to load aborts its request rather than leaving it unanswered. - The trust rules are checked on unpkg and at a dist-tag too.
element.ts had grown to 1103 lines, past the size check's warning. Its parts that do not need the element's private state now live next to it: - style.ts: the shadow root's style sheet. - registry.ts: which element is which (global, the shell's, its F12 button in the shell's page, placed by the page) and the internal events between them; elements of this window's class are now known by a WeakSet filled in the constructor instead of instanceof, so the togglers need not import the class. - togglers.ts: elements with data-devtools-anywhere-toggle. - panel.ts: the panel's remembered sizes and position, and dragging the bar, the grip and the corner. - request.ts: the checks on requests from the frontend. element/index.ts keeps the element itself (690 lines). No change in behavior; DEV.md and comments point to the new path.
…emulation On CI, Chrome sometimes answers Input.dispatchMouseEvent's mouseReleased with touch emulation for the mouse just enabled by sending the page a pointerup and a mouseup, as for a real mouse: 'a release or a move without a touch sends nothing' failed twice in a row on PR #1 and once on main before, and never locally. Chrome's side now ignores those as it already did the hover's pointerover and mouseover; ours still has to send nothing at all.
On CI, Chrome sometimes sends no pointerout for the field the test before clicked, where ours does: 'a middle click fires auxclick' failed twice in three runs on PR #1. What it checks is the press and auxclick: the hover's pointerover/out and mouseover are left out of the comparison.
On CI the Watch pane's prompt was sometimes in the DOM but not laid out yet when clicked, and Puppeteer threw 'Node is either not clickable or not an Element'. The test now waits for a box before each click there (add watch, the prompt, the line number).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.