diff --git a/.gitignore b/.gitignore
index 66fc22b56..31e001b3c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,25 @@
/.zig-cache/
/.lp-cache/
+/.lp-cache-win/
zig-out
lightpanda.id
/src/html5ever/target/
+/html5ever-test/
src/snapshot.bin
+/_link_trace.log
+/_msvc_link.log
+/_msvc_use_lld_false.log
+/_msvc_use_lld_false_rel.log
+/tmp-browser-smoke/**/*.stdout.txt
+/tmp-browser-smoke/**/*.stderr.txt
+/tmp-browser-smoke/**/*.out.txt
+/tmp-browser-smoke/**/*.err.txt
+/tmp-browser-smoke/**/*.before.png
+/tmp-browser-smoke/headed-smoke.png
+/tmp-browser-smoke/links.bmp
+/tmp-browser-smoke/flow-layout/flow-layout.png
+/tmp-browser-smoke/image-smoke/headed-image-smoke.png
+/tmp-browser-smoke/inline-flow/inline-flow.png
+/tmp-browser-smoke/multi-image/multi-image.png
+/tmp-browser-smoke/wrapped-link/wrapped-before.png
+/tmp-browser-smoke/wrapped-link/wrapped-base-before.png
diff --git a/README.md b/README.md
index 1a860fc74..f7aaf372b 100644
--- a/README.md
+++ b/README.md
@@ -123,6 +123,35 @@ INFO app : server running . . . . . . . . . . . . . . . . . [+0ms]
address = 127.0.0.1:9222
```
+### Browser mode switch (fork)
+
+This fork adds a browser mode switch on `fetch` and `serve`:
+
+```console
+./lightpanda serve --browser_mode headed
+```
+
+Shortcuts are available:
+- `--headed`
+- `--headless`
+
+`headed` is currently experimental.
+- On Windows targets, it starts a native headed window lifecycle backend.
+- Windows headed mode now forwards native mouse (down/up/move/wheel/hwheel), click, keydown/keyup (including repeat state), text input (`WM_CHAR`/`WM_UNICHAR`), IME result/preedit composition messages (`WM_IME_COMPOSITION`), back/forward mouse buttons, and window blur into page input, with caret-aware text insertion, `Ctrl/Meta + A` select-all, word-wise caret/edit shortcuts (`Ctrl/Meta + ArrowLeft/ArrowRight`, `Ctrl/Meta + Backspace/Delete`), textarea vertical/line navigation (`ArrowUp/ArrowDown`, line-aware `Home/End`, document `Ctrl/Meta + Home/End`), `Tab`/`Shift+Tab` focus traversal (including positive `tabindex` ordering), and native clipboard shortcuts (`Ctrl/Meta + C/X/V`, `Ctrl+Insert`, `Shift+Insert`, `Shift+Delete`) for text controls. Clipboard shortcuts now dispatch cancelable `copy`/`cut`/`paste` events first and respect `preventDefault()`.
+- On non-Windows targets, it safely falls back to headless execution.
+
+Viewport sizing is configurable for both modes:
+- `--window_width ` rendering works for common sources used by the probes
+- focus/autofocus/input bootstrap is working in headed mode
+- label activation and `Enter` form submit basics work
+- bounded localhost smoke probes exist for navigation, history, reload, stop,
+ wrapped links, and form interactions
+- native tab strip, reopen-closed-tab, and session restore are working in the
+ headed shell
+- native overlays exist for history, bookmarks, downloads, and basic settings
+- headed `browse` now has zoom controls, find-in-page, bookmark persistence,
+ download persistence, homepage navigation, and persisted default zoom /
+ restore-session settings
+- headed `browse` now has a persisted script-popup policy with Win32 settings
+ UI, blocked-runtime coverage, and allowed/blocked script popup acceptance
+- internal `browser://history`, `browser://bookmarks`, `browser://downloads`,
+ `browser://settings`, and `browser://tabs` pages now support stateful
+ actions, not just static snapshots
+- those internal history, bookmark, and download pages now keep per-tab sort
+ state, expose in-page sort controls, and refresh titles/counts plus row order
+ live as the sort mode changes
+- the normal headed shell shortcuts now target internal browser pages first,
+ while the legacy overlays are secondary diagnostic surfaces
+- the headed painter now keeps direct paragraph text in the same inline flow as
+ inline child chips and links for the current simple mixed-inline path,
+ instead of splitting the paragraph into a separate text band above the inline
+ controls
+- invalid or unsupported selector syntax in page JS and stylesheet matching no
+ longer tears down headed mode; selector syntax errors now stay in-page as JS
+ failures while invalid stylesheet selectors are skipped safely
+- headed JS microtask checkpoints now run inside the target context with a real
+ V8 handle scope, removing the clean Google startup `HandleScope::CreateHandle`
+ fatal seen during Promise-heavy page initialization repros
+- the first real CSS/layout compatibility slice is in place for headed
+ documents: `min(...)`, `max(...)`, `clamp(...)`, `%`, `vw`, and `vh` lengths;
+ block auto-margin centering; flex-column centering; centered inline child
+ flow for `text-align:center`; and absolute out-of-flow positioning, with
+ focused painter tests plus bounded headed runtime probes for microtask
+ containment, centered flex hero layout, and absolute corner docking
+- the headed painter now also respects basic text styling on real labels and
+ inline text, including `line-height`, `letter-spacing`, `word-spacing`, and
+ `text-transform`, with Google nav-style and dedicated text-style regression
+ coverage wired into the render tests
+- the headed painter now also threads inherited CSS `opacity` through the
+ display-list path so rect, text, image, and canvas paint commands carry a
+ consistent alpha multiplier into headed composition
+- overflow:auto containers now keep scrolled link regions visible enough for
+ real click activation on the headed surface, and screenshot readiness no
+ longer burns the capture on bodyless-but-substantive or dense text-only
+ frames; bounded scroll, visible-link, opacity, and text-style probes now
+ cover the programmatic scroll state and the resulting navigation path
+- CSS translate transforms now carry through headed painting and DOM
+ hit-testing for translated controls and links, with bounded screenshot and
+ interaction coverage proving the transformed button and link geometry both
+ render and remain targetable
+- the headed painter now also paints a pragmatic one-layer `box-shadow` for
+ boxes and controls, with a renderer test plus a bounded headed screenshot
+ probe proving the visible offset shadow on the real Win32 surface
+- flex row layout now also respects authored `order`, `flex-shrink`,
+ `align-content`, and `align-self`, with bounded headed probes proving
+ ordered links, shrunk boxes, wrapped line spacing, and per-item vertical
+ alignment on the real Win32 surface
+- the headed painter now caches painted element layout boxes so DOM
+ `getBoundingClientRect`/hit-testing can reuse painted geometry for visible
+ elements instead of falling back to the older sibling-position heuristic
+- flex column layout now also respects authored `flex-grow`, `flex-shrink`,
+ `justify-content`, and `column-reverse`, with renderer tests plus bounded
+ headed probes proving the grow, justify, reverse, and stretch paths on the
+ real Win32 surface
+- the browser now accepts an explicit profile directory override and resolves
+ its persistence root through a small host-path abstraction, which is the
+ first cross-platform/bare-metal support seam for cookies, storage,
+ downloads, and telemetry IDs
+
+## Achieved Gates
+
+### Gate A: Windows Headed Foundation
+
+Status: Achieved
+
+- native headed window lifecycle
+- native input translation and text editing baseline
+- `browse` command path
+- Windows runtime stabilization and smoke probes
+
+### Gate B: Headed Browser Interaction MVP
+
+Status: Achieved
+
+- address bar navigation
+- back/forward/reload/stop chrome
+- wrapped-link hit testing and navigation
+- live page restore after stop
+
+### Gate C: Shared Presentation Surface
+
+Status: Achieved
+
+- display-list based headed presentation path
+- screenshot/export on the same presentation path
+- basic text, box, link, and image presentation
+
+### Gate D: Basic Form/Input Reliability
+
+Status: Achieved
+
+- autofocus and initial typing
+- label activation
+- `Enter` submit path
+- stable Windows `SendInput`-driven headed probes
+
+## Remaining Gates
+
+### Gate 1: Browser Shell MVP
+
+Status: Active next milestone
+
+Goal:
+- turn the current single-page headed shell into a minimal real browser shell
+
+Exit criteria:
+- tab strip with open, close, switch, duplicate, and reopen closed tab
+- new-window and basic popup/window handling policy
+- visible loading/error states and disabled chrome state where applicable
+- history UI, bookmarks UI, downloads UI, and basic settings UI
+- find-in-page and zoom controls
+
+Current state inside Gate 1:
+- tabs now cover open, close, switch, duplicate, reopen, and clean session restore
+- history, bookmarks, downloads, settings, find, and zoom are present in the
+ headed shell
+- disabled close-state for the single remaining tab is implemented
+- rendered `_blank` anchor popups now open in a new tab through the native
+ headed surface
+- form-driven `_blank` submission now reaches a stable headed new-tab flow
+- script-driven `window.open()` now reaches stable headed `_blank` and
+ named-target tab flows, and later launcher-page callbacks remain alive after
+ popup activation
+- script popup policy now covers allowed vs blocked `window.open()` behavior,
+ with persisted settings and headed Win32 shell controls
+- browser-side named-target queueing/reuse is now implemented for anchors and
+ form submission, with direct page/session tests covering anchor click, anchor
+ `Enter`, and GET/POST form submission
+- bounded headed probes remain the acceptance gate for `_blank` popup flows,
+ rendered named-target anchor pointer activation, script popup tab reuse, and
+ launcher-background callback survival after popup open
+- bounded headed probes now also cover popup policy persistence through the
+ settings overlay and blocked script-popup runtime behavior
+- rendered same-tab link activation now dispatches a real DOM click first, so
+ `onclick`, `preventDefault`, and click-time href mutation are preserved on the
+ headed surface before any direct navigation fallback
+- dedicated internal browser pages now exist for history, bookmarks, downloads,
+ settings, and tabs, backed by current session state or the persisted stores
+ already in place
+- those browser pages are reachable through both native headed shortcuts and
+ `browser://start`, `browser://tabs`, `browser://history`,
+ `browser://bookmarks`, `browser://downloads`, and `browser://settings`
+ address-bar aliases
+- those browser pages now execute real internal actions:
+ - tab new, activate, duplicate, reload, close, and reopen-closed flows
+ through `browser://tabs/...`
+ - history traverse, reload-safe reopen, and clear-session collapse
+ - bookmark add-current, open, and remove backed by the persisted bookmark
+ store
+ - download source, remove, and clear-inactive backed by the persisted
+ download store
+ - settings toggles for restore-session, script popups, default zoom, and
+ homepage mutation
+ - homepage navigation to an internal page plus restart-time restore of the
+ internal page in the session model
+- the standard shell shortcuts now open those internal pages directly:
+ - `Ctrl+Shift+A` tabs
+ - `Ctrl+H` history
+ - `Ctrl+Shift+B` bookmarks
+ - `Ctrl+J` downloads
+ - `Ctrl+,` settings
+- the internal pages now include a shared shell header/nav plus a
+ `browser://start` hub page, and `Alt+Home` falls back to that start page when
+ no homepage is configured
+- bounded headed browser-page probes now cover:
+ - history, bookmarks, downloads, and settings actions
+ - bookmark add-current, history clear-session, and download clear-all flows
+ - start-page cross-navigation
+ - `browser://start` quick actions and settings-summary mutations through
+ in-page document actions, not only address-bar routes
+ - `browser://start` recent history/bookmark/download preview actions through
+ in-page document actions
+ - tabs-page tab-management actions and reload/reopen recovery
+ - `browser://tabs` indexed closed-tab reopen through in-page document actions
+ - homepage-to-internal-page restart restore
+- legacy overlays are still available for diagnostics on secondary shortcuts,
+ but they are no longer the primary shell path
+- internal page titles now stay user-facing across active presentation,
+ background tab state, restart restore, and the zero-count downloads case
+- headed navigation failures now promote into a structured `browser://error`
+ page instead of a raw placeholder document
+- that error state now remains visible across `browser://start` and
+ `browser://tabs`, with bounded headed probes for invalid-address handling,
+ disabled back/forward chrome on error, error-state preservation, and
+ recovery once the target becomes reachable again
+- `browser://history`, `browser://bookmarks`, and `browser://downloads` now
+ keep live per-tab filter state, support internal `filter/...` and
+ `filter-clear` routes, expose quick-filter links directly on the page, and
+ have bounded headed probes for quick-filter plus clear-filter document
+ actions
+- those same internal history/bookmark/download pages now expose explicit
+ per-row open-in-new-tab actions, with bounded headed probes proving the new
+ tab opens while the originating internal page tab remains intact
+- those same internal history/bookmark/download pages now also keep per-tab
+ sort state, support internal `sort/...` routes, expose in-page sort controls,
+ and have bounded headed probes for sort changes plus sorted row actions
+- `browser://history` now also supports in-page single-entry removal plus
+ safe `remove-before` / `remove-after` pruning that preserves the current live
+ page, with bounded headed probes for single-remove and both prune directions
+- `browser://bookmarks` now supports persisted in-page reorder actions in saved
+ order mode, and `browser://downloads` now supports in-place retry of failed
+ and interrupted entries with bounded headed document-action coverage
+- `browser://bookmarks` now also supports opening all currently visible
+ bookmark rows in new background tabs based on the page's active filter and
+ sort state, with a bounded headed probe proving the filtered bookmarks page
+ stays active while the visible bookmark targets open in saved-order
+- `browser://downloads` now also supports native shell actions for completed
+ entries, including `Open file`, `Reveal file`, and `Open downloads folder`,
+ with bounded headed probes proving each action fires while the originating
+ downloads page remains active
+- headed `browse` tabs now share one persistent cookie jar instead of keeping
+ cookie state session-local per tab, and that cookie jar now survives browser
+ restart and can be cleared from `browser://settings`, with bounded same-tab,
+ cross-tab, restart, and clear-cookies headed probes
+- headed `browse` tabs now also share one persistent origin-scoped
+ `localStorage` shed across tabs and browser restart, and that storage can be
+ cleared from `browser://settings`, with bounded cross-tab, restart, and
+ clear-local-storage headed probes
+- that same headed `localStorage` path now also dispatches real cross-tab
+ `storage` events through `window.onstorage` and `StorageEvent`, with a
+ bounded headed probe proving a listener tab receives the event after a
+ sibling tab mutates `localStorage` and both tabs remain alive afterward
+- headed `browse` tabs now also keep real per-tab `sessionStorage` state that
+ survives same-tab navigation but does not leak across tabs or browser
+ restart, with bounded same-tab, cross-tab, and restart headed probes
+- headed `browse` tabs now also share one persistent origin-scoped IndexedDB
+ shed across tabs and browser restart, and that storage can be cleared from
+ `browser://settings`, with bounded cross-tab, restart, and clear-IndexedDB
+ headed probes
+- that same headed IndexedDB path now also keeps basic object-store index
+ definitions and indexed lookups persistent across tabs and browser restart,
+ with focused DOM tests plus a bounded headed probe proving indexed entries
+ survive restart and can still be read back by index name and key
+- that same headed IndexedDB path now also supports object-store and index
+ cursor iteration, with focused DOM tests plus a bounded headed cross-tab
+ probe proving seeded cursor rows can be read back in sorted order from a
+ sibling tab through both `objectStore.openCursor()` and `index.openCursor()`
+- that same headed IndexedDB path now also exposes real transaction `mode`
+ state on the JS surface for `readonly` vs `readwrite` single-store
+ transactions, with focused DOM coverage plus a bounded headed probe proving
+ page JS can observe the expected mode values before a successful write
+- headed `fetch(...)` now honors credentials policy correctly on authenticated
+ pages, with bounded localhost probes proving:
+ - default same-origin fetch keeps cookie plus inherited auth
+ - `credentials: 'omit'` suppresses both cookie and auth
+ - cross-origin `same-origin` suppresses credentials
+ - cross-origin `include` sends cookies but not inherited auth
+- headed `Request` and `fetch(...)` now also honor `AbortSignal`, with focused
+ DOM tests proving `Request.signal` cloning plus immediate-abort rejection,
+ and a bounded headed probe proving an in-flight slow fetch aborts at runtime
+ with `AbortError` while the server observes the connection being cut
+- root `Content-Disposition: attachment` navigations now promote into the
+ headed download manager instead of degrading into navigation errors:
+ address-bar navigations, in-page link activations, and direct startup URLs
+ all enqueue real downloads, restore the suspended page when one exists, and
+ fall back to `browser://downloads` when there is no live page to restore
+- those same root attachment navigations now adopt the original response stream
+ directly into the headed download manager instead of aborting and issuing a
+ second GET, with bounded headed probes proving a single request for
+ address-bar, in-page link, and direct-startup attachment flows
+- headed Windows `browse` now has a native file chooser path for rendered file
+ inputs, including multi-select file inputs, plus real multipart form
+ submission with selected files and bounded headed probes for single-file
+ select-submit, cancel, replace, and multi-file submit flows
+- that same Win32 chooser path now derives native dialog file filters from
+ common `accept` hints (extensions plus common MIME and wildcard families)
+ instead of ignoring `accept` entirely, with focused helper coverage and full
+ headed upload regression sweeps
+- those same headed upload flows now compose cleanly with named popup targets
+ and attachment responses: bounded probes cover target-tab multipart upload,
+ same-context upload-to-attachment with restored source page plus downloads
+ page visibility, and target-tab upload-to-attachment with both managed
+ download capture and source-tab preservation
+- headed network image requests in the Win32 renderer now use the shared
+ browser `Http` runtime when available instead of the old URLMon-only path,
+ with a bounded localhost probe proving the image request carries
+ `User-Agent: Lightpanda/1.0` and renders successfully on the headed surface
+- those same headed network image requests now inherit page/session request
+ policy for cookies and referer, with a bounded localhost probe proving the
+ image request carries both the page cookie and the active page referer while
+ still rendering successfully on the headed surface
+- those same headed network image requests now also carry redirect-set cookies
+ through the shared `Http` runtime path, with a bounded localhost redirect
+ probe proving the final image request sends both the original page cookie and
+ the cookie set on the 302 hop before the image is rendered on the headed
+ surface
+- those same headed network image requests now distinguish credentialed and
+ anonymous fetch policy, with bounded localhost probes proving a credentialed
+ auth image still carries page cookie, referer, and URL-userinfo Basic
+ `Authorization`, while `crossorigin="anonymous"` suppresses both cookie and
+ auth header and still renders successfully on the headed surface
+- those same headed network image requests now identify themselves more like
+ real image subresources instead of generic fetches, with a bounded localhost
+ probe proving the shared-runtime request carries an explicit image `Accept`
+ header while still rendering successfully on the headed surface
+- same-origin protected subresources now inherit page-URL Basic auth on the
+ shared request-policy path without leaking URL userinfo through `Referer`,
+ with bounded localhost probes proving a relative headed image request and an
+ external script request both carry inherited auth, sanitized referer,
+ cookies, and that the authorized script actually executes afterward
+- those same connected external scripts now also distinguish credentialed vs
+ anonymous fetch policy, with bounded localhost probes proving a credentialed
+ script still carries cookie, sanitized referer, inherited auth, and executes
+ successfully, while `crossorigin="anonymous"` suppresses both cookie and
+ auth on the script request itself and still executes successfully on the
+ headed surface
+- connected external module scripts now ride the same shared request-policy
+ path for both root and child imports, with bounded localhost probes proving
+ credentialed and anonymous static module graphs carry the correct
+ cookie/referer/auth policy on both the root request and the child request,
+ and that the module graph executes successfully on the headed surface
+- connected `link rel=stylesheet` elements now load through the shared browser
+ `Http` runtime path, expose `link.sheet`, participate in
+ `document.styleSheets`, and carry page cookie, sanitized referer, inherited
+ auth, and an explicit stylesheet `Accept` header on protected same-origin
+ loads, with a bounded headed localhost probe proving the stylesheet request
+ succeeds and the page observes both `link.sheet` and stylesheet load
+ completion
+- those same connected external stylesheets now also distinguish credentialed
+ vs anonymous fetch policy, with a bounded headed localhost probe proving
+ `crossorigin="anonymous"` suppresses both cookie and auth while preserving
+ sanitized referer, stylesheet `Accept`, successful stylesheet load, and
+ computed-style application on the headed surface
+- those same connected internal and external stylesheets now populate
+ `cssRules` and feed the current `getComputedStyle` / headed painter path for
+ simple authored rules, with bounded tests and a headed localhost probe
+ proving a protected external stylesheet changes the computed page background
+ instead of only firing `load`
+- those same connected external stylesheet `@import` graphs now carry the
+ correct protected vs anonymous request policy on both the root stylesheet
+ request and the imported child stylesheet request, with bounded headed
+ localhost probes proving imported styles apply successfully in both modes
+- simple `@font-face` parsing and shared-runtime font fetches now ride that
+ same stylesheet-driven path, with `document.fonts` exposing loaded faces by
+ `size`, `status`, `check(...)`, and `load(...)`, and bounded headed
+ localhost probes proving protected and anonymous font requests carry the
+ correct cookie, sanitized referer, auth suppression or inheritance, explicit
+ font `Accept`, and loaded page state on the headed surface
+- headed Win32 text rendering now carries authored `font-family`,
+ `font-weight`, and `font-style` through the display list into real GDI font
+ selection for installed fonts, with a bounded screenshot probe proving the
+ headed surface produces materially different glyph widths for authored font
+ runs instead of always falling back to one generic face
+- those same stylesheet-backed `@font-face` entries now retain supported TTF
+ and OTF bytes, flow through the shared display-list presentation path, and
+ register as private Win32 fonts for headed text rendering, with a bounded
+ two-page localhost screenshot probe proving the same authored family renders
+ with materially different glyph widths when the private font is present vs
+ when the font URL is missing
+- those same private stylesheet-backed font flows now parse multi-source
+ `src:` lists with format hints and prefer a later renderable TTF/OTF
+ fallback over an earlier unsupported WOFF/WOFF2 source when present, with a
+ bounded headed screenshot probe proving a later truetype fallback still
+ affects the surface after an earlier missing `woff2` source
+- on-screen and offscreen canvas 2D contexts now keep real RGBA backing
+ stores for `fillRect`, `clearRect`, `strokeRect`, `getImageData`, and
+ `putImageData`, and headed Win32 `browse` now renders those canvas pixels on
+ the shared display-list path with a bounded screenshot probe proving the
+ rendered border, composited fill, and cleared interior
+- those same on-screen and offscreen canvas 2D contexts now also keep real
+ text state plus Win32-backed `fillText(...)` and `strokeText(...)`, with
+ focused DOM tests and a bounded headed screenshot probe proving red filled
+ and blue stroked glyph pixels reach the real destination canvas surface
+- those same canvas text paths now also expose real `measureText(...)`
+ `TextMetrics` objects backed by the same Win32 measurement path, with
+ focused DOM tests plus a bounded headed screenshot probe proving JS-sized
+ red and blue bars differ on the real surface when authored fonts differ
+- those same on-screen and offscreen canvas 2D contexts now also support a
+ first real `drawImage(...)` slice for `HTMLCanvasElement` and
+ `OffscreenCanvas` sources, including direct copy, simple scaling, and
+ source-rect cropping, with focused DOM tests plus a bounded headed Win32
+ screenshot probe proving copied red, blue, and green source pixels reach the
+ real destination canvas surface
+- those same on-screen and offscreen canvas 2D contexts now also support
+ `drawImage(HTMLImageElement, ...)`, with focused DOM tests plus a bounded
+ headed Win32 screenshot probe proving decoded red and cropped blue image
+ pixels reach the real destination canvas surface
+- those same canvas 2D contexts now also support a first real path slice for
+ `beginPath`, `moveTo`, `lineTo`, `rect`, `fill`, and `stroke`, with focused
+ DOM tests plus a bounded headed Win32 screenshot probe proving filled green
+ regions and blue stroke segments reach the real destination canvas surface
+- the headed Win32 canvas path now also includes a first real `webgl`
+ rendering-context slice for `clearColor(...)` plus `clear(COLOR_BUFFER_BIT)`,
+ with focused DOM tests plus a bounded headed Win32 screenshot probe proving a
+ full `120x80` clear-colored canvas region reaches the real destination
+ surface
+- that same headed Win32 `webgl` slice now also has a bounded runtime quality
+ gate for `drawingBufferWidth` / `drawingBufferHeight`, proving resized WebGL
+ buffer dimensions remain visible to page JS while a clear-colored surface
+ still reaches the headed screenshot path
+- that same headed Win32 `webgl` path now also includes a first real
+ shader/program/buffer draw slice for `createShader`, `shaderSource`,
+ `compileShader`, `createProgram`, `attachShader`, `linkProgram`,
+ `createBuffer`, `bufferData`, `vertexAttribPointer`, and
+ `drawArrays(TRIANGLES, ...)`, with focused DOM tests plus a bounded headed
+ screenshot probe proving a red triangle reaches the real destination canvas
+ surface
+- that same headed Win32 `webgl` path now also supports a first indexed-draw
+ and uniform-color slice for `getUniformLocation`, `uniform4f`,
+ `ELEMENT_ARRAY_BUFFER`, and `drawElements(TRIANGLES, ..., UNSIGNED_SHORT, ...)`,
+ with bounded headed screenshot coverage proving a uniform-colored indexed
+ triangle reaches the real destination canvas surface
+- that same headed Win32 `webgl` path now also supports a first varying-color
+ attribute slice with two enabled vertex attributes, interpolated per-vertex
+ color fill, and bounded headed screenshot coverage proving red, green, and
+ blue regions reach the real destination canvas surface from one triangle
+- the headed browser runtime now also exposes a first real `WebSocket`
+ browser-API slice with `CONNECTING` -> `OPEN` -> `CLOSED` state transitions,
+ `send`, `close`, `onopen`, `onmessage`, `onerror`, and `onclose`, with a
+ focused localhost DOM test plus a bounded headed echo probe proving text
+ frames round-trip on the live headed surface path
+- that same headed `WebSocket` runtime now also covers binary echo plus richer
+ close semantics through `binaryType`, binary `message` payloads, and
+ `CloseEvent` `code` / `reason` / `wasClean`, with a bounded headed localhost
+ probe proving binary frames round-trip and server-initiated close details
+ reach page JS on the live headed surface path
+- that same headed `WebSocket` runtime now also covers client-requested
+ subprotocol negotiation plus surfaced negotiated extensions, with a bounded
+ headed localhost probe proving a requested protocol list yields negotiated
+ `protocol === "superchat"`, `extensions === "permessage-test"`, binary
+ echo still works, and a clean client close reaches page JS correctly
+- the current headed painter now also keeps simple block paragraphs with mixed
+ direct text plus inline child elements on one shared inline row instead of
+ splitting the direct text into a separate label band above the inline chips,
+ with a bounded headed screenshot probe proving left-side paragraph text and
+ inline chips share the same content row
+- that same mixed-inline painter path now also keeps narrow wrapped mixed
+ inline paragraphs in one shared flow across multiple rows and treats `
`
+ as a real line break inside that flow, with bounded headed screenshot probes
+ proving wrapped chips/text stay in one content flow and the following
+ paragraph remains below the wrapped or broken inline content
+- wrapped mixed-inline anchors now also have bounded headed click coverage on a
+ lower wrapped fragment row, proving the lower-row link fragment still
+ navigates correctly after the inline-flow and wrapping changes
+- that same mixed-inline interaction coverage now also includes `
`-split
+ inline links and longer wrapped anchors with multiple later fragments, with
+ bounded headed probes proving navigation still works from those later visual
+ fragments instead of only from the first row
+- that same mixed-inline headed interaction path now also covers later-row
+ controls, with bounded probes proving a wrapped inline button still
+ activates from its lower row and a `
`-split inline text input still
+ focuses and accepts typed text from the later row on the headed surface
+- that same later-row mixed-inline control path now also keeps keyboard
+ behavior after focus, with bounded probes proving a wrapped inline button
+ can be re-activated with `Space` and a `
`-split inline text input can
+ submit its form on `Enter` from the headed surface
+- mixed control/link coexistence is now covered too, with bounded probes
+ proving a wrapped later-row button and a lower later-row link remain
+ independently usable in the same paragraph by both direct click and
+ button-focus `Tab` then `Enter` traversal
+- dense mixed-inline traversal is now covered as well, with a bounded probe
+ proving one wrapped paragraph can hand off focus from a later-row button to
+ a later-row input and then to a later-row link through `Tab` progression
+ while each target still performs its real headed action
+- that same mixed-inline later-row interaction coverage now also includes
+ checkbox/link coexistence, with a bounded probe proving a wrapped later-row
+ checkbox can be toggled by click and `Space`, and that `Tab` then `Enter`
+ still reaches and activates a later-row link in the same paragraph
+- that same mixed-inline later-row control coverage now also includes dense
+ checkbox/button/link coexistence, with a bounded probe proving one wrapped
+ paragraph can handle later-row checkbox click activation, later-row button
+ click activation, and then `Tab`/`Enter` traversal into a later-row link
+- that same later-row mixed-inline selection path now also includes radio/link
+ coexistence, with a bounded probe proving a wrapped later-row radio can be
+ selected by click and that `Tab` then `Enter` still reaches and activates a
+ later-row link in the same paragraph
+- that same dense later-row mixed-inline control coverage now also includes
+ radio/button/link coexistence, with a bounded probe proving one wrapped
+ paragraph can handle later-row radio click selection, later-row button click
+ activation, and then `Tab`/`Enter` traversal into a later-row link
+- that same dense later-row mixed-inline control coverage now also includes one
+ wrapped paragraph containing later-row checkbox, radio, button, and link
+ targets together, with a bounded probe proving click activation on the
+ checkbox, `Tab`+`Space` activation on the later-row radio and button, and
+ `Tab`+`Enter` traversal into the later-row link in DOM order
+- that same dense later-row mixed-inline control coverage now also includes a
+ wrapped same-family radio pair plus later-row button and link, with a
+ bounded probe proving click activation on the first radio, `Tab`+`Space`
+ selection of the second radio in the same group, then `Tab`+`Space` button
+ activation and `Tab`+`Enter` link navigation in DOM order
+- that same dense later-row mixed-inline control coverage now also includes a
+ wrapped same-family checkbox pair plus later-row button and link, with a
+ bounded probe proving click activation on the first checkbox, `Tab`+`Space`
+ activation on the second checkbox, then `Tab`+`Space` button activation and
+ `Tab`+`Enter` link navigation in DOM order
+- that same later-row mixed-inline same-family checkbox coverage now also
+ includes a wrapped form paragraph with a later-row submit control, with a
+ bounded probe proving click activation on the first checkbox, `Tab`+`Space`
+ activation on the second checkbox, and `Tab`+`Space` submission through the
+ real headed form-submit path
+- that same later-row mixed-inline same-family radio coverage now also
+ includes a wrapped form paragraph with a later-row submit control, with a
+ bounded probe proving click activation on the first radio, `Tab`+`Space`
+ selection of the second radio in the same group, and `Tab`+`Space`
+ submission through the real headed form-submit path
+- that same later-row mixed-inline same-family checkbox coverage now also
+ includes a wrapped form paragraph with a later-row text input before the
+ submit control, with a bounded probe proving click activation on the first
+ checkbox, `Tab`+`Space` activation on the second checkbox, `Tab`-driven text
+ entry into the later-row input, and `Tab`+`Space` submission through the
+ real headed form-submit path
+- that same later-row mixed-inline same-family radio coverage now also
+ includes a wrapped form paragraph with a later-row text input before the
+ submit control, with a bounded probe proving click activation on the first
+ radio, `Tab`+`Space` selection of the second radio in the same group,
+ `Tab`-driven text entry into the later-row input, and `Tab`+`Space`
+ submission through the real headed form-submit path
+- that same later-row mixed-inline form coverage now also includes a wrapped
+ mixed-family paragraph where checkbox, radio, text input, and submit coexist
+ in DOM order, with a bounded probe proving click activation on the checkbox,
+ `Tab`+`Space` activation on the later-row radio, typed input on the later-row
+ text field, and `Tab`+`Space` submission through the real headed form-submit
+ path
+- that same later-row mixed-inline form coverage now also includes a denser
+ wrapped mixed-family paragraph where a checkbox pair, a radio pair, text
+ input, and submit coexist in DOM order, with a bounded probe proving click
+ activation on the first checkbox, `Tab`+`Space` activation on the second
+ checkbox, first radio, and second radio, then typed input and real headed
+ form submission through the later-row submit control
+- that same later-row mixed-inline form coverage now also includes a further
+ dense wrapped mixed-family paragraph where a checkbox pair, a radio pair,
+ two text inputs, and submit coexist in DOM order, with a bounded probe
+ proving click activation on the first checkbox, `Tab`+`Space` activation on
+ the second checkbox, first radio, and second radio, then typed input through
+ both later-row text fields before real headed form submission through the
+ later-row submit control
+- that same later-row mixed-inline form coverage now also includes submit/link
+ coexistence after those dense controls, with bounded probes proving the same
+ wrapped paragraph can either reach a later-row link and navigate or continue
+ past that link to a later-row submit control and complete a real headed form
+ submission
+- that same later-row mixed-inline form coverage now also includes two distinct
+ later-row link targets before a later-row submit control, with bounded
+ probes proving the same dense wrapped paragraph can independently reach the
+ first link, reach the second link, or continue past both links to the later-
+ row submit control and complete a real headed form submission
+- that same later-row mixed-inline form coverage now also includes three
+ distinct later-row link targets before a later-row submit control, with
+ bounded probes proving the same dense wrapped paragraph can independently
+ reach the first link, second link, or third link, or continue past all three
+ links to the later-row submit control and complete a real headed form
+ submission
+- that same later-row mixed-inline form coverage now also includes four
+ distinct later-row link targets before a later-row submit control, with
+ bounded probes proving the same dense wrapped paragraph can independently
+ reach the first, second, third, or fourth link, or continue past all four
+ links to the later-row submit control and complete a real headed form
+ submission
+- that same later-row mixed-inline form coverage now also includes five
+ distinct later-row link targets before a later-row submit control, with
+ bounded probes proving the same dense wrapped paragraph can independently
+ reach the first, second, third, fourth, or fifth link, or continue past all
+ five links to the later-row submit control and complete a real headed form
+ submission
+- that same later-row mixed-inline form coverage now also includes six
+ distinct later-row link targets before a later-row submit control, with
+ bounded probes proving the same dense wrapped paragraph can independently
+ reach the first, second, third, fourth, fifth, or sixth link, or continue
+ past all six links to the later-row submit control and complete a real
+ headed form submission
+- that same later-row mixed-inline form coverage now also includes seven
+ distinct later-row link targets before a later-row submit control, with
+ bounded probes proving the same dense wrapped paragraph can independently
+ reach the first, second, third, fourth, fifth, sixth, or seventh link, or
+ continue past all seven links to the later-row submit control and complete a
+ real headed form submission
+- that same later-row mixed-inline form coverage now also includes eight
+ distinct later-row link targets before a later-row submit control, with
+ bounded probes proving the same dense wrapped paragraph can independently
+ reach the first, second, third, fourth, fifth, sixth, seventh, or eighth
+ link, or continue past all eight links to the later-row submit control and
+ complete a real headed form submission
+- next blocker: keep turning internal pages into richer live shell surfaces so
+ fewer browser-shell flows still depend on address-bar routes or secondary
+ overlay surfaces
+
+### Gate 2: Shared Subresource Loader And Profile
+
+Status: Active
+
+Goal:
+- move page assets and browser state onto a consistent browser-managed runtime
+
+Exit criteria:
+- images, connected stylesheets, scripts, fonts, and other subresources use the shared browser
+ network/client path
+- cookies, cache, auth, proxy, redirects, uploads, downloads, and persistent
+ profile storage behave consistently
+- file chooser and download manager flows exist
+- same-origin, CORS, CSP, mixed-content, and certificate error behavior are
+ coherent enough for mainstream browsing
+
+Current known gap entering Gate 2:
+- explicit download requests, adopted root-attachment transfers, and other
+ browser-managed resource flows still do not share one unified runtime path
+ for transfer ownership, persistence, and policy
+- headed `browse` now has one shared persistent cookie jar, origin-scoped
+ `localStorage` store, and origin-scoped IndexedDB store across tabs and
+ restart, with settings clear paths for all three, but broader persisted
+ profile state is still thin: cache policy and stronger profile persistence
+ beyond cookies/storage are still open
+- headed network images now ride the shared `Http` runtime path and inherit
+ page/session cookies, sanitized referer, redirect-set cookies, URL-userinfo
+ Basic Authorization, same-origin page-URL Basic auth inheritance, anonymous
+ credential suppression, and an explicit image `Accept` header, but broader
+ auth beyond page-URL Basic credentials and richer resource-type behavior are
+ still open
+- connected external scripts and static module imports now ride the shared
+ `Http` runtime path with inherited auth, sanitized referer, cookie policy,
+ explicit script `Accept`, and anonymous credential suppression coverage, but
+ broader script/resource parity and one unified subresource ownership path
+ are still open
+- connected `link rel=stylesheet` requests now ride the same shared `Http`
+ runtime path with `link.sheet` / `document.styleSheets` coverage and bounded
+ protected-load auth/cookie/referer/`Accept` verification plus anonymous
+ credential suppression, stylesheet body application now exists for the
+ current simple authored-rule path, and imported child stylesheets now keep
+ the same protected vs anonymous policy as the root request; stylesheet-
+ backed `@font-face` fetches and `document.fonts` now ride that same path,
+ and headed Win32 text rendering now honors both authored installed-font
+ family/style/weight and private TTF/OTF plus WOFF/WOFF2 stylesheet-backed
+ `@font-face` rendering on the surface, including later renderable
+ fallbacks in multi-source `src:` lists, but broader CSS fidelity, real
+ text shaping, wider font-format parity beyond WOFF/WOFF2, script/font/
+ resource parity, and one unified subresource ownership path are still open;
+ the current headed painter now also uses measured Win32 text extents
+ instead of pure character-count heuristics for text runs and inline/button
+ width decisions
+- native file chooser, multi-select file inputs, and multipart upload flows
+ now work end to end in headed Windows `browse`, but upload transport still
+ needs to converge with the same broader shared runtime/policy path as other
+ browser-managed resources
+- popup-target and attachment-response upload combinations are now runtime-
+ covered; the remaining work is less about basic composition and more about
+ converging those flows with the same broader shared transfer/runtime policy
+
+### Gate 3: Layout Engine Replacement
+
+Status: Active
+
+Goal:
+- replace the remaining dummy and heuristic layout paths with a real layout
+ engine
+
+Exit criteria:
+- block and inline formatting contexts behave predictably
+- flexbox support is usable on common sites
+- positioning, overflow, fixed/sticky basics, margin/padding/border handling,
+ and intrinsic sizing are implemented
+- form controls and replaced elements layout correctly in normal documents
+
+Current state inside Gate 3:
+- the first compatibility slice is landed for common real-site layout pressure:
+ safer selector failure containment, length resolution for `%`/`vw`/`vh` plus
+ `min(...)`/`max(...)`/`clamp(...)`, auto-margin centering, flex-column
+ centering, centered inline child flow, and absolute corner positioning
+- that same slice now also covers a first row-direction flex path with wrap,
+ `justify-content` spacing, and `align-items` vertical placement for common
+ chip/button-style rows, plus selector compatibility for `:lang(...)`,
+ `:dir(...)`, `:open`, and vendor `:-webkit-any-link` / `:-moz-any-link`
+- the headed painter now caches painted element layout boxes so
+ `getBoundingClientRect` and hit-testing can reuse painted geometry for
+ visible elements instead of falling back to the older sibling-position
+ heuristic
+- flex column layout now also respects authored `flex-grow`, `flex-shrink`,
+ `justify-content`, and `column-reverse`, with renderer tests plus bounded
+ headed probes proving the grow, justify, reverse, and stretch paths on the
+ real Win32 surface
+- headed screenshot export now waits for a real painted presentation with
+ positive painted height and real draw or interactive regions instead of
+ consuming the one-shot capture on the initial root placeholder frame, with a
+ bounded delayed-content probe proving async timer-driven page content reaches
+ the exported PNG
+- selector compatibility now also covers relative `:has(...)` combinators
+ (`>`, `+`, `~`, and descendant default) plus safer functional pseudo parsing
+ across quoted strings, bracketed attribute values, nested parentheses, and
+ top-level comma splitting
+- stylesheet rule application now also keeps valid selector-list branches when
+ a sibling branch is unsupported by the current engine, so common real-site
+ vendor or pseudo-element branches stop dropping the entire declarations block
+- fixed-position viewport anchoring now survives inline-content-flow containers
+ instead of being re-offset into the parent content box, and the computed
+ style path now defaults common controls and replaced elements like buttons,
+ inputs, selects, textareas, images, canvas, and iframes to `inline-block`
+ instead of `block`
+- flex row layout now also supports bounded item growth from `flex-grow` /
+ common `flex` shorthand handling, so header and search-bar style middle items
+ can expand between fixed siblings instead of staying at their intrinsic width
+- the remaining Gate 3 work is now centered on broader intrinsic sizing,
+ overflow interaction, stronger positioned/fixed behavior, and flex/table edge
+ cases on mainstream sites rather than the cache bridge itself
+- headed screenshot export now also refuses to capture while navigation is
+ still explicitly loading, with a bounded slow-image probe proving the export
+ waits for the real loaded image instead of the earlier pre-load placeholder
+- bounded headed probes now prove:
+ - Promise-microtask selector failures no longer kill the headed browser
+ - centered hero-style flex layouts reach the real Win32 surface
+ - absolute left/right corner docking plus later normal flow reach the real
+ Win32 surface
+ - centered wrapped flex-row content reaches the real Win32 surface across
+ multiple lines
+ - fixed left/right viewport docking survives inline-flow containers while
+ later normal flow stays below on the real Win32 surface
+ - forgiving stylesheet selector lists preserve the valid visual branch while
+ suppressing the duplicate invalid-branch artifact on the real Win32 surface
+ - flex-grow rows now expand the middle item between bounded red/blue siblings
+ on the real Win32 surface
+ - slow image loads are present before screenshot export succeeds
+ - delayed timer-driven content is present in the screenshot export path
+- legacy real-site compatibility moved another step forward:
+ - CSS shorthand expansion now lifts basic `background`, `border`, and
+ `font` declarations into the longhands the current headed painter actually
+ consumes
+ - native table-family elements plus legacy presentational HTML attributes now
+ produce a real centered table search layout instead of flattening into
+ generic block flow
+ - simple `float:left` / `float:right` docking now keeps later body flow below
+ the float band on the headed surface
+ - percentage child heights now resolve from an explicit ancestor height when
+ available instead of incorrectly expanding to the full viewport in common
+ cases like search-box inputs
+ - bounded headed probes now cover both the legacy centered table search shape
+ and left/right float docking, and a fresh live `google.com` capture no
+ longer crashes while visibly benefiting from the shorthand/background
+ compatibility slice
+- the next positioned/stability slice is now landed:
+ - later absolutely positioned siblings now anchor to the containing block
+ instead of being re-based off the evolving child flow cursor
+ - positioned boxes and interactive regions now carry effective `z-index`
+ order through both headed paint and headed hit-testing, so overlapping
+ later-row overlays can paint and click in the expected topmost order
+ - generic block descendants now paint after the ancestor background/border
+ phase instead of being hidden under later parent box fills on the shared
+ display-list path
+ - explicit CSS `height` no longer pollutes own-content height for generic
+ block containers, so normal-flow children stop being pushed down by the
+ full container box height
+ - headed screenshot export now also rejects tiny early paint noise instead of
+ treating a 1x1 placeholder draw as a ready frame
+ - bounded headed probes now cover absolute positioned overlap with topmost
+ click targeting plus the tiny-placeholder screenshot race in addition to
+ the earlier delayed-content screenshot gate
+- CSS background-image compatibility now also has a first real box-paint slice:
+ - inline `background:` shorthand now expands basic `url(...)`, repeat, and
+ position tokens into `background-image`, `background-repeat`, and
+ `background-position`
+ - headed background images now paint through the shared image request path
+ with box clipping plus `repeat-x`, `repeat-y`, and `no-repeat` tiling on
+ the Win32 surface instead of being ignored outright
+ - bounded tests and a headed sprite probe now prove repeated and non-repeated
+ background image boxes render with the expected offset and size on the real
+ screenshot path
+- box paint now also honors a practical uniform `border-radius` on headed
+ content boxes:
+ - fill and stroke rect commands now carry a corner radius through the shared
+ display-list path
+ - the Win32 headed surface now draws rounded fills and rounded borders for
+ nonzero radius boxes instead of flattening everything to square corners
+ - bounded tests plus a headed screenshot probe now prove a rounded pill box
+ clears its corners while an otherwise identical square box still fills its
+ corners on the real screenshot path
+- intrinsic replaced-element sizing and richer background sizing/positioning
+ are now covered on the headed surface:
+ - `img` layout now uses natural dimensions and aspect-ratio backfill when
+ width or height is omitted, instead of defaulting generic block boxes to
+ container width
+ - responsive `img` layout now clamps through `max-width` while preserving
+ aspect ratio on the real headed surface
+ - CSS background parsing no longer loses later declarations after an
+ unquoted `url(...)`, and background shorthand now lifts `background-size`
+ in addition to image/repeat/position
+ - headed background images now carry semantic `background-position`
+ modes for pixel offsets, keywords, and percentages instead of collapsing
+ everything into raw offsets
+ - headed background images now also honor first-pass `background-size`
+ semantics for `contain`, `cover`, explicit lengths, and percentages on the
+ Win32 surface instead of always painting at natural size
+ - bounded tests plus headed probes now cover intrinsic image sizing,
+ responsive image shrink-to-fit behavior, semantic background positioning,
+ and semantic background sizing on the real screenshot path
+ - `object-fit`, `object-position`, and `aspect-ratio` now ride the same
+ headed image path, with display-list preservation plus headed probes
+ proving fill/contain/cover/none/scale-down placement and explicit aspect
+ ratio backfill on the real screenshot path
+ - explicit `box-sizing: content-box` now expands headed block and control
+ boxes by their padding on the real surface instead of treating all used
+ sizes as border-box, with a focused display-list regression and headed
+ width/height probe proving the content-box expansion path
+ - `overflow:hidden` now clips both painted descendants and headed
+ interaction for block and flex containers, while generic block/flex
+ `height`, `min-height`, and `max-height` now affect the used box height
+ instead of acting like loose hints
+ - overflow:auto/scroll containers now track client/scroll metrics, clamp
+ scroll offsets, and headed wheel input scrolls the targeted element before
+ falling back to presentation scrolling
+ - `document.elementFromPoint(...)` now respects ancestor overflow clipping,
+ and headed Win32 input no longer synthesizes anchor/control clicks through
+ the DOM path when no rendered link/control region exists at that client
+ point
+ - stylesheet rule application now honors selector specificity and source
+ order, including inline-style precedence and `!important`, so nested tab
+ labels and legacy Google-style nav clusters keep the intended colors and
+ weights
+- the remaining gap is still large: this is a pragmatic compatibility slice,
+ not a full layout engine
+
+### Gate 4: Paint, Text, And Compositing
+
+Status: Planned
+
+Goal:
+- turn the current simple painter into a real browser rendering pipeline
+
+Exit criteria:
+- font loading and text shaping are good enough for mainstream sites
+- CSS backgrounds, borders, opacity, transforms, clipping, and stacking are
+ implemented at an MVP level
+- image rendering uses the browser resource pipeline
+- canvas, SVG, and screenshot fidelity materially improve
+- dirty-region invalidation avoids full-frame redraws for common interactions
+
+Current slice in progress:
+- the next paint slice should build on the cached layout-box bridge by
+ hardening border/background/stacking edge cases and broader dirty-region
+ invalidation, so mainstream pages like Google stay visually stable as more
+ layout states settle on the real Win32 surface
+
+### Gate 5: Editing, Forms, And App Interactivity
+
+Status: Planned
+
+Goal:
+- make normal website interaction feel dependable
+
+Exit criteria:
+- text selection, clipboard, caret movement, IME, drag/drop, and pointer
+ capture are stable
+- buttons, selects, checkboxes, radios, and file inputs behave correctly
+- contenteditable and common rich-text editing flows work to a practical level
+- keyboard shortcuts and accessibility-driven focus behavior are coherent
+
+### Gate 6: Modern Web Platform Coverage
+
+Status: Planned
+
+Goal:
+- reach enough platform compatibility for mainstream browsing, not just simple
+ pages
+
+Exit criteria:
+- robust fetch/XHR/WebSocket/navigation/history behavior
+- storage APIs needed by common apps are implemented and persistent
+- module/script loading and common JS integration paths are reliable
+- workers and other core async primitives cover representative real sites
+
+### Gate 7: Tabs, Session Management, And Recovery
+
+Status: Planned
+
+Goal:
+- make longer user sessions safe and practical
+
+Exit criteria:
+- persistent session restore
+- crash recovery and restart restore
+- per-tab loading/crash/error state
+- memory cleanup on tab close and navigation churn
+
+### Gate 8: Performance, Stability, And Security
+
+Status: Planned
+
+Goal:
+- raise the fork from experimental to something users can trust
+
+Exit criteria:
+- bounded memory/performance targets for long sessions
+- crash logging and reproducible issue reports
+- regression probes in CI for headed browsing on Windows
+- clear security posture for cookies, storage, network policy, and unsafe
+ content handling
+
+### Gate 9: Packaging And Production Readiness
+
+Status: Planned
+
+Goal:
+- ship a browser, not just a buildable developer project
+
+Exit criteria:
+- Windows installer/package and portable build
+- versioned releases and upgrade path
+- default profile directory and migration behavior
+- documentation for install, troubleshoot, and recover
+
+## Acceptance Bar For Production
+
+Treat the browser as production-ready only when all of these are true:
+
+- a normal user can install it and browse daily sites without needing CDP
+- common login, search, reading, download, upload, and form flows work
+- multi-tab browsing is stable for long sessions
+- rendering quality is good enough that users do not need another browser to
+ visually verify the page
+- core crash, stop, reload, navigation, and recovery paths are predictable
+
+## Working Rule For Future Milestones
+
+Prefer milestones that move the product from "experimental headed demo" toward
+"installable minimalist browser." If a change only helps automation but does
+not materially improve the browser product, it should usually rank below work
+that advances the gates above.
diff --git a/docs/HEADED_MODE_PRODUCTION_EXECUTION_GUIDE.md b/docs/HEADED_MODE_PRODUCTION_EXECUTION_GUIDE.md
new file mode 100644
index 000000000..66d039c64
--- /dev/null
+++ b/docs/HEADED_MODE_PRODUCTION_EXECUTION_GUIDE.md
@@ -0,0 +1,837 @@
+# Headed Mode Production Execution Guide
+
+This document is for an assistant working inside the Lightpanda headed fork.
+It is not a brainstorm. It is the execution order for taking the current fork
+from "headed foundation with many working slices" to "production-ready
+minimalist browser for real daily use".
+
+Read this together with:
+- `docs/FULL_BROWSER_MASTER_TRACKER.md`
+- `docs/HEADED_MODE_ROADMAP.md`
+- `docs/WINDOWS_FULL_USE.md`
+
+The branch to treat as product truth is:
+- `fork/headed-mode-foundation`
+
+## Product Bar
+
+The first production cut must satisfy all of these:
+- headed browsing on Windows is a first-class product mode, not a demo
+- normal users can browse common sites without relying on CDP automation
+- shell UX is stable: tabs, address bar, back/forward/reload/stop, downloads,
+ bookmarks, history, settings, session restore, crash recovery
+- rendered output is good enough for mainstream sites, not just localhost probes
+- storage and network policy are coherent across tabs and restart
+- screenshots use the same surface the user sees
+- headless and CDP continue to work and are not regressed by headed work
+- build and validation workflow self-recovers from known transient Windows/Zig
+ cache failures
+
+## Current Baseline
+
+Assume these are already in place unless a regression proves otherwise:
+- native Win32 headed window lifecycle
+- address bar, back, forward, reload, stop
+- tab strip, duplicate, reopen closed tab, session restore
+- internal `browser://start`, `browser://tabs`, `browser://history`,
+ `browser://bookmarks`, `browser://downloads`, and `browser://settings`
+- persisted cookies, localStorage, IndexedDB, downloads, bookmarks, settings,
+ telemetry/profile identity
+- file upload and attachment download promotion
+- image, stylesheet, script, font, and authenticated subresource loading
+- basic canvas 2D drawing plus first WebGL slices
+- bounded headed probe coverage across the existing `tmp-browser-smoke/` suites
+- Windows/MSVC build success for the fork
+
+Do not spend time re-solving those unless they are broken again.
+
+## Architecture Map
+
+Use these files as the primary ownership map:
+
+- App and runtime wiring
+ - `src/App.zig`
+ - `src/Config.zig`
+ - `src/main.zig`
+ - `src/lightpanda.zig`
+- profile and host-path handling
+ - `src/HostPaths.zig`
+- display backend and shell command bridge
+ - `src/display/Display.zig`
+ - `src/display/win32_backend.zig`
+ - `src/display/BrowserCommand.zig`
+- paint and presentation path
+ - `src/render/DisplayList.zig`
+ - `src/render/DocumentPainter.zig`
+- browser core and page/session behavior
+ - `src/browser/Browser.zig`
+ - `src/browser/Page.zig`
+ - `src/browser/EventManager.zig`
+- network/runtime plumbing
+ - `src/http/`
+ - `src/browser/webapi/net/`
+- HTML/CSS/DOM/Web APIs
+ - `src/browser/webapi/`
+ - `src/browser/webapi/element/html/`
+- canvas and graphics
+ - `src/browser/webapi/canvas/CanvasRenderingContext2D.zig`
+ - `src/browser/webapi/canvas/CanvasSurface.zig`
+ - `src/browser/webapi/canvas/OffscreenCanvas.zig`
+ - `src/browser/webapi/canvas/WebGLRenderingContext.zig`
+- smoke and acceptance probes
+ - `tmp-browser-smoke/`
+
+## Non-Negotiable Rules
+
+1. Keep headed rendering, screenshots, and hit-testing on one shared surface.
+ Do not add screenshot-only or probe-only rendering paths.
+2. When a site or probe fails, fix the shared engine path, not the single page.
+3. Every substantive slice must land with:
+ - a focused Zig test where possible
+ - a bounded headed probe for the real Win32 surface
+4. Preserve headless and CDP behavior.
+5. Do not delete `.lp-cache-win` for routine build recovery; it is expensive to
+ rebuild and usually not the real problem.
+6. Treat stale logs as stale until reproduced. Old `_link_trace.log` style
+ failures are not proof of the current blocker.
+
+## Delivery Checkpointing
+
+When the run is organized into fixed-deliverable slices:
+- after every verified batch of 25 completed deliverables, make a normal
+ commit and push it to the current fork branch head
+- do not wait for a much larger slice to finish before checkpointing
+- keep verification ahead of the commit and push so each checkpoint is a real
+ recovery point
+
+## Build Self-Recovery Routine
+
+If a Windows build times out or fails before real compiler diagnostics:
+
+1. Check for orphaned processes first.
+ - Inspect `zig`, `cargo`, `ninja`, `build.exe`, `cl`, `link`, and
+ `lld-link`.
+ - Kill only confirmed orphan PIDs.
+2. Capture fresh logs.
+ - `zig build -Dtarget=x86_64-windows-msvc --summary all 1> tmp-current-build.stdout.txt 2> tmp-current-build.stderr.txt`
+3. Classify.
+ - `failed to spawn build runner ... build.exe: FileNotFound` means default
+ `.zig-cache` corruption.
+ - `GetLastError(5): Access is denied` while Zig tries to spawn children
+ usually means an environment restriction, not a source break.
+ - only direct parser/type/linker diagnostics justify code edits
+4. Validate the toolchain separately.
+ - `zig build --help`
+ - a tiny direct `zig build-exe` probe
+ - retry with fresh cache dirs:
+ - `--cache-dir .zig-cache-recover`
+ - `--global-cache-dir .zig-global-cache-recover`
+5. If fresh-cache build works, recover normal operation with:
+ - `powershell -ExecutionPolicy Bypass -File .\scripts\windows\manage_build_artifacts.ps1 -CleanBuildCaches`
+6. Re-run the default build and continue only after it succeeds.
+
+Timeout budgets:
+- warm rebuild: about 5 minutes
+- cold/fresh-cache build: 15 to 20 minutes
+
+## Definite Execution Order
+
+Do the remaining work in this order. Do not jump ahead to packaging before the
+release gates below are genuinely green.
+
+### Phase 0: Build, Test, and Probe Discipline
+
+Objective:
+- make the fork routine to build, diagnose, and validate
+
+Primary files:
+- `scripts/windows/manage_build_artifacts.ps1`
+- `docs/WINDOWS_FULL_USE.md`
+- `build.zig`
+- probe scripts under `tmp-browser-smoke/`
+
+Tasks:
+- keep the build-cache recovery workflow documented and stable
+- standardize captured build logs for every long Windows build
+- convert the existing smoke directories into named gate suites, not ad hoc runs
+- separate warm-build expectations from cold-build expectations in docs
+- ensure the main validation runbook tells future assistants which probe family
+ to run for each subsystem change
+
+Exit criteria:
+- a future assistant can recover from corrupted `.zig-cache` without guessing
+- every major subsystem has a known bounded probe entry point
+- cold build timing no longer gets misclassified as a hang
+
+### Phase 1: Rendering and Composition Fidelity
+
+Objective:
+- make the visible headed surface reliable enough for mainstream sites
+
+Primary files:
+- `src/render/DocumentPainter.zig`
+- `src/render/DisplayList.zig`
+- `src/display/win32_backend.zig`
+- `src/browser/webapi/element/html/Image.zig`
+
+Tasks:
+- complete the remaining high-value CSS/layout fidelity gaps in the shared
+ display-list path
+- strengthen clipping, overflow, and compositing behavior for nested content
+- improve image placement, scaling, alpha, and transformed hit-testing parity
+- ensure caret, selection, focus rings, and control states remain visually
+ coherent during scroll and zoom
+- remove any remaining placeholder presentation behavior that is still visible
+ on real pages
+- prioritize failures that affect text-heavy pages, commerce/product pages, and
+ documentation sites before edge-case art demos
+
+Acceptance:
+- `tmp-browser-smoke/layout-smoke`
+- `tmp-browser-smoke/inline-flow`
+- `tmp-browser-smoke/flow-layout`
+- `tmp-browser-smoke/rendered-link-dom`
+- `tmp-browser-smoke/showcase`
+
+Exit criteria:
+- pages no longer depend on dummy layout/presentation behavior to remain usable
+- the visible surface is the same surface used by screenshots and hit-testing
+
+### Phase 2: Text, Fonts, Editing, and IME
+
+Objective:
+- make reading and typing feel native enough for real use
+
+Primary files:
+- `src/display/win32_backend.zig`
+- `src/render/DocumentPainter.zig`
+- `src/browser/webapi/element/html/Input.zig`
+- `src/browser/webapi/element/html/TextArea.zig`
+- `src/browser/webapi/element/html/Label.zig`
+
+Tasks:
+- finish the Windows text-input polish that docs already call out as incomplete:
+ IME candidate UI, composition edge cases, dead keys, and keyboard-layout
+ correctness
+- improve text measurement and font fallback for mixed-family real pages
+- validate selection, clipboard, caret movement, focus traversal, and form
+ editing against more realistic pages
+- keep headed text metrics consistent with canvas text metrics and DOM geometry
+- ensure zoom and DPI scaling do not break caret or text-control behavior
+
+Acceptance:
+- `tmp-browser-smoke/form-controls`
+- `tmp-browser-smoke/font-smoke`
+- `tmp-browser-smoke/font-render`
+- `tmp-browser-smoke/find`
+- `tmp-browser-smoke/zoom`
+
+Exit criteria:
+- users can reliably type, edit, paste, select, and navigate forms on the real
+ surface without input corruption or visual desync
+
+### Phase 3: Canvas and Graphics Completion
+
+Objective:
+- move from early canvas/WebGL slices to "common web graphics just work"
+
+Primary files:
+- `src/browser/webapi/canvas/CanvasRenderingContext2D.zig`
+- `src/browser/webapi/canvas/CanvasPath.zig`
+- `src/browser/webapi/canvas/CanvasSurface.zig`
+- `src/browser/webapi/canvas/OffscreenCanvas.zig`
+- `src/browser/webapi/canvas/WebGLRenderingContext.zig`
+- `src/browser/webapi/element/html/Canvas.zig`
+
+Known high-value gaps to close first:
+- `CanvasRenderingContext2D.zig` still carries no-op transforms:
+ `save`, `restore`, `scale`, `rotate`, `translate`, `transform`,
+ `setTransform`, `resetTransform`
+- `CanvasRenderingContext2D.zig` still carries no-op or partial path APIs:
+ `quadraticCurveTo`, `bezierCurveTo`, `arc`, `arcTo`, `clip`
+- `OffscreenCanvas.zig` still has stubbed `convertToBlob` and
+ `transferToImageBitmap`
+
+Tasks:
+- finish transform stack semantics and path rasterization needed by common chart
+ and editor libraries
+- complete clipping and compositing semantics used by canvases embedded in real
+ layouts
+- advance OffscreenCanvas enough for libraries that move rendering off the main
+ canvas object
+- continue WebGL from clear/basic triangle support to the minimum viable buffer,
+ shader, texture, and resize behavior needed by common UI/chart use
+- make canvas-backed hit-testing, screenshots, and paints stay consistent
+
+Acceptance:
+- `tmp-browser-smoke/canvas-smoke`
+- any new focused graphics probes added for transforms, clipping, and blob/image
+ export
+
+Exit criteria:
+- mainstream canvas/chart pages and simple WebGL pages render on the headed
+ surface without obvious placeholder behavior
+
+### Phase 4: DOM, CSS, and Web API Compatibility
+
+Objective:
+- close the high-frequency API gaps that cause real sites to branch away or fail
+
+Primary files:
+- `src/browser/webapi/Window.zig`
+- `src/browser/webapi/Performance.zig`
+- `src/browser/webapi/ResizeObserver.zig`
+- `src/browser/webapi/XMLSerializer.zig`
+- `src/browser/webapi/selector/`
+- `src/browser/webapi/navigation/`
+- `src/browser/webapi/element/html/`
+
+Known gaps worth prioritizing:
+- `Window.alert` is still exposed as a noop
+- `Performance` includes stub timing values
+- `ResizeObserver` is skeletal
+- `XMLSerializer` returns an empty structure
+- `Slot` and some shadow/DOM details are still placeholder-level
+
+Tasks:
+- prioritize APIs that modern frameworks use for layout, scheduling, routing,
+ and hydration
+- improve selector and DOM mutation correctness only where it changes site
+ behavior, not for abstract spec score alone
+- add the missing observer/performance/browser APIs required for real app shells
+- keep invalid input contained to the page rather than crashing headed mode
+
+Acceptance:
+- focused Zig tests per API family
+- real-site or localhost probes for framework shells that previously branched
+ away because an API was stubbed
+
+Exit criteria:
+- common app-shell frameworks stop failing on obvious missing API families
+
+### Phase 5: Networking, Navigation, and Security Correctness
+
+Objective:
+- make real page loading behavior coherent across tabs, restarts, and protected
+ resources
+
+Primary files:
+- `src/http/`
+- `src/browser/webapi/net/`
+- `src/browser/webapi/element/html/Link.zig`
+- `src/browser/webapi/element/html/Style.zig`
+- `src/browser/webapi/element/html/Image.zig`
+- `src/lightpanda.zig`
+
+Tasks:
+- continue tightening request policy for cookies, referer, auth, redirects, and
+ cache behavior across every subresource class
+- ensure navigation failure handling, attachment handling, and popup policy stay
+ correct under restart, back/forward, and retry flows
+- validate websocket, fetch-abort, credentialed fetch, stylesheet import, and
+ script/module behavior against realistic sequence timing
+- harden download/file-path isolation and shell-action safety
+- make sure profile persistence works identically whether the profile path is
+ absolute or repo-local relative
+
+Acceptance:
+- `tmp-browser-smoke/image-smoke`
+- `tmp-browser-smoke/stylesheet-smoke`
+- `tmp-browser-smoke/fetch-abort`
+- `tmp-browser-smoke/fetch-credentials`
+- `tmp-browser-smoke/websocket-smoke`
+- `tmp-browser-smoke/downloads`
+- `tmp-browser-smoke/attachment-downloads`
+
+Exit criteria:
+- cross-tab and restart behavior for network-backed features is deterministic
+- protected subresources and downloads behave like one browser, not a set of
+ unrelated demos
+
+### Phase 6: Shell, Profile, and Product Polish
+
+Objective:
+- turn the current shell into a product people can live in for long sessions
+
+Primary files:
+- `src/lightpanda.zig`
+- `src/display/BrowserCommand.zig`
+- `src/display/win32_backend.zig`
+- `src/HostPaths.zig`
+
+Tasks:
+- polish tab UX, keyboard shortcuts, disabled states, focus behavior, and
+ internal-page navigation flows
+- keep history/bookmarks/downloads/settings pages usable on long-lived profiles
+- harden crash recovery, startup restore, homepage behavior, and error-page
+ recovery
+- ensure native shell actions from downloads remain safe and predictable
+- stabilize profile-path behavior across manual overrides, relative paths, and
+ future packaged installs
+
+Acceptance:
+- `tmp-browser-smoke/browser-pages`
+- `tmp-browser-smoke/tabs`
+- `tmp-browser-smoke/settings`
+- `tmp-browser-smoke/popup`
+- `tmp-browser-smoke/file-upload`
+- `tmp-browser-smoke/manual-user`
+
+Exit criteria:
+- a user can browse, close, reopen, recover, download, and manage settings over
+ a long session without needing CDP or manual profile surgery
+
+### Phase 7: Reliability, Performance, and Crash Recovery
+
+Objective:
+- make the browser trustworthy for repeated daily use
+
+Primary files:
+- `src/App.zig`
+- `src/crash_handler.zig`
+- `src/lightpanda.zig`
+- `src/display/win32_backend.zig`
+- `src/render/DocumentPainter.zig`
+
+Tasks:
+- add memory, startup, and steady-state performance checkpoints
+- investigate leaks and unbounded growth across tabs, fonts, images, and canvas
+- make crash capture and restart recovery explicit, not accidental
+- audit long-session behavior for downloads, internal pages, and storage
+- add soak runs that open, reload, close, and restore tabs repeatedly
+
+Acceptance:
+- repeated headed probe loops without crash or runaway memory growth
+- a manual soak script for long-lived headed sessions
+
+Exit criteria:
+- the browser survives long sessions and repeated reopen cycles without obvious
+ degradation
+
+### Phase 8: Packaging, Install, and Release
+
+Objective:
+- ship the fork as a usable Windows browser build, not just a local developer
+ artifact
+
+Primary files:
+- `build.zig`
+- `docs/WINDOWS_FULL_USE.md`
+- packaging/release scripts added for this phase
+
+Tasks:
+- define install layout, default profile location, and user-visible data paths
+- package the executable and dependent runtime assets coherently
+- keep first-run experience, logging, and update instructions clear
+- document supported Windows version, known limitations, and fallback modes
+- turn the current tracker state into a release checklist with explicit gates
+
+Exit criteria:
+- a new user can install, launch, browse, and find their profile/downloads
+ without reading source code
+
+## Release Gate Matrix
+
+Do not call the fork production ready until these suites are green in headed
+mode on the release candidate build:
+
+- shell and navigation
+ - `tmp-browser-smoke/tabs`
+ - `tmp-browser-smoke/browser-pages`
+ - `tmp-browser-smoke/settings`
+ - `tmp-browser-smoke/wrapped-link`
+ - `tmp-browser-smoke/popup`
+- rendering and layout
+ - `tmp-browser-smoke/layout-smoke`
+ - `tmp-browser-smoke/inline-flow`
+ - `tmp-browser-smoke/font-render`
+ - `tmp-browser-smoke/image-smoke`
+- forms and file handling
+ - `tmp-browser-smoke/form-controls`
+ - `tmp-browser-smoke/file-upload`
+ - `tmp-browser-smoke/downloads`
+ - `tmp-browser-smoke/attachment-downloads`
+- storage and session
+ - `tmp-browser-smoke/cookie-persistence`
+ - `tmp-browser-smoke/localstorage-persistence`
+ - `tmp-browser-smoke/indexeddb-persistence`
+ - `tmp-browser-smoke/sessionstorage-scope`
+- network/runtime
+ - `tmp-browser-smoke/fetch-abort`
+ - `tmp-browser-smoke/fetch-credentials`
+ - `tmp-browser-smoke/websocket-smoke`
+ - `tmp-browser-smoke/stylesheet-smoke`
+- graphics
+ - `tmp-browser-smoke/canvas-smoke`
+
+Also require:
+- successful default-cache Windows build
+- successful fresh-cache Windows build
+- one long manual session run on a non-trivial real-site mix
+
+## Bare Metal Path
+
+The bare-metal path is the next deployment target after the headed Windows
+product path is green. It is not a separate browser. It is the same browser
+core compiled against a narrower host surface.
+
+Do not fork browser behavior. Move OS assumptions out to explicit platform
+services and keep the display list, browser pages, input model, and request
+policy shared.
+
+### Bare Metal Target Definition
+
+- Boot directly into the Lightpanda shell on a freestanding or firmware-style
+ target.
+- No Win32, no desktop shell, no implicit app-data directory, no dependence on
+ a host user profile.
+- Use the same browser pages and same headed presentation pipeline semantics.
+- Start on QEMU or an emulator image first. Hardware support comes after the
+ emulator path is stable.
+
+### Platform Seams
+
+- `src/App.zig`: stop assuming the platform is a desktop app with a process
+ profile directory.
+- `src/HostPaths.zig`: split filesystem-backed profile resolution from the
+ abstract notion of a browser profile root.
+- `src/display/Display.zig`: keep the backend boundary generic so Win32 and
+ bare-metal backends can share the same `DisplayList` contract.
+- `src/display/win32_backend.zig`: treat as the hosted reference backend, not
+ the product architecture.
+- `src/Net.zig` and `src/http/`: isolate sockets, timers, and I/O readiness
+ behind platform services.
+- `src/crash_handler.zig` and `src/log.zig`: support non-console sinks such as
+ serial output or a ring buffer.
+- `build.zig`: add a freestanding or bare-metal target class and select the
+ platform module at compile time.
+- `src/sys/`: grow the platform-specific services for framebuffer, input,
+ timers, persistent storage, and optional network glue.
+
+### Bare Metal Execution Order
+
+#### Phase 9: Platform Service Boundary
+
+Objective:
+- make the browser core compile against explicit host services instead of
+ Win32 or desktop assumptions
+
+Primary files:
+- `src/App.zig`
+- `src/HostPaths.zig`
+- `src/lightpanda.zig`
+- `src/display/Display.zig`
+- `src/Net.zig`
+- `build.zig`
+- `src/sys/`
+
+Tasks:
+- define a small host-service surface for:
+ - profile storage
+ - display surface
+ - input events
+ - clock and timers
+ - logging
+ - fatal exit / reboot hooks
+- move direct `std.fs` usage in persistence and startup flows behind the host
+ storage service
+- keep `Browser`, `Page`, `EventManager`, `DocumentPainter`, and
+ `DisplayList` free of firmware-specific code
+- add a mock host implementation so unit tests can run without a windowing
+ system
+- make sure the build system can select the hosted Windows backend or the
+ bare-metal backend without changing browser logic
+
+Acceptance:
+- the browser core compiles with the mock host
+- persistence and startup code paths are testable without Win32
+- the current Windows backend remains intact while the host seam is added
+
+Exit criteria:
+- the browser no longer assumes desktop process semantics in core code
+
+#### Phase 10: Boot and Presentation
+
+Objective:
+- get pixels and input on screen with the same shared display-list path
+
+Primary files:
+- `src/display/Display.zig`
+- `src/display/win32_backend.zig`
+- `src/render/DisplayList.zig`
+- `src/render/DocumentPainter.zig`
+- `src/browser/EventManager.zig`
+- `src/browser/Page.zig`
+- `src/browser/webapi/element/html/Canvas.zig`
+- `src/sys/`
+
+Tasks:
+- implement a bare-metal display backend that consumes `DisplayList` and paints
+ to a framebuffer or equivalent compositor target
+- implement a visible boot/loading state so startup failures are obvious
+- wire keyboard and pointer input into the same browser event path used by the
+ Windows backend
+- keep screenshots and visible output semantically identical to the headed
+ Win32 path
+- choose one initial boot stack and keep it narrow:
+ - QEMU or emulator framebuffer first
+ - then physical hardware
+- do not create a second renderer just for the boot path
+
+Acceptance:
+- boot lands in the browser shell
+- `browser://start` renders on the bare-metal surface
+- click, type, scroll, and tab switching work in the boot image
+- framebuffer screenshots are reproducible from the same presentation state
+
+Exit criteria:
+- the same browser UI can be driven on the bare-metal surface without desktop
+ dependencies
+
+#### Phase 11: Persistence and Networking
+
+Objective:
+- make browser state survive power loss and restart on a non-desktop target
+
+Primary files:
+- `src/HostPaths.zig`
+- `src/lightpanda.zig`
+- `src/browser/webapi/storage/`
+- `src/browser/webapi/net/`
+- `src/http/`
+- `src/browser/webapi/element/html/Link.zig`
+- `src/browser/webapi/element/html/Style.zig`
+- `src/browser/webapi/element/html/Image.zig`
+
+Tasks:
+- back profile data with a durable bare-metal store instead of host app-data
+ paths
+- preserve cookies, localStorage, IndexedDB, bookmarks, downloads, settings,
+ and session state across reboot
+- expose or emulate the minimum storage semantics needed by the existing
+ browser pages
+- bring up networking with the smallest viable path that supports HTTP, HTTPS,
+ redirects, cookies, and downloads
+- keep request policy identical to the Windows path for protected resources
+- make failures explicit when storage or network hardware is absent
+
+Acceptance:
+- profile state survives reboot
+- navigation, downloads, and storage-backed browser pages work after restart
+- protected subresource behavior matches the Windows path
+
+Exit criteria:
+- the browser can be used across power cycles without losing the user profile
+
+#### Phase 12: Boot Image and Release
+
+Objective:
+- produce a bootable browser image that can be tested and shipped
+
+Primary files:
+- `build.zig`
+- `docs/WINDOWS_FULL_USE.md`
+- packaging and release scripts added for this phase
+
+Tasks:
+- add boot-image packaging to the build or adjacent packaging scripts
+- define image layout, firmware assumptions, and required device support
+- document supported emulator or hardware classes, memory floor, input devices,
+ and network devices
+- create repeatable boot smoke scripts and artifact capture
+- keep the concrete packaging entry point at
+ `scripts/windows/package_bare_metal_image.ps1` and drive the launch smoke
+ from `tmp-browser-smoke/bare-metal-release/chrome-bare-metal-image-probe.ps1`
+ so a future assistant can reproduce the bundle without guessing
+- keep the release gate honest by also running
+ `tmp-browser-smoke/bare-metal-release/chrome-bare-metal-policy-probe.ps1`
+ against the localhost request-policy harness so the same browser path proves
+ both launch readiness and request-policy parity
+- keep the bare-metal restart path honest by also running
+ `tmp-browser-smoke/bare-metal-release/chrome-bare-metal-tabs-session-restore-probe.ps1`
+ so the packaged binary proves tab opening, switching, closing, and session
+ restore across a restart with the same profile directory
+- keep the profile persistence path honest by also running
+ `tmp-browser-smoke/bare-metal-release/chrome-bare-metal-persistence-probe.ps1`
+ so the packaged binary proves bookmarks, homepage, settings writes, and
+ restore-session changes survive a restart with the same profile directory
+- keep the storage-layer path honest by also running the cookie,
+ localStorage, and IndexedDB restart probes under
+ `tmp-browser-smoke/bare-metal-release/` so the packaged binary proves the
+ durable storage layer survives power loss for all three state buckets
+- use `zig build bare_metal_release -Dtarget=x86_64-windows-msvc -Dtarget_class=bare_metal`
+ as the canonical end-to-end validation command for the package-and-smoke
+ slice on Windows
+- expect the release bundle archive at
+ `tmp-browser-smoke/bare-metal-release.zip` and keep the smoke artifacts
+ under `tmp-browser-smoke/bare-metal-release/`
+- keep a fast recovery path for image boot failures
+
+Acceptance:
+- a bootable image launches the browser shell in an emulator or on hardware
+- the same smoke suites have a bare-metal execution mode
+- startup, navigation, and restart survive repeated boot cycles
+
+Exit criteria:
+- the bare-metal path can be delivered and reproduced without source edits
+
+### Bare Metal Release Gate
+
+Do not call the bare-metal path production ready until:
+- the image boots reliably on the chosen emulator and at least one target
+ hardware class
+- the browser shell is usable with keyboard and pointer input
+- profile state persists across reboot
+- the network path handles normal navigation and downloads, and the packaged
+ release smoke set includes `chrome-bare-metal-image-probe.ps1`,
+ `chrome-bare-metal-policy-probe.ps1`,
+ `chrome-bare-metal-download-probe.ps1`, and
+ `chrome-bare-metal-start-shell-probe.ps1`,
+ `chrome-bare-metal-tabs-session-restore-probe.ps1`, and
+ `chrome-bare-metal-persistence-probe.ps1`, plus the cookie, localStorage,
+ and IndexedDB persistence probes, including Ctrl+L address commit and
+ restart-persistence coverage for bookmarks, settings, and storage buckets
+- the same core browser pages work without Win32
+- boot and runtime failures are reproducible from saved logs
+
+### Bare Metal Validation Rule
+
+Every bare-metal slice must end with:
+1. a compile check for the bare-metal target or the nearest supported host
+ equivalent
+2. the relevant unit tests for the modules that changed
+3. the relevant smoke/probe suite for the surface that changed
+4. saved logs or artifacts for any failure
+
+Do not move to the next bare-metal layer until the current layer compiles and
+its validation passes.
+
+### Bare Metal Module Split
+
+Keep the bare-metal host code in `src/sys/` and related build glue, not in the
+browser core.
+
+Split the first bring-up into these host modules:
+- `src/sys/boot.zig` for startup, panic routing, and shutdown
+- `src/sys/framebuffer.zig` for the pixel surface and screenshot capture
+- `src/sys/input.zig` for keyboard and pointer event ingestion
+- `src/sys/timer.zig` for monotonic time, sleeps, and animation pacing
+- `src/sys/storage.zig` for profile persistence and file emulation
+- `src/sys/net.zig` for the transport and socket/driver shim
+- `src/sys/serial_log.zig` for log output when no desktop console exists
+
+Rules:
+- browser code never talks to drivers directly
+- the display backend consumes a generic surface, not a boot-specific API
+- persistence goes through the storage service, not raw block I/O in browser
+ code
+- request policy stays in the shared HTTP/browser layers; only the transport
+ changes
+- the boot module owns the initial run loop and the last-resort failure path
+
+### Bare Metal Smoke Order
+
+Bring up the image in this order:
+1. boot banner plus panic output
+2. framebuffer fill and browser chrome paint
+3. keyboard focus, text entry, and mouse click delivery
+4. `browser://start` and `browser://tabs`
+5. profile save/restore across a restart
+6. network navigation to a localhost smoke page
+7. downloads and storage-backed browser pages
+8. one real-site fetch with the same request policy as Windows
+
+If a step fails, fix the earliest missing layer. Do not debug later layers
+before the current one is stable.
+
+### Bare Metal Batch 1: 25 Deliverables
+
+This is the first concrete bare-metal checkpoint batch. Complete the items in
+order. Do not skip ahead. Every item still follows the Bare Metal Validation
+Rule, and the whole batch ends with the commit-and-push checkpoint rule.
+
+1. Add `src/sys/host.zig` with the canonical host-service interface for
+ storage, display, input, timer, logging, and power control. Exit check: the
+ browser core compiles against the interface without Win32 imports in core
+ files.
+2. Add a mock host implementation for unit tests. Exit check: startup and
+ persistence tests run without a windowing system or firmware target.
+3. Refactor `src/App.zig` to accept host services instead of assuming desktop
+ process semantics. Exit check: app startup no longer depends on implicit
+ app-data or desktop globals.
+4. Add `src/sys/boot.zig` for startup, panic routing, and shutdown. Exit check:
+ boot code can emit a visible failure and terminate cleanly.
+5. Add `src/sys/serial_log.zig` for non-console logging. Exit check: fatal
+ errors can be captured on a serial sink or ring buffer.
+6. Add `src/sys/timer.zig` for monotonic time, sleeps, and pacing. Exit check:
+ animation and event loops can advance without `std.time` calls in browser
+ core code.
+7. Add `src/sys/input.zig` for keyboard and pointer ingestion. Exit check: a
+ platform test can inject keypress and click events deterministically.
+8. Add `src/sys/framebuffer.zig` for pixel output and screenshot capture.
+ Exit check: a framebuffer test can draw and read back pixels.
+9. Add `src/sys/storage.zig` for profile root resolution and durable file
+ operations. Exit check: profile files can be created, reopened, and
+ enumerated on the mock host.
+10. Add `src/sys/net.zig` for transport and socket glue. Exit check: the
+ network shim can be swapped without changing browser request policy code.
+11. Extend `build.zig` with a target-class switch for hosted vs bare-metal
+ builds. Exit check: both target classes can be selected without editing
+ browser logic.
+12. Refactor `src/HostPaths.zig` to use profile file and subdir helpers for
+ bare-metal-safe path resolution. Exit check: profile file paths resolve in
+ tests and on the mock host.
+13. Replace direct profile-directory assumptions in `src/lightpanda.zig` with
+ host-safe directory helpers. Exit check: cookies, settings, session,
+ bookmarks, and downloads still round-trip under the hosted backend.
+14. Add compile tests that instantiate browser core types with the mock host.
+ Exit check: `Browser`, `Page`, `EventManager`, `DisplayList`, and
+ `DocumentPainter` compile without Win32-specific branches.
+15. Add the bare-metal display backend skeleton that consumes `DisplayList`.
+ Exit check: the backend can accept a list and paint a trivial frame.
+16. Add a visible boot and loading state. Exit check: startup failures are
+ obvious on the framebuffer instead of vanishing into a black screen.
+17. Wire keyboard focus and text entry through the bare-metal input path. Exit
+ check: address bar focus and text input work in the shell.
+18. Wire pointer input through the bare-metal input path. Exit check: links and
+ controls are activatable with a pointer.
+19. Bring up `browser://start` on the bare-metal surface. Exit check: the
+ start page renders and responds to interaction.
+20. Bring up `browser://tabs` and tab switching on the bare-metal surface.
+ Exit check: opening, switching, and closing tabs work across a restart.
+21. Back profile persistence for bookmarks, settings, and session state with
+ the durable storage layer. Exit check: the data survives a restart in the
+ emulator.
+22. Back cookies, localStorage, and IndexedDB with the durable storage layer.
+ Exit check: storage-backed browser pages still work after power loss.
+23. Implement the minimal bare-metal HTTP and HTTPS navigation path. Exit
+ check: localhost navigation and
+ `tmp-browser-smoke/bare-metal-release/chrome-bare-metal-download-probe.ps1`
+ pass on the emulator.
+24. Verify one real-site fetch on bare metal with the same request policy as
+ Windows. Exit check: the same policy gates content on both hosted and
+ bare-metal paths.
+25. Package a bootable image and emulator launch smoke with artifact capture.
+ Exit check: a repeatable image launch can be driven without source edits.
+
+## Anti-Patterns
+
+Do not do these:
+- do not add probe-only rendering behavior that real pages cannot use
+- do not special-case one site if the underlying engine path is still wrong
+- do not treat old logs as current truth
+- do not mark a phase done because a single localhost probe turned green
+- do not delete dependency caches to solve a transient local Zig cache issue
+
+## Definition Of Done
+
+The fork is production ready only when:
+- the default Windows headed build is routine and self-recovering
+- the shell is stable for long daily sessions
+- common sites render and interact correctly enough for ordinary use
+- the existing smoke matrix is green and organized as a release gate
+- the remaining obvious API stubs no longer drive common sites off the happy
+ path
+- the product can be installed and used by someone who is not inside the repo
+- the bare-metal path has a bootable browser image, persistent profile, working
+ input and networking, and its own reproducible smoke gates
diff --git a/docs/HEADED_MODE_ROADMAP.md b/docs/HEADED_MODE_ROADMAP.md
new file mode 100644
index 000000000..e0f81d344
--- /dev/null
+++ b/docs/HEADED_MODE_ROADMAP.md
@@ -0,0 +1,55 @@
+# Headed Mode Roadmap (Fork)
+
+This fork targets full headed-mode browser usage while preserving Lightpanda's
+current headless strengths.
+
+For the full product plan from the current headed foundation to a
+production-ready minimalist Zig browser, see
+`docs/FULL_BROWSER_MASTER_TRACKER.md`.
+
+## Current Status
+
+- `--browser_mode headless|headed` is now accepted.
+- `--headed` and `--headless` shortcuts are available.
+- On Windows targets, `headed` now starts a native window lifecycle backend.
+- On non-Windows targets, `headed` still uses a safe headless fallback with warning.
+- `--window_width` / `--window_height` now drive window/screen/viewport values.
+- Display runtime abstraction exists with page lifecycle hooks and a Win32 thread backend.
+- CDP viewport APIs update runtime viewport (`Emulation.*Metrics*`, `Browser.setWindowBounds`).
+- Win32 headed backend now forwards native mouse (down/up/move/wheel/hwheel), click, keydown/keyup, text input (`WM_CHAR`/`WM_UNICHAR`), IME result/preedit composition messages (`WM_IME_COMPOSITION`), back/forward mouse buttons, and window blur events into page input.
+- Win32 headed backend now propagates native key repeat state into `KeyboardEvent.repeat`.
+- Text control editing now includes caret-aware insertion paths, `Ctrl/Meta + A` select-all, word-wise keyboard edit/navigation shortcuts, textarea vertical/line navigation, `Tab`/`Shift+Tab` focus traversal with `tabindex` ordering, and native clipboard shortcuts (`Ctrl/Meta + C/X/V`, `Ctrl+Insert`, `Shift+Insert`, `Shift+Delete`) with cancelable clipboard event dispatch.
+- Windows prereq checker + runbook added (`scripts/windows`, `docs/WINDOWS_FULL_USE.md`).
+
+## Milestones
+
+1. Display abstraction
+- Introduce a renderer backend interface with a no-op backend and a
+ real windowed backend (Win32 lifecycle backend implemented).
+- Keep DOM, JS, networking, and CDP independent from the window backend.
+
+2. Window lifecycle
+- Implement window creation, resize, close, and frame pump.
+- Wire browser/page lifecycle events to the display backend.
+
+3. Layout and paint pipeline
+- Build incremental layout + paint passes from DOM/CSS state.
+- Add dirty-region invalidation to avoid full-frame redraws.
+
+4. Input + event synthesis
+- Convert OS input events (mouse/keyboard/wheel/focus) into DOM events.
+- Keep CDP input paths consistent with native input behavior.
+
+5. Screenshots and surfaces
+- Expose pixel surfaces for screenshots/recording while in headed mode.
+- Ensure parity between headless and headed screenshot semantics.
+
+6. Stabilization
+- Add headed integration tests (window lifecycle, input, rendering, resize).
+- Validate performance, memory, and crash-handling budgets.
+
+## Design Constraints
+
+- No regressions to existing headless CLI/CDP behavior.
+- Feature flags must keep partial implementations safe.
+- Keep platform-specific code isolated behind backend boundaries.
diff --git a/docs/WINDOWS_FULL_USE.md b/docs/WINDOWS_FULL_USE.md
new file mode 100644
index 000000000..b6b3de122
--- /dev/null
+++ b/docs/WINDOWS_FULL_USE.md
@@ -0,0 +1,65 @@
+# Lightpanda Full Use on Windows (Fork)
+
+This fork now has:
+
+- Runtime browser mode switch (`--browser_mode headless|headed`)
+- Runtime viewport controls (`--window_width`, `--window_height`)
+- CDP viewport controls (`Emulation.setDeviceMetricsOverride`, `Emulation.clearDeviceMetricsOverride`, `Browser.setWindowBounds`)
+
+## 1) Check Windows prerequisites
+
+Run:
+
+```powershell
+powershell -ExecutionPolicy Bypass -File .\scripts\windows\check_lightpanda_windows_prereqs.ps1
+```
+
+If `SymlinkCreate` fails, enable Windows Developer Mode and reopen your shell.
+Without symlink capability, Zig dependency unpacking can fail (`depot_tools`).
+`DeveloperMode` can still show `FAIL` if symlink creation already works in your
+current shell context.
+
+## 2) Build options
+
+1. Native Windows build:
+- Works only when symlink creation is available in the current shell.
+- Then run normal build commands (for example `zig build run -- help`).
+
+2. WSL build (recommended fallback):
+- Build and run from WSL where symlink behavior is reliable.
+- Connect automation clients from Windows host to the WSL endpoint.
+
+## 3) Runtime usage examples
+
+CLI:
+
+```powershell
+.\lightpanda.exe serve --browser_mode headed --window_width 1366 --window_height 768 --host 127.0.0.1 --port 9222
+```
+
+CDP viewport override:
+
+- `Emulation.setDeviceMetricsOverride`
+- `Emulation.clearDeviceMetricsOverride`
+- `Browser.setWindowBounds` with width/height
+
+## 4) Current headed status
+
+`headed` mode now has a native Windows window lifecycle backend:
+
+- window open/close with page lifecycle
+- native Win32 message pump on a dedicated thread
+- viewport resize wiring from CLI and CDP metrics/window-bounds APIs
+- native mouse (down/up/move/wheel/hwheel), click, keydown/keyup, text input (`WM_CHAR`/`WM_UNICHAR`), IME result/preedit composition messages (`WM_IME_COMPOSITION`), back/forward mouse buttons, and window blur wired into page input handling
+- native key repeat state is propagated to `KeyboardEvent.repeat`
+- text controls now keep insertion at the active caret/selection and support `Ctrl/Meta + A` select-all
+- text controls also support word-wise keyboard editing (`Ctrl/Meta + ArrowLeft/ArrowRight`, `Ctrl/Meta + Backspace/Delete`)
+- textareas now support vertical and line-aware caret movement (`ArrowUp/ArrowDown`, line-aware `Home/End`, document `Ctrl/Meta + Home/End`)
+- keyboard focus traversal now supports `Tab` / `Shift+Tab` with `tabindex` ordering
+- native clipboard shortcuts are wired for text controls (`Ctrl/Meta + C/X/V`, `Ctrl+Insert`, `Shift+Insert`, `Shift+Delete`)
+- clipboard shortcuts dispatch cancelable `copy`/`cut`/`paste` events and respect `preventDefault()`
+
+Graphical rendering and native input translation are still in-progress:
+
+- frame presentation pipeline
+- IME candidate/composition UI and dead-key edge cases
diff --git a/scripts/windows/check_lightpanda_windows_prereqs.ps1 b/scripts/windows/check_lightpanda_windows_prereqs.ps1
new file mode 100644
index 000000000..56808feea
--- /dev/null
+++ b/scripts/windows/check_lightpanda_windows_prereqs.ps1
@@ -0,0 +1,77 @@
+Set-StrictMode -Version Latest
+$ErrorActionPreference = "Stop"
+
+function Write-Status {
+ param(
+ [string]$Name,
+ [bool]$Ok,
+ [string]$Details
+ )
+ $mark = if ($Ok) { "PASS" } else { "FAIL" }
+ Write-Host ("[{0}] {1} - {2}" -f $mark, $Name, $Details)
+}
+
+$allOk = $true
+
+# 1) Developer mode (enables non-admin symlink creation on many setups)
+$devModeKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock"
+$devModeValue = $null
+try {
+ $devModeValue = (Get-ItemProperty -Path $devModeKey -Name AllowDevelopmentWithoutDevLicense -ErrorAction Stop).AllowDevelopmentWithoutDevLicense
+} catch {
+ $devModeValue = 0
+}
+$devModeEnabled = ($devModeValue -eq 1)
+Write-Status "DeveloperMode" $devModeEnabled ("AllowDevelopmentWithoutDevLicense={0}" -f $devModeValue)
+
+# 2) Symlink capability test
+$symlinkOk = $false
+$tmpRoot = Join-Path $env:TEMP ("lightpanda_symlink_test_{0}" -f [Guid]::NewGuid().ToString("N"))
+try {
+ New-Item -ItemType Directory -Path $tmpRoot -Force | Out-Null
+ $target = Join-Path $tmpRoot "target.txt"
+ $link = Join-Path $tmpRoot "link.txt"
+ Set-Content -Path $target -Value "ok" -Encoding UTF8
+ New-Item -ItemType SymbolicLink -Path $link -Target $target -ErrorAction Stop | Out-Null
+ $symlinkOk = $true
+} catch {
+ $symlinkOk = $false
+} finally {
+ Remove-Item -Path $tmpRoot -Recurse -Force -ErrorAction SilentlyContinue
+}
+Write-Status "SymlinkCreate" $symlinkOk "Create symbolic links in current shell"
+if (-not $symlinkOk) { $allOk = $false }
+
+# 3) Zig
+$zigVersion = $null
+try {
+ $zigVersion = (zig version).Trim()
+} catch {
+ $zigVersion = $null
+}
+$zigOk = ($null -ne $zigVersion -and $zigVersion.Length -gt 0)
+$zigDetails = if ($zigOk) { "zig {0}" -f $zigVersion } else { "zig not found in PATH" }
+Write-Status "Zig" $zigOk $zigDetails
+if (-not $zigOk) { $allOk = $false }
+
+# 4) WSL availability (recommended fallback workflow)
+$wslOk = $false
+try {
+ $null = wsl.exe --status 2>$null
+ $wslOk = $true
+} catch {
+ $wslOk = $false
+}
+$wslDetails = if ($wslOk) { "wsl.exe available" } else { "wsl.exe not available" }
+Write-Status "WSL" $wslOk $wslDetails
+
+if ($allOk) {
+ Write-Host ""
+ Write-Host "Windows prerequisites look good for local Lightpanda development."
+ exit 0
+}
+
+Write-Host ""
+Write-Host "One or more required prerequisites failed."
+Write-Host "See docs/WINDOWS_FULL_USE.md for remediation."
+exit 1
diff --git a/scripts/windows/manage_build_artifacts.ps1 b/scripts/windows/manage_build_artifacts.ps1
new file mode 100644
index 000000000..6b2565039
--- /dev/null
+++ b/scripts/windows/manage_build_artifacts.ps1
@@ -0,0 +1,158 @@
+param(
+ [string]$RepoRoot = "C:\Users\adyba\src\lightpanda-browser",
+ [switch]$CleanBuildCaches,
+ [switch]$CleanDependencyCaches,
+ [switch]$CleanSliceOutputs
+)
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = "Stop"
+
+function Get-PathSizeBytes {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$Path
+ )
+
+ if (-not (Test-Path -LiteralPath $Path)) {
+ return [int64]0
+ }
+
+ $sum = [int64]0
+ foreach ($file in (Get-ChildItem -LiteralPath $Path -Recurse -Force -File -ErrorAction SilentlyContinue)) {
+ $sum += [int64]$file.Length
+ }
+ return $sum
+}
+
+function Get-FileSizeBytes {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$Path
+ )
+
+ if (-not (Test-Path -LiteralPath $Path)) {
+ return [int64]0
+ }
+
+ return [int64](Get-Item -LiteralPath $Path -Force).Length
+}
+
+function Format-Size {
+ param(
+ [Parameter(Mandatory = $true)]
+ [int64]$Bytes
+ )
+
+ if ($Bytes -ge 1TB) { return "{0:N2} TB" -f ($Bytes / 1TB) }
+ if ($Bytes -ge 1GB) { return "{0:N2} GB" -f ($Bytes / 1GB) }
+ if ($Bytes -ge 1MB) { return "{0:N2} MB" -f ($Bytes / 1MB) }
+ if ($Bytes -ge 1KB) { return "{0:N2} KB" -f ($Bytes / 1KB) }
+ return "{0} B" -f $Bytes
+}
+
+function New-ArtifactRecord {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$Path,
+ [Parameter(Mandatory = $true)]
+ [string]$Category,
+ [Parameter(Mandatory = $true)]
+ [bool]$DefaultClean,
+ [Parameter(Mandatory = $true)]
+ [string]$Reason,
+ [Parameter(Mandatory = $true)]
+ [ValidateSet("directory", "file")]
+ [string]$Kind
+ )
+
+ $exists = Test-Path -LiteralPath $Path
+ $bytes = if (-not $exists) {
+ [int64]0
+ } elseif ($Kind -eq "file") {
+ Get-FileSizeBytes -Path $Path
+ } else {
+ Get-PathSizeBytes -Path $Path
+ }
+
+ [pscustomobject]@{
+ Path = $Path
+ Name = Split-Path -Path $Path -Leaf
+ Kind = $Kind
+ Category = $Category
+ DefaultClean = $DefaultClean
+ Exists = $exists
+ SizeBytes = $bytes
+ Size = Format-Size -Bytes $bytes
+ Reason = $Reason
+ }
+}
+
+function Remove-Artifact {
+ param(
+ [Parameter(Mandatory = $true)]
+ [pscustomobject]$Artifact
+ )
+
+ if (-not $Artifact.Exists) {
+ return
+ }
+
+ if ($Artifact.Kind -eq "directory") {
+ Remove-Item -LiteralPath $Artifact.Path -Recurse -Force
+ } else {
+ Remove-Item -LiteralPath $Artifact.Path -Force
+ }
+}
+
+$artifacts = @(
+ (New-ArtifactRecord -Path (Join-Path $RepoRoot ".zig-cache") -Category "Build cache" -DefaultClean $true -Reason "Transient Zig local cache. Safe to delete; next build will be cold." -Kind "directory"),
+ (New-ArtifactRecord -Path (Join-Path $RepoRoot ".zig-cache-next") -Category "Build cache" -DefaultClean $true -Reason "Alternate transient Zig cache from local slice builds. Safe to delete." -Kind "directory"),
+ (New-ArtifactRecord -Path (Join-Path $RepoRoot "zig-out-slice") -Category "Slice output" -DefaultClean $true -Reason "Local slice output directory. Regenerated by slice builds." -Kind "directory"),
+ (New-ArtifactRecord -Path (Join-Path $RepoRoot "lightpanda-slice.exe") -Category "Slice output" -DefaultClean $true -Reason "Manual local slice binary. Safe to delete if not in use." -Kind "file"),
+ (New-ArtifactRecord -Path (Join-Path $RepoRoot "lightpanda-slice.pdb") -Category "Slice output" -DefaultClean $true -Reason "Debug symbols for manual local slice binary." -Kind "file"),
+ (New-ArtifactRecord -Path (Join-Path $RepoRoot "lightpanda-slice.lib") -Category "Slice output" -DefaultClean $true -Reason "Import library for manual local slice binary." -Kind "file"),
+ (New-ArtifactRecord -Path (Join-Path $RepoRoot "tmp-browser-smoke\bare-metal-release") -Category "Slice output" -DefaultClean $true -Reason "Packaged bare-metal launch bundle and its smoke artifacts." -Kind "directory"),
+ (New-ArtifactRecord -Path (Join-Path $RepoRoot "tmp-browser-smoke\bare-metal-release.zip") -Category "Slice output" -DefaultClean $true -Reason "Packaged bare-metal release archive." -Kind "file"),
+ (New-ArtifactRecord -Path (Join-Path $RepoRoot ".lp-cache") -Category "Dependency cache" -DefaultClean $false -Reason "Bootstrap cache for downloaded tools/dependencies. Deleting is safe but costly to regenerate." -Kind "directory"),
+ (New-ArtifactRecord -Path (Join-Path $RepoRoot ".lp-cache-win") -Category "Dependency cache" -DefaultClean $false -Reason "Windows V8/depot_tools cache. Large and expensive to rebuild." -Kind "directory"),
+ (New-ArtifactRecord -Path (Join-Path $RepoRoot "zig-out") -Category "Primary output" -DefaultClean $false -Reason "Current compiled outputs. Usually keep unless you want a full artifact reset." -Kind "directory")
+)
+
+Write-Host "Lightpanda build artifact report"
+Write-Host ""
+$artifacts |
+ Sort-Object @{ Expression = "SizeBytes"; Descending = $true }, Name |
+ Select-Object Name,Category,Exists,DefaultClean,Size,Reason |
+ Format-Table -AutoSize | Out-String -Width 220 |
+ Write-Host
+
+$toRemove = @()
+if ($CleanBuildCaches) {
+ $toRemove += $artifacts | Where-Object { $_.Exists -and $_.Category -eq "Build cache" }
+}
+if ($CleanSliceOutputs) {
+ $toRemove += $artifacts | Where-Object { $_.Exists -and $_.Category -eq "Slice output" }
+}
+if ($CleanDependencyCaches) {
+ $toRemove += $artifacts | Where-Object { $_.Exists -and $_.Category -eq "Dependency cache" }
+}
+
+$toRemove = @($toRemove | Sort-Object Path -Unique)
+
+if ($toRemove.Count -eq 0) {
+ Write-Host "No cleanup requested. Use -CleanBuildCaches, -CleanSliceOutputs, and/or -CleanDependencyCaches."
+ exit 0
+}
+
+$totalBytes = ($toRemove | Measure-Object -Property SizeBytes -Sum).Sum
+Write-Host ""
+Write-Host ("Deleting {0} artifact(s), reclaiming about {1}." -f $toRemove.Count, (Format-Size -Bytes ([int64]$totalBytes)))
+
+foreach ($artifact in $toRemove) {
+ Write-Host ("Removing {0}" -f $artifact.Path)
+ Remove-Artifact -Artifact $artifact
+}
+
+Write-Host ""
+Write-Host "Cleanup complete."
diff --git a/scripts/windows/package_bare_metal_image.ps1 b/scripts/windows/package_bare_metal_image.ps1
new file mode 100644
index 000000000..78d2dee52
--- /dev/null
+++ b/scripts/windows/package_bare_metal_image.ps1
@@ -0,0 +1,210 @@
+[CmdletBinding()]
+param(
+ [string]$RepoRoot,
+ [string]$BrowserExe,
+ [string]$PackageRoot,
+ [string]$Url = "https://example.com/",
+ [switch]$RunSmoke
+)
+
+$ErrorActionPreference = 'Stop'
+
+$scriptRoot = $PSScriptRoot
+if (-not $RepoRoot) {
+ $RepoRoot = (Resolve-Path (Join-Path $scriptRoot "..\..")).Path
+}
+if (-not $BrowserExe) {
+ $BrowserExe = Join-Path $RepoRoot "zig-out\bin\lightpanda.exe"
+}
+if (-not $PackageRoot) {
+ $PackageRoot = Join-Path $RepoRoot "tmp-browser-smoke\bare-metal-release\image"
+}
+
+function New-BareMetalLaunchScript {
+ param(
+ [string]$LaunchScriptPath,
+ [string]$RepoRoot
+ )
+
+ $launchScript = @'
+[CmdletBinding()]
+param(
+ [string]$Url = "https://example.com/",
+ [string]$ScreenshotPath = $(Join-Path $PSScriptRoot "artifacts\launch.png"),
+ [string]$ProfileRoot = $(Join-Path $PSScriptRoot "profile")
+)
+
+$ErrorActionPreference = 'Stop'
+
+if (-not ("BareMetalImageUser32" -as [type])) {
+ Add-Type @"
+using System;
+using System.Runtime.InteropServices;
+using System.Text;
+
+public static class BareMetalImageUser32 {
+ [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+ public static extern int GetWindowTextW(IntPtr hWnd, StringBuilder text, int count);
+}
+"@
+}
+
+$browserExe = Join-Path $PSScriptRoot "boot\lightpanda.exe"
+if (-not (Test-Path $browserExe)) {
+ throw "bare metal image executable missing: $browserExe"
+}
+
+New-Item -ItemType Directory -Force -Path (Split-Path -Parent $ScreenshotPath) | Out-Null
+New-Item -ItemType Directory -Force -Path $ProfileRoot | Out-Null
+
+$stdout = Join-Path $PSScriptRoot "artifacts\launch.stdout.txt"
+$stderr = Join-Path $PSScriptRoot "artifacts\launch.stderr.txt"
+$resultPath = Join-Path $PSScriptRoot "artifacts\launch-result.json"
+if (Test-Path $ScreenshotPath) { Remove-Item $ScreenshotPath -Force }
+if (Test-Path $stdout) { Remove-Item $stdout -Force }
+if (Test-Path $stderr) { Remove-Item $stderr -Force }
+if (Test-Path $resultPath) { Remove-Item $resultPath -Force }
+
+$psi = New-Object System.Diagnostics.ProcessStartInfo
+$psi.FileName = $browserExe
+$psi.Arguments = "browse --headed --window_width 1280 --window_height 720 --screenshot_png `"$ScreenshotPath`" `"$Url`""
+$psi.WorkingDirectory = $PSScriptRoot
+$psi.UseShellExecute = $false
+$psi.RedirectStandardOutput = $true
+$psi.RedirectStandardError = $true
+$psi.EnvironmentVariables["APPDATA"] = $ProfileRoot
+$psi.EnvironmentVariables["LOCALAPPDATA"] = $ProfileRoot
+
+$process = New-Object System.Diagnostics.Process
+$process.StartInfo = $psi
+$process.Start() | Out-Null
+
+$stdoutTask = $process.StandardOutput.ReadToEndAsync()
+$stderrTask = $process.StandardError.ReadToEndAsync()
+$deadline = (Get-Date).AddSeconds(90)
+$screenshotReady = $false
+$windowTitle = ""
+
+while ((Get-Date) -lt $deadline) {
+ if (-not $process.HasExited -and $process.MainWindowHandle -ne 0) {
+ $titleBuffer = New-Object System.Text.StringBuilder 512
+ [void][BareMetalImageUser32]::GetWindowTextW($process.MainWindowHandle, $titleBuffer, $titleBuffer.Capacity)
+ $windowTitle = $titleBuffer.ToString()
+ if ($windowTitle -like "*Example Domain*") {
+ $screenshotReady = $true
+ }
+ }
+
+ if (Test-Path $ScreenshotPath) {
+ $item = Get-Item $ScreenshotPath
+ if ($item.Length -gt 0) {
+ $screenshotReady = $true
+ break
+ }
+ }
+
+ if ($process.HasExited) {
+ break
+ }
+
+ Start-Sleep -Milliseconds 250
+}
+
+if (-not $process.HasExited) {
+ Start-Sleep -Milliseconds 500
+ if (-not $process.HasExited) {
+ Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
+ }
+}
+
+$stdoutTask.Wait()
+$stderrTask.Wait()
+$stdoutTask.Result | Set-Content -Path $stdout -Encoding Ascii
+$stderrTask.Result | Set-Content -Path $stderr -Encoding Ascii
+
+$result = [ordered]@{
+ pid = $process.Id
+ exited = $process.HasExited
+ exit_code = if ($process.HasExited) { $process.ExitCode } else { $null }
+ success = $screenshotReady
+ screenshot_ready = $screenshotReady
+ screenshot_exists = (Test-Path $ScreenshotPath)
+ screenshot_size = if (Test-Path $ScreenshotPath) { (Get-Item $ScreenshotPath).Length } else { 0 }
+ window_title = $windowTitle
+ stdout = $stdout
+ stderr = $stderr
+ screenshot_path = $ScreenshotPath
+ profile_root = $ProfileRoot
+ url = $Url
+}
+
+$result | ConvertTo-Json -Depth 4 -Compress | Set-Content -Path $resultPath -Encoding Ascii
+Write-Output ($result | ConvertTo-Json -Depth 4 -Compress)
+
+if (-not $screenshotReady) {
+ throw "bare metal image launch did not reach a ready screenshot"
+}
+'@
+
+ Set-Content -Path $LaunchScriptPath -Value $launchScript -Encoding Ascii
+}
+
+$browserExists = Test-Path $BrowserExe
+if (-not $browserExists) {
+ throw "bare metal browser binary not found: $BrowserExe"
+}
+
+if (Test-Path $PackageRoot) {
+ Remove-Item $PackageRoot -Recurse -Force
+}
+
+$bootDir = Join-Path $PackageRoot "boot"
+$artifactsDir = Join-Path $PackageRoot "artifacts"
+New-Item -ItemType Directory -Force -Path $bootDir, $artifactsDir | Out-Null
+Copy-Item -Force $BrowserExe (Join-Path $bootDir "lightpanda.exe")
+
+$launchScriptPath = Join-Path $PackageRoot "launch.ps1"
+New-BareMetalLaunchScript -LaunchScriptPath $launchScriptPath -RepoRoot $RepoRoot
+
+$manifestPath = Join-Path $PackageRoot "manifest.json"
+$gitCommit = $null
+try {
+ $gitCommit = (git -C $RepoRoot rev-parse HEAD).Trim()
+} catch {
+ $gitCommit = $null
+}
+
+$manifest = [ordered]@{
+ package_root = $PackageRoot
+ browser_exe = $BrowserExe
+ launch_script = $launchScriptPath
+ boot_binary = (Join-Path $bootDir "lightpanda.exe")
+ created_utc = (Get-Date).ToUniversalTime().ToString("o")
+ git_commit = $gitCommit
+ url = $Url
+}
+$manifest | ConvertTo-Json -Depth 4 | Set-Content -Path $manifestPath -Encoding Ascii
+
+$archivePath = Join-Path (Split-Path -Parent (Split-Path -Parent $PackageRoot)) "bare-metal-release.zip"
+if (Test-Path $archivePath) {
+ Remove-Item $archivePath -Force
+}
+Compress-Archive -Path (Join-Path $PackageRoot "*") -DestinationPath $archivePath -Force
+
+$launchResult = $null
+if ($RunSmoke) {
+ $launchResult = & $launchScriptPath -Url $Url | ConvertFrom-Json
+}
+
+$result = [ordered]@{
+ package_root = $PackageRoot
+ manifest_path = $manifestPath
+ launch_script = $launchScriptPath
+ boot_binary = (Join-Path $bootDir "lightpanda.exe")
+ archive_path = $archivePath
+ archive_exists = (Test-Path $archivePath)
+ archive_size = if (Test-Path $archivePath) { (Get-Item $archivePath).Length } else { 0 }
+ launch_result = $launchResult
+}
+
+$result | ConvertTo-Json -Depth 6 -Compress
diff --git a/src/App.zig b/src/App.zig
index 2d930fd6e..7d747938e 100644
--- a/src/App.zig
+++ b/src/App.zig
@@ -20,10 +20,12 @@ const std = @import("std");
const Allocator = std.mem.Allocator;
-const log = @import("log.zig");
const Config = @import("Config.zig");
+const HostPaths = @import("HostPaths.zig");
+const Host = @import("sys/host.zig").Host;
const Snapshot = @import("browser/js/Snapshot.zig");
const Platform = @import("browser/js/Platform.zig");
+const Display = @import("display/Display.zig");
const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
const RobotStore = @import("browser/Robots.zig").RobotStore;
@@ -35,21 +37,24 @@ const App = @This();
http: Http,
config: *const Config,
platform: Platform,
+display: Display,
snapshot: Snapshot,
telemetry: Telemetry,
allocator: Allocator,
arena_pool: ArenaPool,
robots: RobotStore,
app_dir_path: ?[]const u8,
+host: ?*Host = null,
shutdown: bool = false,
-pub fn init(allocator: Allocator, config: *const Config) !*App {
+pub fn init(allocator: Allocator, config: *const Config, host: ?*Host) !*App {
const app = try allocator.create(App);
errdefer allocator.destroy(app);
app.* = .{
.config = config,
.allocator = allocator,
+ .display = Display.init(allocator, config, host),
.robots = RobotStore.init(allocator),
.http = undefined,
.platform = undefined,
@@ -57,10 +62,12 @@ pub fn init(allocator: Allocator, config: *const Config) !*App {
.app_dir_path = undefined,
.telemetry = undefined,
.arena_pool = undefined,
+ .host = host,
};
app.http = try Http.init(allocator, &app.robots, config);
errdefer app.http.deinit();
+ app.display.setHttpRuntime(&app.http);
app.platform = try Platform.init();
errdefer app.platform.deinit();
@@ -68,7 +75,11 @@ pub fn init(allocator: Allocator, config: *const Config) !*App {
app.snapshot = try Snapshot.load();
errdefer app.snapshot.deinit();
- app.app_dir_path = getAndMakeAppDir(allocator);
+ app.app_dir_path = if (host) |host_ref|
+ host_ref.resolveProfileDir(config.profileDir())
+ else
+ HostPaths.resolveProfileDir(allocator, config.profileDir());
+ app.display.setAppDataPath(app.app_dir_path);
app.telemetry = try Telemetry.init(app, config.mode);
errdefer app.telemetry.deinit();
@@ -93,28 +104,9 @@ pub fn deinit(self: *App) void {
self.robots.deinit();
self.http.deinit();
self.snapshot.deinit();
+ self.display.deinit();
self.platform.deinit();
self.arena_pool.deinit();
allocator.destroy(self);
}
-
-fn getAndMakeAppDir(allocator: Allocator) ?[]const u8 {
- if (@import("builtin").is_test) {
- return allocator.dupe(u8, "/tmp") catch unreachable;
- }
- const app_dir_path = std.fs.getAppDataDir(allocator, "lightpanda") catch |err| {
- log.warn(.app, "get data dir", .{ .err = err });
- return null;
- };
-
- std.fs.cwd().makePath(app_dir_path) catch |err| switch (err) {
- error.PathAlreadyExists => return app_dir_path,
- else => {
- allocator.free(app_dir_path);
- log.warn(.app, "create data dir", .{ .err = err, .path = app_dir_path });
- return null;
- },
- };
- return app_dir_path;
-}
diff --git a/src/Config.zig b/src/Config.zig
index 5a4cc58e1..919d48e78 100644
--- a/src/Config.zig
+++ b/src/Config.zig
@@ -25,6 +25,7 @@ const dump = @import("browser/dump.zig");
pub const RunMode = enum {
help,
+ browse,
fetch,
serve,
version,
@@ -32,6 +33,10 @@ pub const RunMode = enum {
};
pub const CDP_MAX_HTTP_REQUEST_SIZE = 4096;
+pub const DEFAULT_VIEWPORT_WIDTH: u32 = 1920;
+pub const DEFAULT_VIEWPORT_HEIGHT: u32 = 1080;
+pub const DEFAULT_HTTP_TIMEOUT_MS: u31 = 5000;
+pub const DEFAULT_INTERACTIVE_HTTP_TIMEOUT_MS: u31 = 30000;
// max message size
// +14 for max websocket payload overhead
@@ -60,56 +65,61 @@ pub fn deinit(self: *const Config, allocator: Allocator) void {
pub fn tlsVerifyHost(self: *const Config) bool {
return switch (self.mode) {
- inline .serve, .fetch, .mcp => |opts| opts.common.tls_verify_host,
+ inline .serve, .fetch, .browse, .mcp => |opts| opts.common.tls_verify_host,
else => unreachable,
};
}
pub fn obeyRobots(self: *const Config) bool {
return switch (self.mode) {
- inline .serve, .fetch, .mcp => |opts| opts.common.obey_robots,
+ inline .serve, .fetch, .browse, .mcp => |opts| opts.common.obey_robots,
else => unreachable,
};
}
pub fn httpProxy(self: *const Config) ?[:0]const u8 {
return switch (self.mode) {
- inline .serve, .fetch, .mcp => |opts| opts.common.http_proxy,
+ inline .serve, .fetch, .browse, .mcp => |opts| opts.common.http_proxy,
else => unreachable,
};
}
pub fn proxyBearerToken(self: *const Config) ?[:0]const u8 {
return switch (self.mode) {
- inline .serve, .fetch, .mcp => |opts| opts.common.proxy_bearer_token,
+ inline .serve, .fetch, .browse, .mcp => |opts| opts.common.proxy_bearer_token,
.help, .version => null,
};
}
pub fn httpMaxConcurrent(self: *const Config) u8 {
return switch (self.mode) {
- inline .serve, .fetch, .mcp => |opts| opts.common.http_max_concurrent orelse 10,
+ inline .serve, .fetch, .browse, .mcp => |opts| opts.common.http_max_concurrent orelse 10,
else => unreachable,
};
}
pub fn httpMaxHostOpen(self: *const Config) u8 {
return switch (self.mode) {
- inline .serve, .fetch, .mcp => |opts| opts.common.http_max_host_open orelse 4,
+ inline .serve, .fetch, .browse, .mcp => |opts| opts.common.http_max_host_open orelse 4,
else => unreachable,
};
}
pub fn httpConnectTimeout(self: *const Config) u31 {
return switch (self.mode) {
- inline .serve, .fetch, .mcp => |opts| opts.common.http_connect_timeout orelse 0,
+ inline .serve, .fetch, .browse, .mcp => |opts| opts.common.http_connect_timeout orelse 0,
else => unreachable,
};
}
pub fn httpTimeout(self: *const Config) u31 {
return switch (self.mode) {
- inline .serve, .fetch, .mcp => |opts| opts.common.http_timeout orelse 5000,
+ .browse => |opts| opts.common.http_timeout orelse DEFAULT_INTERACTIVE_HTTP_TIMEOUT_MS,
+ .serve => |opts| opts.common.http_timeout orelse if (opts.common.browser_mode == .headed)
+ DEFAULT_INTERACTIVE_HTTP_TIMEOUT_MS
+ else
+ DEFAULT_HTTP_TIMEOUT_MS,
+ inline .fetch, .mcp => |opts| opts.common.http_timeout orelse DEFAULT_HTTP_TIMEOUT_MS,
else => unreachable,
};
}
@@ -120,39 +130,67 @@ pub fn httpMaxRedirects(_: *const Config) u8 {
pub fn httpMaxResponseSize(self: *const Config) ?usize {
return switch (self.mode) {
- inline .serve, .fetch, .mcp => |opts| opts.common.http_max_response_size,
+ inline .serve, .fetch, .browse, .mcp => |opts| opts.common.http_max_response_size,
else => unreachable,
};
}
pub fn logLevel(self: *const Config) ?log.Level {
return switch (self.mode) {
- inline .serve, .fetch, .mcp => |opts| opts.common.log_level,
+ inline .serve, .fetch, .browse, .mcp => |opts| opts.common.log_level,
else => unreachable,
};
}
pub fn logFormat(self: *const Config) ?log.Format {
return switch (self.mode) {
- inline .serve, .fetch, .mcp => |opts| opts.common.log_format,
+ inline .serve, .fetch, .browse, .mcp => |opts| opts.common.log_format,
else => unreachable,
};
}
pub fn logFilterScopes(self: *const Config) ?[]const log.Scope {
return switch (self.mode) {
- inline .serve, .fetch, .mcp => |opts| opts.common.log_filter_scopes,
+ inline .serve, .fetch, .browse, .mcp => |opts| opts.common.log_filter_scopes,
else => unreachable,
};
}
pub fn userAgentSuffix(self: *const Config) ?[]const u8 {
return switch (self.mode) {
- inline .serve, .fetch, .mcp => |opts| opts.common.user_agent_suffix,
+ inline .serve, .fetch, .browse, .mcp => |opts| opts.common.user_agent_suffix,
+ .help, .version => null,
+ };
+}
+
+pub fn profileDir(self: *const Config) ?[]const u8 {
+ return switch (self.mode) {
+ inline .serve, .fetch, .browse, .mcp => |opts| opts.common.profile_dir,
.help, .version => null,
};
}
+pub fn browserMode(self: *const Config) BrowserMode {
+ return switch (self.mode) {
+ inline .serve, .fetch, .browse, .mcp => |opts| opts.common.browser_mode,
+ .help, .version => .headless,
+ };
+}
+
+pub fn windowWidth(self: *const Config) u32 {
+ return switch (self.mode) {
+ inline .serve, .fetch, .browse, .mcp => |opts| opts.common.window_width orelse DEFAULT_VIEWPORT_WIDTH,
+ .help, .version => DEFAULT_VIEWPORT_WIDTH,
+ };
+}
+
+pub fn windowHeight(self: *const Config) u32 {
+ return switch (self.mode) {
+ inline .serve, .fetch, .browse, .mcp => |opts| opts.common.window_height orelse DEFAULT_VIEWPORT_HEIGHT,
+ .help, .version => DEFAULT_VIEWPORT_HEIGHT,
+ };
+}
+
pub fn maxConnections(self: *const Config) u16 {
return switch (self.mode) {
.serve => |opts| opts.cdp_max_connections,
@@ -169,12 +207,20 @@ pub fn maxPendingConnections(self: *const Config) u31 {
pub const Mode = union(RunMode) {
help: bool, // false when being printed because of an error
+ browse: Browse,
fetch: Fetch,
serve: Serve,
version: void,
mcp: Mcp,
};
+pub const Browse = struct {
+ url: [:0]const u8,
+ common: Common = .{ .browser_mode = .headed },
+ screenshot_bmp_path: ?[:0]const u8 = null,
+ screenshot_png_path: ?[:0]const u8 = null,
+};
+
pub const Serve = struct {
host: []const u8 = "127.0.0.1",
port: u16 = 9222,
@@ -203,6 +249,11 @@ pub const Fetch = struct {
strip: dump.Opts.Strip = .{},
};
+pub const BrowserMode = enum {
+ headless,
+ headed,
+};
+
pub const Common = struct {
obey_robots: bool = false,
proxy_bearer_token: ?[:0]const u8 = null,
@@ -217,6 +268,10 @@ pub const Common = struct {
log_format: ?log.Format = null,
log_filter_scopes: ?[]log.Scope = null,
user_agent_suffix: ?[]const u8 = null,
+ profile_dir: ?[:0]const u8 = null,
+ browser_mode: BrowserMode = .headless,
+ window_width: ?u32 = null,
+ window_height: ?u32 = null,
};
/// Pre-formatted HTTP headers for reuse across Http and Client.
@@ -324,13 +379,46 @@ pub fn printUsageAndExit(self: *const Config, success: bool) void {
\\--user_agent_suffix
\\ Suffix to append to the Lightpanda/X.Y User-Agent
\\
+ \\--profile_dir Explicit browser profile root for cookies, storage,
+ \\ downloads, telemetry IDs, and other persisted state.
+ \\ Defaults to the platform app-data directory when
+ \\ available.
+ \\
+ \\--browser_mode Browser mode: headless or headed.
+ \\ Defaults to headless.
+ \\
+ \\--headed Shortcut for '--browser_mode headed'
+ \\
+ \\--headless Shortcut for '--browser_mode headless'
+ \\
+ \\--window_width Window/viewport width in CSS pixels.
+ \\ Defaults to 1920.
+ \\
+ \\--window_height Window/viewport height in CSS pixels.
+ \\ Defaults to 1080.
+ \\
;
// MAX_HELP_LEN|
const usage =
\\usage: {s} command [options] [URL]
\\
- \\Command can be either 'fetch', 'serve', 'mcp' or 'help'
+ \\Command can be either 'browse', 'fetch', 'serve', 'mcp' or 'help'
+ \\
+ \\browse command
+ \\Opens the specified URL in a native browser window.
+ \\Example: {s} browse https://lightpanda.io/
+ \\
+ \\Options:
+ \\--screenshot_bmp
+ \\ Save the first rendered headed browse frame as a BMP file.
+ \\ Argument must be the output path.
+ \\
+ \\--screenshot_png
+ \\ Save the first rendered headed browse frame as a PNG file.
+ \\ Argument must be the output path.
+ \\
+ ++ common_options ++
\\
\\fetch command
\\Fetches the specified URL
@@ -391,7 +479,7 @@ pub fn printUsageAndExit(self: *const Config, success: bool) void {
\\Displays this message
\\
;
- std.debug.print(usage, .{ self.exec_name, self.exec_name, self.exec_name, self.exec_name, self.exec_name });
+ std.debug.print(usage, .{ self.exec_name, self.exec_name, self.exec_name, self.exec_name, self.exec_name, self.exec_name });
if (success) {
return std.process.cleanExit();
}
@@ -422,6 +510,8 @@ pub fn parseArgs(allocator: Allocator) !Config {
const mode: Mode = switch (run_mode) {
.help => .{ .help = true },
+ .browse => .{ .browse = parseBrowseArgs(allocator, &args) catch
+ return init(allocator, exec_name, .{ .help = false }) },
.serve => .{ .serve = parseServeArgs(allocator, &args) catch
return init(allocator, exec_name, .{ .help = false }) },
.fetch => .{ .fetch = parseFetchArgs(allocator, &args) catch
@@ -466,6 +556,34 @@ fn inferMode(opt: []const u8) ?RunMode {
return .serve;
}
+ if (std.mem.eql(u8, opt, "--headed")) {
+ return .browse;
+ }
+
+ if (std.mem.eql(u8, opt, "--headless")) {
+ return .browse;
+ }
+
+ if (std.mem.eql(u8, opt, "--browser_mode")) {
+ return .browse;
+ }
+
+ if (std.mem.eql(u8, opt, "--window_width")) {
+ return .browse;
+ }
+
+ if (std.mem.eql(u8, opt, "--window_height")) {
+ return .browse;
+ }
+
+ if (std.mem.eql(u8, opt, "--screenshot_bmp")) {
+ return .browse;
+ }
+
+ if (std.mem.eql(u8, opt, "--screenshot_png")) {
+ return .browse;
+ }
+
if (std.mem.eql(u8, opt, "--port")) {
return .serve;
}
@@ -477,6 +595,63 @@ fn inferMode(opt: []const u8) ?RunMode {
return null;
}
+fn parseBrowseArgs(
+ allocator: Allocator,
+ args: *std.process.ArgIterator,
+) !Browse {
+ var url: ?[:0]const u8 = null;
+ var common: Common = .{ .browser_mode = .headed };
+ var screenshot_bmp_path: ?[:0]const u8 = null;
+ var screenshot_png_path: ?[:0]const u8 = null;
+
+ while (args.next()) |opt| {
+ if (try parseCommonArg(allocator, opt, args, &common)) {
+ continue;
+ }
+
+ if (std.mem.eql(u8, "--screenshot_bmp", opt)) {
+ const str = args.next() orelse {
+ log.fatal(.app, "missing argument value", .{ .arg = "--screenshot_bmp" });
+ return error.InvalidArgument;
+ };
+ screenshot_bmp_path = try allocator.dupeZ(u8, str);
+ continue;
+ }
+
+ if (std.mem.eql(u8, "--screenshot_png", opt)) {
+ const str = args.next() orelse {
+ log.fatal(.app, "missing argument value", .{ .arg = "--screenshot_png" });
+ return error.InvalidArgument;
+ };
+ screenshot_png_path = try allocator.dupeZ(u8, str);
+ continue;
+ }
+
+ if (std.mem.startsWith(u8, opt, "--")) {
+ log.fatal(.app, "unknown argument", .{ .mode = "browse", .arg = opt });
+ return error.UnkownOption;
+ }
+
+ if (url != null) {
+ log.fatal(.app, "duplicate browse url", .{ .help = "only 1 URL can be specified" });
+ return error.TooManyURLs;
+ }
+ url = try allocator.dupeZ(u8, opt);
+ }
+
+ if (url == null) {
+ log.fatal(.app, "missing browse url", .{ .help = "URL to browse must be provided" });
+ return error.MissingURL;
+ }
+
+ return .{
+ .url = url.?,
+ .common = common,
+ .screenshot_bmp_path = screenshot_bmp_path,
+ .screenshot_png_path = screenshot_png_path,
+ };
+}
+
fn parseServeArgs(
allocator: Allocator,
args: *std.process.ArgIterator,
@@ -845,5 +1020,113 @@ fn parseCommonArg(
return true;
}
+ if (std.mem.eql(u8, "--profile_dir", opt)) {
+ const str = args.next() orelse {
+ log.fatal(.app, "missing argument value", .{ .arg = "--profile_dir" });
+ return error.InvalidArgument;
+ };
+ common.profile_dir = try allocator.dupeZ(u8, str);
+ return true;
+ }
+
+ if (std.mem.eql(u8, "--browser_mode", opt)) {
+ const str = args.next() orelse {
+ log.fatal(.app, "missing argument value", .{ .arg = "--browser_mode" });
+ return error.InvalidArgument;
+ };
+
+ common.browser_mode = std.meta.stringToEnum(BrowserMode, str) orelse {
+ log.fatal(.app, "invalid option choice", .{ .arg = "--browser_mode", .value = str });
+ return error.InvalidArgument;
+ };
+ return true;
+ }
+
+ if (std.mem.eql(u8, "--headed", opt)) {
+ common.browser_mode = .headed;
+ return true;
+ }
+
+ if (std.mem.eql(u8, "--headless", opt)) {
+ common.browser_mode = .headless;
+ return true;
+ }
+
+ if (std.mem.eql(u8, "--window_width", opt)) {
+ const str = args.next() orelse {
+ log.fatal(.app, "missing argument value", .{ .arg = "--window_width" });
+ return error.InvalidArgument;
+ };
+
+ common.window_width = std.fmt.parseInt(u32, str, 10) catch |err| {
+ log.fatal(.app, "invalid argument value", .{ .arg = "--window_width", .err = err });
+ return error.InvalidArgument;
+ };
+ if (common.window_width.? == 0) {
+ log.fatal(.app, "invalid argument value", .{ .arg = "--window_width", .value = str });
+ return error.InvalidArgument;
+ }
+ return true;
+ }
+
+ if (std.mem.eql(u8, "--window_height", opt)) {
+ const str = args.next() orelse {
+ log.fatal(.app, "missing argument value", .{ .arg = "--window_height" });
+ return error.InvalidArgument;
+ };
+
+ common.window_height = std.fmt.parseInt(u32, str, 10) catch |err| {
+ log.fatal(.app, "invalid argument value", .{ .arg = "--window_height", .err = err });
+ return error.InvalidArgument;
+ };
+ if (common.window_height.? == 0) {
+ log.fatal(.app, "invalid argument value", .{ .arg = "--window_height", .value = str });
+ return error.InvalidArgument;
+ }
+ return true;
+ }
+
return false;
}
+
+test "browse defaults to interactive http timeout" {
+ var config = try Config.init(std.testing.allocator, "test", .{
+ .browse = .{ .url = "https://example.com/" },
+ });
+ defer config.deinit(std.testing.allocator);
+
+ try std.testing.expectEqual(DEFAULT_INTERACTIVE_HTTP_TIMEOUT_MS, config.httpTimeout());
+}
+
+test "headed serve defaults to interactive http timeout" {
+ var config = try Config.init(std.testing.allocator, "test", .{
+ .serve = .{ .common = .{ .browser_mode = .headed } },
+ });
+ defer config.deinit(std.testing.allocator);
+
+ try std.testing.expectEqual(DEFAULT_INTERACTIVE_HTTP_TIMEOUT_MS, config.httpTimeout());
+}
+
+test "headless serve keeps shorter default http timeout" {
+ var config = try Config.init(std.testing.allocator, "test", .{
+ .serve = .{},
+ });
+ defer config.deinit(std.testing.allocator);
+
+ try std.testing.expectEqual(DEFAULT_HTTP_TIMEOUT_MS, config.httpTimeout());
+}
+
+test "explicit http timeout overrides interactive defaults" {
+ var config = try Config.init(std.testing.allocator, "test", .{
+ .browse = .{
+ .url = "https://example.com/",
+ .common = .{
+ .browser_mode = .headed,
+ .http_timeout = 1234,
+ },
+ },
+ });
+ defer config.deinit(std.testing.allocator);
+
+ try std.testing.expectEqual(@as(u31, 1234), config.httpTimeout());
+}
diff --git a/src/HostPaths.zig b/src/HostPaths.zig
new file mode 100644
index 000000000..2deae2215
--- /dev/null
+++ b/src/HostPaths.zig
@@ -0,0 +1,152 @@
+// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
@@ -22,3 +24,8 @@
testing.expectEqual($('#link1'), document.links[0]);
testing.expectEqual($('#link2'), document.links[1]);
+
+
diff --git a/src/browser/tests/document/element_from_point.html b/src/browser/tests/document/element_from_point.html
index 0ee07deba..38e435dc8 100644
--- a/src/browser/tests/document/element_from_point.html
+++ b/src/browser/tests/document/element_from_point.html
@@ -8,6 +8,10 @@