一键导入
webview-ui
Build or iterate on a Pulp WebView UI using the native WebView bridge, embedded assets, directory-backed dev resources, and focused WebView validation.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Build or iterate on a Pulp WebView UI using the native WebView bridge, embedded assets, directory-backed dev resources, and focused WebView validation.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Pulp musical/media time primitives, immutable compiled tempo maps, monotonic transport beats, exact sample anchors, corrected inverse conversion, and shared transport quantization arithmetic.
Import designs from Figma, Stitch, v0, Pencil, React Native, or Claude Design into Pulp web-compat JS with automated visual validation. Claude Design imports also scaffold a pulp::view::EditorBridge handler file. Versioned parser, format, and compatibility-schema detection lives behind `--detect-only` and `--report-new-format`.
Pick the right Pulp Stream for a given I/O task, wire async callbacks correctly without deadlocking the worker, and avoid the backpressure / cancellation footguns in `pulp::runtime::AsyncStream`.
Editor lifecycle and multi-view attach for Pulp plugins — when to override Processor::create_view(), the open → notify_attached → resize → close protocol, release_view() ownership rules, and secondary-view roles.
Load, run, and test VST3 / AU / CLAP / LV2 plugins from Pulp code. Use when working on `core/host/` (scanner, plugin_slot, signal_graph), when adding a new format backend, when wiring a plugin into a SignalGraph, or when writing an integration test that needs a real plug-in binary.
Audio Unit v3 (AUAudioUnit) format adapter for Pulp — render-block wiring, parameter tree bridging, MIDI / sysex via AURenderEvent, sidechain pulls, state persistence, iOS extension surface, and the pitfalls discovered while wiring the adapter.
| name | webview-ui |
| description | Build or iterate on a Pulp WebView UI using the native WebView bridge, embedded assets, directory-backed dev resources, and focused WebView validation. |
Use this skill when working on Pulp's optional WebView layer: HTML/JS panels, Monaco-style tools, documentation views, or mixed native/WebView windows.
Pulp WebView is an opt-in compatibility layer, not the primary UI path. Default to native Pulp UI unless the task truly benefits from existing web content.
Main APIs:
pulp::view::WebViewPanelpulp::view::WebViewOptionspulp::view::WindowHostpulp::view::AssetManagerBridge contract:
window.pulp.postMessage(type, payload, id)window.pulp.on(type, callback)set_message_handler(...)post_message(...)evaluate_js(..., callback)PULP_BUILD_WEBVIEW=ON is now the honest opt-in switch for the common Pulp
WebView layer on:
Required platform notes:
gtk+-3.0 and webkit2gtk-4.1 available through pkg-configKeep claims narrow:
pulp-webview-palette build proofchoc::ui::WebView)CHOC's WebView layer carries file drag-and-drop, exposed to page JS — no Pulp
C++ wiring needed, it rides the choc/gui/choc_WebView.h we already wrap in
core/view/src/web_view.cpp:
window.chocStartFileDrag(["/abs/path", ...])
from a pointerdown/mousedown handler.chocfilesdropped event;
e.detail.paths is the array of absolute paths.Platform reality: macOS (WKWebView) + Linux (WebKitGTK) — both directions. Windows (WebView2) — inbound only (outbound is a documented no-op: windowed WebView2 can't start a drag from web content without moving to composition hosting). The JS call exists on all platforms for parity.
⚠ Interim source: this lands via an interim fork pin of CHOC
(danielraffel/choc tag pulp-webview-dnd-poc1, carrying WebView drag-and-drop
support; Tracktion/choc pull request 64 is the upstream tracker).
When that support is available upstream, the pin reverts to
Tracktion/choc with no API change. The runnable reference lives in the choc repo
(examples/webview_drag_and_drop.cpp), not in Pulp.
set_html(...)make_webview_bridge_bootstrap_script()1.5. Placeholder first paint
WebViewOptions::initial_html when the final page is dark-themed and the
platform WebView would otherwise flash white before the real content loadstransparent_background = true when the final page also wants a
transparent/native host backgroundpulp_add_binary_data(...)AssetManagermake_webview_embedded_resource_fetcher(...)make_webview_directory_resource_fetcher(...)Do not chase Monaco's deprecated AMD path.
Use a prebundled ESM workflow:
MonacoEnvironment.getWorkerUrl(...) or getWorker(...) at those generated worker fileswindow.monaco only if the page wants that global convenienceTreat raw Monaco source or raw ESM output as build inputs, not as the final browser payload. Worker and CSS handling belongs to the bundler.
Current proven local recipe:
cd /Users/danielraffel/Code/monaco-editor
npm install
npm run build-monaco-editor
cd /path/to/pulp
node examples/webview-monaco/build_monaco_bundle.mjs \
--monaco-root /Users/danielraffel/Code/monaco-editor \
--out-dir /path/to/pulp/build/examples/webview-monaco/dist
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DPULP_BUILD_WEBVIEW=ON \
-DPULP_MONACO_BUNDLE_DIR=/path/to/pulp/build/examples/webview-monaco/dist
cmake --build build --target pulp-webview-monaco -j8
./build/examples/webview-monaco/pulp-webview-monaco
Important notes from the working proof:
examples/webview-monaco/build_monaco_bundle.mjseditor.main.js so Monaco's real CSS/features come throughvs/languages/features/... layout@vscode/monaco-lsp-client; for the Monaco example we stub that at bundle time because the example is not doing LSPbridge unavailable is expected there because only the native host injects window.pulpeditor readyWebView backend availability and host embedding availability are separate. A
WebViewPanel can exist even when the surrounding WindowHost or
PluginViewHost cannot accept a native child surface. Always check:
native_handle()attach_native_child_view(...)Current host truth:
WindowHost child embedding is built in on macOSWindowHost does not expose standalone child-view embedding
handles yetPluginViewHost child embedding is built in on macOS and iOSWindowHost::Factory or PluginViewHost::FactoryFor palette / inspector style UI:
WindowHostWindowHostattach_native_child_view(...)set_native_child_view_bounds(...)For standalone native-child embeds, do not size from startup constants once
the host is live. Read WindowHost::get_content_size() for the current
content bounds, attach the child with that size, and keep it in sync via
WindowHost::set_resize_callback(...). examples/webview-monaco/main.cpp
is the current reference pattern.
For plugin-editor embedding, use the view::NativeViewHost widget — do NOT
hand-roll plugin_view_host()->attach_native_child_view(...):
NativeViewHost to the editor tree, size it with flex
(host->flex().flex_grow = 1.0f to fill), and call
set_native_child(panel->native_handle(), snapshot_fn)PluginViewHost
(editor) and a WindowHost (standalone). It falls back from
plugin_view_host() to window_host()
(core/view/src/native_view_host.cpp try_attach() / active_viewport_transform())on_view_opened/resized/closed plumbingexamples/webview-plugin/ for the reference Processor-backed editorexamples/webview-plugin)WebViewPanel on the Processor, never in the view
tree. ViewBridge::close() destroys the whole view tree on editor close
(core/format/src/view_bridge.cpp). A view-owned WebViewPanel is recreated
every close/reopen, discarding the WKWebView's JS heap + page/scroll state. Put
the panel on the Processor (create once, lazily in create_view()); the
NativeViewHost only BORROWS native_handle(). Rule of thumb: anything that
must survive editor close lives on the Processor. Pinned by a pointer-identity
test across two open/close cycles in test/test_webview_plugin_example.cpp.plugin_view_host()-only attach SILENTLY no-ops in standalone. In
standalone the root is hosted by a WindowHost, not a PluginViewHost
(core/format/src/standalone.cpp), so plugin_view_host() is null and a
hand-rolled attach returns without attaching — the WebView never appears and
nothing errors. NativeViewHost's window_host() fallback is the fix.WKWebView.pageZoom = scale parity is deliberately DEFERRED (not a
native-editor blocker).snapshot_png() returns ZERO bytes even when is_ready()==true —
WKWebView's takeSnapshot needs an on-screen window with painted content.
Example/headless tests therefore assert the snapshot ROUTE by equality
(NativeViewHost::capture_native_overlay_png == panel->snapshot_png()), not a
literal non-empty size; the non-empty forwarding guarantee is pinned at the
widget level in test/test_native_view_host.cpp.For a standalone app that should behave like a single-pane WebView shell:
pulp::format::StandaloneApp::run_with_editor(...)StandaloneConfig::show_settings_tab = false to skip the outer
standalone Editor/Settings chrome entirelyexamples/webview-plugin/main.cpp is the current kiosk-style proofWebViewOptions::initial_html
with a dark placeholder so the host opens into the final visual familyCMakeLists.txt via pulp_app_icon(...); it stays optional and
should not be wired through the runtime WebView code itselfFor focused local proof:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DPULP_BUILD_WEBVIEW=ON
cmake --build build --target pulp-test-webview -j8
./build/test/pulp-test-webview --reporter compact
If example code changed, also build the relevant example target:
cmake --build build --target pulp-webview-palette -j8
Current proof expectations:
For quick visual inspection, keep a static browser preview page beside the runtime assets when useful. That gives you a cheap screenshot path without pretending the browser preview replaces native validation.
For Monaco specifically:
A WebView is an OS-composited native child view (attach_native_child_view); it
is NOT painted into the Pulp Skia canvas, so a plain headless capture
(render_to_png / a host back-buffer read) of an editor that hosts one sees a
blank where the WebView is. To make a WebView editor self-verifiable headlessly,
the owning Pulp View wrapper must opt in:
set_contains_native_overlay(true) when it attaches the WebView, andcapture_native_overlay_png() to forward WebViewPanel::snapshot_png()
(macOS: WKWebView takeSnapshot, no screen-recording permission needed).The smart capture path (pulp::view::capture_view / pulp-screenshot --backend auto / standalone --screenshot) then returns the in-process WebView snapshot
instead of silently rastering a blank, and refuses with a reason only when no
snapshot is available. Without the flag+override, auto captures a blank overlay
area silently. See examples/webview-plugin WebViewEditorPane for the pattern
and the screenshot skill for the dispatch contract.
When porting a WebView editor to the native bridge (translating browser <canvas> + Canvas2D code to pulp's canvas* bridge globals via a JS shim like Spectr's canvas2d-shim.ts), several browser idioms silently break. Authoritative reference is the import-design skill's "Canvas2D Bridge Gotchas" section — see that for the full rule set with example code. Top-of-mind summary:
ctx.arc() does not add to a path — bridge canvasArc strokes immediately. Synthesize as ~32 canvasLineTo segments so ctx.fill() honors the active gradient. Same applies to arcTo, ellipse, roundRect.(color, pos) pairs — passing JSON makes the bridge parse zero stops → silent fall-through to default white fill. canvasSetLinearGradient(id, x0, y0, x1, y1, c1, p1, c2, p2, ...).(cx, cy, R), not the spec's two-circle form. Map (x0,y0,r0,x1,y1,r1) → (x1, y1, r1) (visually identical when r0=0, same center).save_layer isolation — since pulp v0.74.1, ctx.clearRect() on one canvas no longer erases pixels another sibling just painted. Pin SDK >= 0.74.1 for any multi-canvas editor.