Closes #48.
The issue's first ask cannot be built as written
--remote-debugging/--automation: enable WebKit's remote inspector … and print the port/UUID. That alone makes every existing WebDriver-ish tool usable.
There is no port and no UUID. Measured, not inferred:
_setRemoteInspectionAllowed:— named in the issue — no longer exists in the runtime.respondsToSelector:→ 0. Sending it unguarded would crash.- No endpoint accessor of any kind:
_inspectorURL,_remoteInspectorURL,_remoteInspectionPort,_webInspectorURL,inspectorURL— all absent. WEBKIT_INSPECTOR_SERVERis a WebKitGTK/WPE variable — 0 hits in macOS's WebKit.- An inspectable
WKWebViewwith loaded content holds zero internet sockets across the host process and all three WebKit XPC children. Discovery is Mach/XPC towebinspectord. safaridriverrefuses anybrowserNameother than Safari, by design.
So the closing claim is false, and I'd suggest striking it from the issue — it's the premise the rest of item 1 rests on. What's actually available is setInspectable:, which puts the app in Safari's Develop menu: a human debugging affordance, not automation.
What that turned up instead: DevTools have never worked
Two independent reasons, either alone sufficient:
- No flag turned them on.
dev_toolsdefaults tofalse; the only flag,--no-devtools, sets itfalseagain. The sole other writer wascraft.json. The SDK compounded it — it pushes only the negative, sodevTools: detectDevMode()beingtruesent nothing. - Even when enabled, it didn't work on macOS 13.3+, which introduced
isInspectabledefaulting toNO. Measured on 26.3.1:
developerExtrasEnabled=YES -> isInspectable = 0And macos.zig:6149 asserted the opposite — "the webview sets setInspectable:YES" — while that string appeared only in that comment, repo-wide. Someone reading it would reasonably close this issue as already done.
Fixed: --dev-tools, a respondsToSelector:-guarded setInspectable:, the SDK pushing both directions, comment corrected. Confirmed in the shipped binary — with the flag, isInspectable = true; without it the call isn't reached.
--headless
Mostly two calls not happening: createWindowWithStyle has never ordered its window on screen — showAllWindows and activateWindowedApp do, later — so skipping both plus setting the accessory activation policy is the mechanism.
Verified against the shipped binary, with a control:
craft --headless -> on-screen craft windows: 0
craft (shown) -> on-screen craft windows: 1and a headless run's page reached a local HTTP server carrying the right document.title, so it loaded and ran script while invisible.
Not folded into --benchmark, deliberately: that mode strips the user content controller, the injected script, the message handler and both webview globals, so nothing could talk to the page afterwards.
The cost, which is documented because it is not recoverable
An unshown window doesn't drive the compositor, so requestAnimationFrame stops after one frame and page timers fall to ~1Hz. Animations, canvas loops, and "wait until the spinner stops" see frame one forever.
No invisible configuration avoids this. What drives the compositor is the window being unoccluded, which needs activation — stealing focus and showing a Dock icon. That's a worse trade for exactly the operator this mode exists to help, so it's in --help and the docs rather than papered over.
--headless --menubar-only exits 1 instead of silently ignoring one.
A correction from my own testing
My first probe reported that a never-shown window doesn't load the page at all — loaded=0, and JS failing on an empty DOM. That was wrong: it was cold-start latency, the WebContent process launching. At a 2s budget every configuration loads; at 0.3s none do, including the visible control. I'd have designed around a constraint that doesn't exist.
Scope
eval-in-page and screenshot against a running instance need a control channel craft owns — transport, discovery, and an auth boundary for something that evaluates arbitrary JavaScript in an app's page. None of that exists today. It's a separate change, and half-building it here would be worse than not starting.
Worth recording for that follow-up: takeSnapshotWithConfiguration: works with no visible window and returns fresh pixels after DOM mutation, so headless screenshots are viable. But craft's shipped capture path is not the way to get them — CGWindowListCreateImage returns NIL for a never-shown window, and it's marked obsoleted in the macOS 15 SDK, linking today only because bridge_screen_capture.zig:120 declares it by hand and bypasses the availability annotation.
zig build test clean, bun test 476 pass, Windows cross-build clean, tsc --noEmit clean, pickier 38 warnings — identical to main. All new tests control-checked; the devTools one caught a wrong premise in my own test, which is fixed.