bitwrench 2.0.25 → 2.0.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +10 -4
  2. package/dist/bitwrench-bccl.cjs.js +1 -1
  3. package/dist/bitwrench-bccl.cjs.min.js +1 -1
  4. package/dist/bitwrench-bccl.cjs.min.js.gz +0 -0
  5. package/dist/bitwrench-bccl.esm.js +1 -1
  6. package/dist/bitwrench-bccl.esm.min.js +1 -1
  7. package/dist/bitwrench-bccl.esm.min.js.gz +0 -0
  8. package/dist/bitwrench-bccl.umd.js +1 -1
  9. package/dist/bitwrench-bccl.umd.min.js +1 -1
  10. package/dist/bitwrench-bccl.umd.min.js.gz +0 -0
  11. package/dist/bitwrench-code-edit.cjs.js +1 -1
  12. package/dist/bitwrench-code-edit.cjs.min.js +1 -1
  13. package/dist/bitwrench-code-edit.es5.js +1 -1
  14. package/dist/bitwrench-code-edit.es5.min.js +1 -1
  15. package/dist/bitwrench-code-edit.esm.js +1 -1
  16. package/dist/bitwrench-code-edit.esm.min.js +1 -1
  17. package/dist/bitwrench-code-edit.umd.js +1 -1
  18. package/dist/bitwrench-code-edit.umd.min.js +1 -1
  19. package/dist/bitwrench-code-edit.umd.min.js.gz +0 -0
  20. package/dist/bitwrench-debug.js +1 -1
  21. package/dist/bitwrench-debug.min.js +1 -1
  22. package/dist/bitwrench-lean.cjs.js +623 -155
  23. package/dist/bitwrench-lean.cjs.min.js +7 -7
  24. package/dist/bitwrench-lean.cjs.min.js.gz +0 -0
  25. package/dist/bitwrench-lean.es5.js +650 -157
  26. package/dist/bitwrench-lean.es5.min.js +5 -5
  27. package/dist/bitwrench-lean.es5.min.js.gz +0 -0
  28. package/dist/bitwrench-lean.esm.js +623 -155
  29. package/dist/bitwrench-lean.esm.min.js +6 -6
  30. package/dist/bitwrench-lean.esm.min.js.gz +0 -0
  31. package/dist/bitwrench-lean.umd.js +623 -155
  32. package/dist/bitwrench-lean.umd.min.js +7 -7
  33. package/dist/bitwrench-lean.umd.min.js.gz +0 -0
  34. package/dist/bitwrench-util-css.cjs.js +1 -1
  35. package/dist/bitwrench-util-css.cjs.min.js +1 -1
  36. package/dist/bitwrench-util-css.es5.js +1 -1
  37. package/dist/bitwrench-util-css.es5.min.js +1 -1
  38. package/dist/bitwrench-util-css.esm.js +1 -1
  39. package/dist/bitwrench-util-css.esm.min.js +1 -1
  40. package/dist/bitwrench-util-css.umd.js +1 -1
  41. package/dist/bitwrench-util-css.umd.min.js +1 -1
  42. package/dist/bitwrench-util-css.umd.min.js.gz +0 -0
  43. package/dist/bitwrench.cjs.js +621 -153
  44. package/dist/bitwrench.cjs.min.js +6 -6
  45. package/dist/bitwrench.cjs.min.js.gz +0 -0
  46. package/dist/bitwrench.css +1 -1
  47. package/dist/bitwrench.d.ts +18 -11
  48. package/dist/bitwrench.es5.js +647 -154
  49. package/dist/bitwrench.es5.min.js +6 -6
  50. package/dist/bitwrench.es5.min.js.gz +0 -0
  51. package/dist/bitwrench.esm.js +621 -153
  52. package/dist/bitwrench.esm.min.js +5 -5
  53. package/dist/bitwrench.esm.min.js.gz +0 -0
  54. package/dist/bitwrench.umd.js +621 -153
  55. package/dist/bitwrench.umd.min.js +6 -6
  56. package/dist/bitwrench.umd.min.js.gz +0 -0
  57. package/dist/builds.json +95 -95
  58. package/dist/bwserve.cjs.js +140 -7
  59. package/dist/bwserve.esm.js +141 -8
  60. package/dist/sri.json +45 -45
  61. package/docs/bitwrench-for-wasm.md +851 -0
  62. package/docs/bitwrench_api.md +133 -23
  63. package/docs/llm-bitwrench-guide.md +6 -5
  64. package/docs/state-management.md +27 -3
  65. package/docs/thinking-in-bitwrench.md +3 -2
  66. package/package.json +11 -9
  67. package/readme.html +17 -8
  68. package/src/bitwrench.d.ts +18 -11
  69. package/src/bitwrench.js +617 -148
  70. package/src/bwserve/bwclient.js +3 -3
  71. package/src/bwserve/client.js +26 -0
  72. package/src/bwserve/index.js +110 -3
  73. package/src/cli/attach.js +7 -5
  74. package/src/cli/serve.js +53 -10
  75. package/src/version.js +3 -3
@@ -0,0 +1,851 @@
1
+ # Bitwrench for WebAssembly
2
+
3
+ ## Table of Contents
4
+
5
+ 0. [The Thesis](#0-the-thesis)
6
+ 1. [The WASM/JS Boundary Problem](#1-the-wasmjs-boundary-problem)
7
+ 2. [TACO as a Rendering IDL](#2-taco-as-a-rendering-idl)
8
+ 3. [bwserve: The Browser as a Display Server](#3-bwserve-the-browser-as-a-display-server)
9
+ 4. [Architecture Patterns](#4-architecture-patterns)
10
+ 5. [Rust Integration](#5-rust-integration)
11
+ 6. [C/C++ Integration](#6-cc-integration)
12
+ 7. [AI and Agent-Driven UI](#7-ai-and-agent-driven-ui)
13
+ 8. [Why Not DOM Diffing?](#8-why-not-dom-diffing)
14
+ 9. [Comparison to Other Approaches](#9-comparison-to-other-approaches)
15
+ 10. [What You Get and What You Don't](#10-what-you-get-and-what-you-dont)
16
+
17
+ **Related docs:** [Thinking in Bitwrench](thinking-in-bitwrench.md) | [bwserve](bwserve.md) | [App Patterns](app-patterns.md) | [Embedded Tutorial](tutorial-embedded.md)
18
+
19
+ ---
20
+
21
+ ## 0. The Thesis
22
+
23
+ Systems programmers who need a web UI have several options: per-DOM-call bindings (`web-sys`/`wasm-bindgen`), shipping a full language runtime to the browser (Blazor), learning a WASM-native component framework (Yew, Leptos, Dioxus), or wrapping a browser in a desktop shell (Tauri, Electron). Each makes a different set of tradeoffs around payload size, boundary crossing cost, and how much of the browser platform is accessible.
24
+
25
+ This document describes a different tradeoff: **the WASM module produces UI as JSON data, and a JS-side library handles rendering.**
26
+
27
+ The WASM module produces TACO objects -- `{t, a, c, o}`, bitwrench's plain-object component format. It sends them across the WASM/JS boundary as structured data. Bitwrench on the JS side materializes them into DOM elements with CSS, accessibility, text selection, forms, and browser-native layout. Events flow back. Screenshots flow back. The WASM module does not interact with the DOM directly.
28
+
29
+ With bwserve, the protocol extends to a full display-server model -- analogous to X11/Wayland for web UIs. The WASM module (running in-page, in a Worker, or as a network server) sends rendering commands and receives user events through a message protocol.
30
+
31
+ This architecture also applies to **AI-driven interfaces**. A local WASM model (or an agent connected to one) can generate TACO specs, push them to the browser, observe the result via screenshots, and iterate.
32
+
33
+ > **The core idea: a WASM module that computes a state change already knows what changed. It can say so directly with a targeted patch, rather than re-describing the entire UI for a differ to reconcile.**
34
+
35
+ ---
36
+
37
+ ## 1. The WASM/JS Boundary Problem
38
+
39
+ WebAssembly runs in a sandboxed linear memory space. It cannot touch the DOM directly. Every interaction with the browser requires crossing a boundary -- calling a JS function from WASM or vice versa. This boundary is the central architectural constraint of any WASM-based web application.
40
+
41
+ ### The cost of crossing
42
+
43
+ Each boundary crossing has overhead: type marshaling, memory copying, function dispatch. The cost per crossing is small (~50-200ns), but it accumulates. A framework that crosses the boundary once per DOM operation -- one `createElement`, one `setAttribute`, one `appendChild` -- pays this cost hundreds or thousands of times per render.
44
+
45
+ ### How existing frameworks handle it
46
+
47
+ **Yew / Leptos / Dioxus (Rust):** These frameworks run a virtual DOM or reactive graph inside WASM and call `web-sys` bindings (which wrap `wasm-bindgen`) for each DOM mutation. A render of 100 elements means hundreds of boundary crossings: create element, set attribute, set text, append child, for each node. The frameworks optimize by batching and diffing, but the fundamental cost is per-DOM-operation.
48
+
49
+ **Blazor (.NET):** Ships the entire .NET runtime as WASM (~2-5MB download). Every DOM mutation crosses the boundary through a JS interop layer. The runtime overhead is substantial both in payload and per-operation cost.
50
+
51
+ **Raw `web-sys`:** The most direct approach. You call `document.create_element("div")` from Rust, which becomes a `wasm-bindgen` call, which becomes a JS function call, which calls the DOM API. This works and gives full control, but each DOM operation is a separate boundary crossing.
52
+
53
+ ### An alternative: batch the boundary
54
+
55
+ A different approach is to cross the boundary **once per render, not once per DOM operation.** The WASM module builds a TACO object (a JSON-serializable data structure) and sends it across the boundary as a single message. The JS-side library materializes the entire tree in one shot using `bw.createDOM()` or `bw.DOM()`.
56
+
57
+ ```
58
+ WASM side JS side (bitwrench)
59
+ --------- -------------------
60
+ Build TACO tree in linear memory |
61
+ Serialize to JSON string |
62
+ |
63
+ --- one boundary crossing ---> |
64
+ JSON.parse()
65
+ bw.DOM('#app', taco)
66
+ createElement('div')
67
+ setAttribute('class', 'card')
68
+ createTextNode('Hello')
69
+ appendChild(...)
70
+ ... (all native JS, zero crossings)
71
+ ```
72
+
73
+ 100 elements = 1 crossing, not 300+. The DOM operations happen inside JS where they're native function calls with zero marshaling overhead.
74
+
75
+ For incremental updates, the same principle applies. Instead of diffing an entire virtual tree across the boundary, the WASM module sends a targeted patch message: `{type: 'patch', target: '#counter', content: '42'}`. One crossing, one DOM mutation.
76
+
77
+ ---
78
+
79
+ ## 2. TACO as a Rendering IDL
80
+
81
+ An IDL (Interface Definition Language) defines a contract between two systems that may use different languages and memory models. Protocol Buffers are an IDL for RPC. OpenAPI is an IDL for REST APIs. TACO is effectively an **IDL for UI rendering.**
82
+
83
+ ### The format
84
+
85
+ ```javascript
86
+ {
87
+ t: 'div', // tag name
88
+ a: { class: 'card', id: 'main' }, // HTML attributes
89
+ c: [ // content: string, TACO, or array
90
+ { t: 'h1', c: 'Dashboard' },
91
+ { t: 'p', c: 'Status: online' }
92
+ ]
93
+ }
94
+ ```
95
+
96
+ Four keys. `t` is the tag. `a` is attributes. `c` is content. `o` is options (lifecycle hooks, state -- JS-side only). The first three are pure data. Any language that can produce JSON can produce a TACO.
97
+
98
+ ### Why this matters for WASM
99
+
100
+ A Rust struct, a C string buffer, a Python dict -- all can represent `{t, a, c}`. There is no special type system, no framework-specific abstractions, no closures or proxies to marshal. The TACO is data that describes UI. The renderer is a separate system that consumes that data.
101
+
102
+ Compare this to what you'd need to cross the boundary for other frameworks:
103
+
104
+ | Framework | What crosses the boundary | Serializable? |
105
+ |-----------|--------------------------|---------------|
106
+ | React | Virtual DOM tree with closures, refs, fiber nodes | No |
107
+ | Vue | Reactive proxy objects with dependency tracking | No |
108
+ | Svelte | Compiler-generated update functions | No (they're code) |
109
+ | Solid | Signal graph with closure-based subscriptions | No |
110
+ | **Bitwrench** | **JSON object: `{t, a, c}`** | **Yes** |
111
+
112
+ The other frameworks in this table were designed for same-runtime use -- the component model and the renderer share a JS heap. TACO was designed for cases where the producer and renderer are separated by a boundary (server-driven UI via bwserve, embedded devices like ESP32). That same property makes it usable across the WASM/JS boundary without adaptation.
113
+
114
+ ### The `o` split
115
+
116
+ The fourth key, `o:`, contains JavaScript-specific concerns: lifecycle hooks (`mounted`, `unmount`), component state (`state`, `render`), and handle methods (`handle`, `slots`). These are functions that can only live in JS.
117
+
118
+ For WASM, this split is a natural fit: the **specification** (what to render) crosses the boundary as data, while the **behavior** (DOM event handling, lifecycle management) stays in JS where DOM access is native.
119
+
120
+ For cases where the WASM module needs to define behavior, bwserve provides `register` and `call` -- send a function string once, invoke it by name later. This keeps the boundary thin while allowing the WASM side to install client-side logic when needed.
121
+
122
+ ---
123
+
124
+ ## 3. bwserve: The Browser as a Display Server
125
+
126
+ bwserve is bitwrench's server-driven UI protocol. It was originally designed for Node.js servers pushing UI to browsers. But the protocol is transport-agnostic -- any process that can send JSON messages can drive a bitwrench UI. Including a WASM module.
127
+
128
+ ### The protocol
129
+
130
+ Nine message types. Five for DOM operations, three for code execution, one for component dispatch:
131
+
132
+ | Type | What it does | Example |
133
+ |------|-------------|---------|
134
+ | `replace` | Replace element subtree | `{type:'replace', target:'#app', node:{t:'div', c:'Hello'}}` |
135
+ | `patch` | Update text or attributes | `{type:'patch', target:'counter', content:'42'}` |
136
+ | `append` | Add child element | `{type:'append', target:'#list', node:{t:'li', c:'New'}}` |
137
+ | `remove` | Delete element | `{type:'remove', target:'#old'}` |
138
+ | `batch` | Multiple operations | `{type:'batch', ops:[...]}` |
139
+ | `register` | Send named function | `{type:'register', name:'scroll', body:'function(s){...}'}` |
140
+ | `call` | Invoke function by name | `{type:'call', name:'scroll', args:['#chat']}` |
141
+ | `exec` | Run arbitrary JS | `{type:'exec', code:'document.title="Hi"'}` |
142
+ | `message` | Component dispatch | `{type:'message', target:'#card', action:'setTitle', data:'New'}` |
143
+
144
+ ### The full loop
145
+
146
+ ```
147
+ WASM Module (application) Browser (display server)
148
+ ------------------------- -----------------------
149
+ State + logic bitwrench + bwserve client
150
+ | |
151
+ |-- replace: full render --------> bw.apply() -> bw.DOM()
152
+ |-- patch: targeted update ------> bw.apply() -> bw.patch()
153
+ |-- call: scroll, focus, etc. ---> bw.apply() -> invoke function
154
+ |-- batch: multiple ops ---------> bw.apply() -> sequential
155
+ | |
156
+ <-- event: {target, type, data} -- subscribed listener fires
157
+ <-- screenshot: PNG buffer ------- html2canvas -> POST-back
158
+ ```
159
+
160
+ The browser renders what it's told, forwards user events back through the protocol, and can capture screenshots. The WASM module owns state, makes decisions, and pushes UI updates.
161
+
162
+ ### Where the WASM module runs
163
+
164
+ The bwserve protocol works regardless of where the WASM module lives:
165
+
166
+ **Same page (in-browser WASM):** The WASM module runs in the main thread or a Web Worker. Messages pass through `postMessage` or direct function calls. Lowest latency, simplest deployment -- one HTML file.
167
+
168
+ **Web Worker:** The WASM module runs in a dedicated Worker thread. Messages pass through `postMessage`. The UI thread stays responsive even during heavy computation. Good for CPU-intensive applications (simulation, ML inference, data processing).
169
+
170
+ **Service Worker:** The WASM module intercepts fetch requests and serves bwserve protocol responses. The page opens an EventSource to the Service Worker. Works offline once the Service Worker is installed.
171
+
172
+ **Network server:** The WASM module compiles to WASI and runs as an actual HTTP server (or compiles to native). The browser connects via SSE over the network. This is the standard bwserve architecture -- the transport just happens to be HTTP instead of in-process messaging.
173
+
174
+ All four deployments use the same protocol messages. The WASM module's code doesn't change -- only the transport layer differs.
175
+
176
+ ---
177
+
178
+ ## 4. Architecture Patterns
179
+
180
+ ### Pattern A: In-Page WASM with Direct Calls
181
+
182
+ The simplest pattern. The WASM module exposes functions that return TACO JSON strings. JavaScript calls them and renders the result.
183
+
184
+ ```
185
+ index.html
186
+ loads bitwrench.umd.js
187
+ loads app.wasm (via wasm-bindgen, emscripten, etc.)
188
+ calls wasm.render() -> returns TACO JSON -> bw.DOM('#app', JSON.parse(result))
189
+ on user event -> calls wasm.handle_event(event_data) -> returns patch JSON
190
+ ```
191
+
192
+ JavaScript glue (minimal):
193
+
194
+ ```javascript
195
+ // Glue code -- the only JS you write
196
+ import init, { render, handle_event } from './app.js'; // wasm-bindgen output
197
+
198
+ async function start() {
199
+ await init();
200
+
201
+ // Initial render -- WASM produces TACO, JS renders it
202
+ var taco = JSON.parse(render());
203
+ bw.DOM('#app', taco);
204
+
205
+ // Forward events to WASM by element ID.
206
+ // The WASM module produces elements with IDs; the glue
207
+ // routes events on those elements back to WASM.
208
+ bw.$('#app')[0].addEventListener('click', function(e) {
209
+ var id = e.target.id;
210
+ if (!id) return;
211
+
212
+ var event = JSON.stringify({
213
+ target: id,
214
+ type: 'click',
215
+ value: e.target.value || ''
216
+ });
217
+
218
+ var messages = JSON.parse(handle_event(event));
219
+ messages.forEach(function(msg) { bw.apply(msg); });
220
+ });
221
+ }
222
+
223
+ start();
224
+ ```
225
+
226
+ The Rust side (see Section 5 for full detail) builds JSON strings. The glue handles rendering and event forwarding.
227
+
228
+ ### Pattern B: Worker-Based WASM with Message Passing
229
+
230
+ For CPU-intensive applications. The WASM module runs in a Worker. The main thread handles rendering and event dispatch.
231
+
232
+ ```
233
+ main thread Worker thread
234
+ ----------- -------------
235
+ bitwrench.js app.wasm
236
+ bw.DOM(), bw.apply() state + logic
237
+ | |
238
+ |<-- postMessage: bwserve msg ------|
239
+ |--- postMessage: user action ----->|
240
+ ```
241
+
242
+ Main thread (glue):
243
+
244
+ ```javascript
245
+ var worker = new Worker('worker.js');
246
+
247
+ worker.onmessage = function(e) {
248
+ // Worker sends bwserve protocol messages
249
+ var messages = e.data;
250
+ messages.forEach(function(msg) { bw.apply(msg); });
251
+ };
252
+
253
+ // Forward user events to Worker by element ID
254
+ document.addEventListener('click', function(e) {
255
+ if (e.target.id) {
256
+ worker.postMessage({ target: e.target.id, type: 'click' });
257
+ }
258
+ });
259
+ ```
260
+
261
+ Worker:
262
+
263
+ ```javascript
264
+ // worker.js
265
+ importScripts('app_wasm_glue.js');
266
+
267
+ // WASM module sends rendering commands back to main thread
268
+ function send(messages) {
269
+ postMessage(messages);
270
+ }
271
+
272
+ onmessage = function(e) {
273
+ var result = wasm_handle_event(JSON.stringify(e.data));
274
+ send(JSON.parse(result));
275
+ };
276
+
277
+ // Initial render
278
+ wasm_init().then(function() {
279
+ send(JSON.parse(wasm_render()));
280
+ });
281
+ ```
282
+
283
+ The WASM module in the Worker has the same interface as Pattern A -- it produces JSON, the glue forwards it. The Worker boundary is invisible to both sides.
284
+
285
+ ### Pattern C: WASM Server via bwserve (Network)
286
+
287
+ The WASM module compiles to a server binary (native or WASI). It speaks HTTP and serves the bwserve protocol. The browser connects via SSE -- standard bwserve, no special WASM integration needed.
288
+
289
+ ```
290
+ WASM server binary Browser
291
+ (compiled to native) (any browser)
292
+ | |
293
+ |-- GET / -> HTML shell ---------->|
294
+ |-- GET /bw/events -> SSE -------->|
295
+ |-- SSE: replace/patch/etc ------->| bw.apply() -> DOM
296
+ |<-- POST /bw/return/action -------|
297
+ |<-- POST /bw/return/screenshot ---|
298
+ ```
299
+
300
+ This pattern has the broadest capabilities. The WASM module:
301
+ - Runs as a real server process (can access filesystem, databases, hardware)
302
+ - Gets auto-reconnect for free (SSE spec)
303
+ - Supports multiple browser clients simultaneously
304
+ - Can capture screenshots via `client.screenshot()`
305
+ - Works behind a reverse proxy, in Docker, on a Raspberry Pi
306
+
307
+ The same Rust/C++ code that runs as an in-page WASM module can compile to a native server binary with a different transport layer. The application logic is identical.
308
+
309
+ ---
310
+
311
+ ## 5. Rust Integration
312
+
313
+ ### Producing TACO from Rust
314
+
315
+ TACO is JSON. Rust's `serde_json` produces JSON. The integration is straightforward.
316
+
317
+ ```rust
318
+ use serde_json::json;
319
+
320
+ fn render_dashboard(state: &AppState) -> String {
321
+ json!({
322
+ "t": "div", "a": {"class": "bw-container"}, "c": [
323
+ {"t": "h1", "c": &state.title},
324
+ {"t": "div", "a": {"class": "bw-row"}, "c": [
325
+ stat_card("Users", &state.users.to_string(), "primary"),
326
+ stat_card("Revenue", &format!("${}", state.revenue), "success"),
327
+ stat_card("Orders", &state.orders.to_string(), "info")
328
+ ]},
329
+ {"t": "button", "a": {
330
+ "id": "refresh-btn",
331
+ "class": "bw-btn bw-btn-primary"
332
+ }, "c": "Refresh"}
333
+ ]
334
+ }).to_string()
335
+ }
336
+
337
+ fn stat_card(label: &str, value: &str, variant: &str) -> serde_json::Value {
338
+ json!({
339
+ "t": "div", "a": {"class": format!("bw-stat-card bw-stat-card-{}", variant)}, "c": [
340
+ {"t": "div", "a": {"class": "bw-stat-card-value"}, "c": value},
341
+ {"t": "div", "a": {"class": "bw-stat-card-label"}, "c": label}
342
+ ]
343
+ })
344
+ }
345
+ ```
346
+
347
+ ### A helper crate (optional)
348
+
349
+ For ergonomics, a thin wrapper around `serde_json::Value`:
350
+
351
+ ```rust
352
+ // taco.rs -- ~50 lines, not a framework
353
+
354
+ use serde_json::{json, Value};
355
+
356
+ pub fn tag(t: &str) -> Value {
357
+ json!({"t": t})
358
+ }
359
+
360
+ pub fn el(t: &str, attrs: Value, children: Vec<Value>) -> Value {
361
+ json!({"t": t, "a": attrs, "c": children})
362
+ }
363
+
364
+ pub fn text(t: &str, content: &str) -> Value {
365
+ json!({"t": t, "c": content})
366
+ }
367
+
368
+ pub fn div(attrs: Value, children: Vec<Value>) -> Value {
369
+ el("div", attrs, children)
370
+ }
371
+
372
+ pub fn button(label: &str, id: &str) -> Value {
373
+ json!({
374
+ "t": "button",
375
+ "a": {"id": id, "class": "bw-btn bw-btn-primary"},
376
+ "c": label
377
+ })
378
+ }
379
+
380
+ // bwserve protocol messages
381
+ pub fn msg_replace(target: &str, node: Value) -> Value {
382
+ json!({"type": "replace", "target": target, "node": node})
383
+ }
384
+
385
+ pub fn msg_patch(target: &str, content: &str) -> Value {
386
+ json!({"type": "patch", "target": target, "content": content})
387
+ }
388
+
389
+ pub fn msg_batch(ops: Vec<Value>) -> Value {
390
+ json!({"type": "batch", "ops": ops})
391
+ }
392
+ ```
393
+
394
+ With this helper:
395
+
396
+ ```rust
397
+ use taco::*;
398
+
399
+ fn render(state: &AppState) -> String {
400
+ msg_replace("#app", div(
401
+ json!({"class": "container"}),
402
+ vec![
403
+ text("h1", &state.title),
404
+ text("p", &format!("Count: {}", state.count)),
405
+ button("+1", "inc-btn"),
406
+ button("Reset", "reset-btn"),
407
+ ]
408
+ )).to_string()
409
+ }
410
+
411
+ fn handle_event(state: &mut AppState, target_id: &str) -> String {
412
+ match target_id {
413
+ "inc-btn" => {
414
+ state.count += 1;
415
+ msg_patch("count", &state.count.to_string()).to_string()
416
+ }
417
+ "reset-btn" => {
418
+ state.count = 0;
419
+ msg_patch("count", "0").to_string()
420
+ }
421
+ _ => "[]".to_string()
422
+ }
423
+ }
424
+ ```
425
+
426
+ The WASM module produces JSON and receives JSON. DOM bindings (`web-sys`) are not needed.
427
+
428
+ ### wasm-bindgen export (in-page pattern)
429
+
430
+ ```rust
431
+ use wasm_bindgen::prelude::*;
432
+ use std::sync::Mutex;
433
+
434
+ static STATE: Mutex<AppState> = Mutex::new(AppState::new());
435
+
436
+ #[wasm_bindgen]
437
+ pub fn render() -> String {
438
+ let state = STATE.lock().unwrap();
439
+ render_dashboard(&state)
440
+ }
441
+
442
+ #[wasm_bindgen]
443
+ pub fn handle_event(event_json: &str) -> String {
444
+ let event: serde_json::Value = serde_json::from_str(event_json).unwrap();
445
+ let target = event["target"].as_str().unwrap_or("");
446
+ let mut state = STATE.lock().unwrap();
447
+ handle_event_inner(&mut state, target)
448
+ }
449
+ ```
450
+
451
+ ### Axum/Actix server (network pattern)
452
+
453
+ The same rendering code compiles to a native server:
454
+
455
+ ```rust
456
+ use axum::{routing::get, Router};
457
+ use axum::response::sse::{Event, Sse};
458
+ use futures::stream::Stream;
459
+
460
+ async fn sse_handler() -> Sse<impl Stream<Item = Result<Event, std::io::Error>>> {
461
+ let state = AppState::new();
462
+ let initial = render_dashboard(&state);
463
+ let event = Event::default().data(initial);
464
+ // ... SSE stream setup
465
+ }
466
+
467
+ #[tokio::main]
468
+ async fn main() {
469
+ let app = Router::new()
470
+ .route("/bw/events", get(sse_handler))
471
+ .route("/", get(serve_shell));
472
+ // ...
473
+ }
474
+ ```
475
+
476
+ Same `render_dashboard()`, same `handle_event()`. Different transport. The rendering logic is independent of deployment target.
477
+
478
+ ---
479
+
480
+ ## 6. C/C++ Integration
481
+
482
+ C and C++ developers face the same boundary problem, often with tighter constraints -- embedded targets, no allocator, fixed-size buffers. Bitwrench includes a relaxed JSON format (`r-prefix`) that addresses C/C++ string-building ergonomics.
483
+
484
+ ### Relaxed JSON for C strings
485
+
486
+ Standard JSON in C requires escaping every double quote:
487
+
488
+ ```c
489
+ // Standard JSON -- escape nightmare
490
+ char msg[] = "{\"type\":\"patch\",\"target\":\"temp\",\"content\":\"23.5 C\"}";
491
+ ```
492
+
493
+ Bitwrench's r-prefix format uses single quotes:
494
+
495
+ ```c
496
+ // r-prefix relaxed JSON -- natural C strings
497
+ char msg[] = "r{'type':'patch','target':'temp','content':'23.5 C'}";
498
+ ```
499
+
500
+ `bw.parseJSONFlex()` on the browser side converts single quotes to double quotes before parsing. The `r` prefix signals the format.
501
+
502
+ ### C helper macros
503
+
504
+ ```c
505
+ #include <stdio.h>
506
+ #include <string.h>
507
+
508
+ // Simple TACO builder macros for fixed-size buffers
509
+ #define BW_PATCH(buf, target, content) \
510
+ snprintf(buf, sizeof(buf), \
511
+ "r{'type':'patch','target':'%s','content':'%s'}", \
512
+ target, content)
513
+
514
+ #define BW_REPLACE(buf, target, taco) \
515
+ snprintf(buf, sizeof(buf), \
516
+ "r{'type':'replace','target':'%s','node':%s}", \
517
+ target, taco)
518
+
519
+ // TACO builders
520
+ #define TACO_TEXT(buf, tag, text) \
521
+ snprintf(buf, sizeof(buf), "{'t':'%s','c':'%s'}", tag, text)
522
+
523
+ #define TACO_DIV(buf, cls, children) \
524
+ snprintf(buf, sizeof(buf), \
525
+ "{'t':'div','a':{'class':'%s'},'c':[%s]}", cls, children)
526
+
527
+ // Usage
528
+ void update_display(float temp, int humidity) {
529
+ char temp_str[16], humid_str[16];
530
+ char card1[128], card2[128], row[512], msg[1024];
531
+
532
+ snprintf(temp_str, sizeof(temp_str), "%.1f C", temp);
533
+ snprintf(humid_str, sizeof(humid_str), "%d%%", humidity);
534
+
535
+ TACO_TEXT(card1, "p", temp_str);
536
+ TACO_TEXT(card2, "p", humid_str);
537
+
538
+ snprintf(row, sizeof(row), "%s,%s", card1, card2);
539
+ TACO_DIV(msg, "readings", row);
540
+
541
+ // Send via whatever transport (HTTP response, WebSocket, serial)
542
+ send_to_browser(msg);
543
+ }
544
+ ```
545
+
546
+ ### Emscripten WASM
547
+
548
+ For C/C++ compiled to WASM via Emscripten:
549
+
550
+ ```c
551
+ #include <emscripten.h>
552
+
553
+ // Called from JS glue
554
+ EMSCRIPTEN_KEEPALIVE
555
+ const char* render() {
556
+ static char buf[4096];
557
+ float temp = read_sensor();
558
+
559
+ snprintf(buf, sizeof(buf),
560
+ "r{'type':'replace','target':'#app','node':"
561
+ "{'t':'div','c':["
562
+ "{'t':'h1','c':'Sensor Dashboard'},"
563
+ "{'t':'p','a':{'id':'temp'},'c':'%.1f C'},"
564
+ "{'t':'button','a':{'id':'toggle-btn','class':'bw-btn'},"
565
+ "'c':'Toggle LED'}"
566
+ "]}}", temp);
567
+
568
+ return buf;
569
+ }
570
+
571
+ EMSCRIPTEN_KEEPALIVE
572
+ const char* handle_event(const char* target_id) {
573
+ static char buf[256];
574
+
575
+ if (strcmp(target_id, "toggle-btn") == 0) {
576
+ toggle_led();
577
+ BW_PATCH(buf, "led-status", led_state() ? "ON" : "OFF");
578
+ }
579
+
580
+ return buf;
581
+ }
582
+ ```
583
+
584
+ The JS glue calls `render()` and `handle_event()`, gets back JSON strings, and passes them to `bw.apply()`. The C code does not interact with the DOM.
585
+
586
+ ---
587
+
588
+ ## 7. AI and Agent-Driven UI
589
+
590
+ ### The premise
591
+
592
+ A local WASM module (or a native process compiled from the same source) runs an AI model or hosts an agent. It needs to present a UI to the user. The UI is not a fixed layout with dynamic data -- it's a **dynamically generated interface** that the AI constructs based on context, user history, task state, and its own reasoning.
593
+
594
+ This is different from traditional web apps where the UI is designed at development time and data fills in at runtime. Here, the UI itself is generated at runtime.
595
+
596
+ ### TACO as AI output format
597
+
598
+ AI models typically output structured data -- JSON, function calls, tool use. TACO is structured data that maps directly to UI. The model needs to produce objects with four keys: `t`, `a`, `c`, `o`. No HTML syntax, JSX conventions, or framework-specific component APIs.
599
+
600
+ ```json
601
+ {"t": "div", "c": [
602
+ {"t": "h2", "c": "Analysis Results"},
603
+ {"t": "p", "c": "Found 3 anomalies in the dataset."},
604
+ {"t": "div", "a": {"class": "bw-card"}, "c": [
605
+ {"t": "h3", "c": "Anomaly #1"},
606
+ {"t": "p", "c": "Temperature spike at 14:32 UTC"}
607
+ ]},
608
+ {"t": "button", "a": {"id": "investigate-btn"}, "c": "Investigate"}
609
+ ]}
610
+ ```
611
+
612
+ This is a straightforward structured output for an LLM. The BCCL component vocabulary (makeCard, makeTable, makeAlert, makeStatCard) provides higher-level building blocks that a model can reference by name. The token count is lower than equivalent HTML or JSX because there is less syntactic overhead.
613
+
614
+ ### The visual feedback loop
615
+
616
+ bwserve's screenshot capability closes the loop. The AI generates UI, the browser renders it, the AI captures a screenshot, evaluates the result visually (via a vision model or heuristics), and refines:
617
+
618
+ ```
619
+ AI Model (WASM) Browser
620
+ ----------- -------
621
+ Generate TACO spec |
622
+ |-- replace: render UI --------> bw.DOM() -> visible page
623
+ | |
624
+ |-- screenshot request --------> html2canvas -> capture
625
+ <-- PNG image data -------------|
626
+ | |
627
+ Evaluate screenshot |
628
+ (too cramped? colors wrong? |
629
+ text overflow? wrong layout?) |
630
+ | |
631
+ Generate refined TACO spec |
632
+ |-- replace: updated UI -------> bw.DOM() -> improved page
633
+ ```
634
+
635
+ bwserve implements `client.screenshot()` for this purpose -- it sends a `call` message that triggers `html2canvas` on the client, which captures the DOM to a canvas, converts to PNG, and POSTs the image data back to the server. The server resolves a Promise with the image buffer.
636
+
637
+ ### Agent architecture
638
+
639
+ A more complete agent pattern:
640
+
641
+ ```
642
+ +-----------------+
643
+ | AI Agent (WASM)|
644
+ | |
645
+ | Reasoning loop:|
646
+ | 1. Observe | <-- screenshot, user actions
647
+ | 2. Plan | <-- internal state, goals
648
+ | 3. Act | --> TACO specs, patches
649
+ | 4. Evaluate | <-- screenshot feedback
650
+ +-----------------+
651
+ |
652
+ bwserve protocol
653
+ |
654
+ +-----------------+
655
+ | Browser |
656
+ | (display) |
657
+ | |
658
+ | bw.apply() |
659
+ | event dispatch |
660
+ | screenshot |
661
+ +-----------------+
662
+ ```
663
+
664
+ The agent observes the UI state (via screenshots or DOM queries), plans updates based on its goals, acts by sending bwserve messages, and evaluates the result.
665
+
666
+ ### Multi-surface agent UI
667
+
668
+ bwserve targets are CSS selectors. An agent can manage multiple independent UI regions:
669
+
670
+ ```rust
671
+ // Agent manages three surfaces simultaneously
672
+ fn agent_step(state: &AgentState) -> Vec<Value> {
673
+ let mut messages = vec![];
674
+
675
+ // Main content area -- task-specific UI
676
+ messages.push(msg_replace("#main", render_task_ui(&state.current_task)));
677
+
678
+ // Sidebar -- agent's reasoning visible to user
679
+ messages.push(msg_replace("#sidebar", render_reasoning_log(&state.log)));
680
+
681
+ // Status bar -- progress and controls
682
+ messages.push(msg_patch("status", &format!(
683
+ "Step {}/{} -- {}", state.step, state.total, state.phase
684
+ )));
685
+
686
+ messages
687
+ }
688
+ ```
689
+
690
+ Each surface is independently addressable. The agent can update the reasoning sidebar without touching the main content. Targeted patches mean only the changed region re-renders.
691
+
692
+ ### Local-first AI
693
+
694
+ A WASM-based AI agent running locally (in the browser or on localhost) has properties that cloud-based agents don't:
695
+
696
+ - **Privacy**: Data never leaves the device. The model runs in WASM, the UI runs in the browser, the communication is localhost or in-process.
697
+ - **Latency**: No network round-trip for UI updates. The WASM module and the browser are on the same machine. Sub-millisecond message delivery.
698
+ - **Offline**: Once the page is loaded and the WASM module is cached, everything works without a network connection.
699
+ - **Inspectable**: The bwserve protocol is JSON. You can log every message, replay sessions, debug with browser DevTools. The bwcli attach REPL can inspect the DOM in real time.
700
+
701
+ These properties are relevant for local AI assistants, on-device ML tools, privacy-sensitive applications, and developer tools where the data should not leave the machine.
702
+
703
+ ---
704
+
705
+ ## 8. Why Not DOM Diffing?
706
+
707
+ The virtual DOM was React's central contribution to UI architecture. The idea: the application describes the entire UI on every state change, and the framework diffs the new description against the previous one to determine the minimum set of DOM mutations. This relieves the developer from tracking what changed.
708
+
709
+ This is a reasonable tradeoff for a JavaScript-only application where closures and mutable state make dependency tracking hard. But the tradeoff looks different when the state authority and the renderer are separated by a WASM/JS boundary.
710
+
711
+ ### Diffing across the WASM boundary
712
+
713
+ A WASM module that manages application state in linear memory typically knows what changed -- it just ran the computation that caused the change. Re-describing the entire UI tree so a JS-side differ can identify the delta has several costs:
714
+
715
+ 1. **Wasteful**: Serialize the full tree across the WASM/JS boundary (O(n) data transfer) so the differ can do O(n) comparison to find the one thing that changed
716
+ 2. **Redundant**: The WASM module already knows the answer. It computed `state.count += 1`. It knows `#counter` needs to update to `"43"`.
717
+ 3. **Architecturally wrong**: The diff requires maintaining two copies of the virtual tree on the JS side -- the previous and the current. For a WASM app, that's state duplication across the boundary.
718
+
719
+ With bitwrench:
720
+
721
+ ```rust
722
+ // The WASM module knows exactly what changed
723
+ state.count += 1;
724
+ msg_patch("counter", &state.count.to_string()) // one message, one DOM update
725
+ ```
726
+
727
+ One patch message, one DOM mutation. The WASM module communicates what it already knows.
728
+
729
+ ### Historical context
730
+
731
+ Desktop UI toolkits (MFC, Swing, Qt, Cocoa) used targeted updates rather than tree diffing. In MFC, `SetWindowText("New Title")` repainted the control's title. In Swing, `label.setText("43")` updated one label. In Qt, signal-slot connections trigger specific updates. The application tracked what changed and told the framework directly.
732
+
733
+ bitwrench follows the same pattern: `el.bw.setTitle('New')` updates one component, `bw.patch('#counter', '43')` updates one element, `client.patch('counter', '43')` sends one message. The application (or the WASM module) is the source of truth about what changed.
734
+
735
+ React's diffing approach solved a real problem -- in large JS applications with shared mutable state, manually tracking dependencies is error-prone. Whether that tradeoff applies to a given WASM application depends on the application's complexity and state management approach.
736
+
737
+ ### When diffing would be needed
738
+
739
+ If your WASM module genuinely doesn't know what changed -- for example, if it receives an opaque data blob from an external source and re-derives the entire UI from scratch -- then a diff-based approach would be useful. bitwrench doesn't provide this. Some alternatives:
740
+
741
+ 1. Diff the **data** in WASM (compare old state to new state)
742
+ 2. Emit targeted patches for what changed
743
+ 3. Or, for small enough UIs, just re-render with `replace` -- `bw.DOM()` is fast enough for most subtrees
744
+
745
+ Diffing data in WASM (where you have typed structs and linear memory) is cheaper than diffing virtual DOM trees in JS (where everything is heap-allocated objects with GC pressure).
746
+
747
+ ---
748
+
749
+ ## 9. Comparison to Other Approaches
750
+
751
+ ### Yew / Leptos / Dioxus (Rust WASM frameworks)
752
+
753
+ These are the most direct alternatives. They run a component framework inside WASM and call `web-sys` for DOM operations.
754
+
755
+ | Concern | Yew/Leptos/Dioxus | bitwrench + WASM |
756
+ |---------|-------------------|------------------|
757
+ | Component model | Rust macros + framework | JSON objects (TACO) |
758
+ | DOM access | Per-operation via web-sys | Batched via bw.apply() |
759
+ | Boundary crossings | 100s per render | 1 per render |
760
+ | Virtual DOM / signals | In WASM (Rust) | None (explicit patches) |
761
+ | CSS | Framework-specific | bw.css() / bw.loadStyles() |
762
+ | Accessibility | Manual | Browser-native (real HTML) |
763
+ | Server rendering | Framework-specific SSR | bw.html() or bwserve |
764
+ | Learning curve | Rust + framework | Rust + TACO (4 keys) |
765
+ | Bundle size | ~200KB+ (framework + app) | ~40KB (bitwrench) + app WASM |
766
+
767
+ The fundamental difference is where the component model lives. Yew/Leptos/Dioxus place it in WASM (Rust macros, reactive primitives, virtual DOM). The bitwrench approach places it in JS (TACO rendering, CSS generation, component handles) and uses JSON as the interface between the application language and the renderer.
768
+
769
+ ### Blazor (.NET WASM)
770
+
771
+ | Concern | Blazor | bitwrench + WASM |
772
+ |---------|--------|------------------|
773
+ | Runtime payload | 2-5MB (.NET in WASM) | ~40KB (bitwrench) |
774
+ | Component model | Razor templates (.NET) | TACO JSON |
775
+ | DOM access | JS interop bridge | Batched via bw.apply() |
776
+ | Language | C# only | Any (Rust, C, C++, Go, ...) |
777
+ | Server mode | Blazor Server (SignalR) | bwserve (SSE) |
778
+
779
+ Blazor bundles a language runtime in WASM. The bitwrench approach does not require a language-specific runtime on the client -- the client library renders JSON regardless of what produced it.
780
+
781
+ ### Tauri / Electron
782
+
783
+ | Concern | Tauri/Electron | bitwrench + WASM |
784
+ |---------|---------------|------------------|
785
+ | Deployment | Desktop app installer | URL (web page) |
786
+ | Backend | Native process (Rust/Node) | WASM (in-browser or server) |
787
+ | Frontend | You write JS/React/Vue | TACO from any language |
788
+ | Distribution | App store / download | Link sharing |
789
+ | Bundle size | 10-100MB+ | Page weight (~40KB + WASM) |
790
+
791
+ Tauri and Electron are designed for desktop application distribution. If the UI can be served as a web page, the packaging and distribution step is not needed -- the tradeoff is losing native OS integration (system tray, menus, file system access without permissions).
792
+
793
+ ### Canvas-based (SDL + Emscripten, egui)
794
+
795
+ | Concern | Canvas/SDL/egui | bitwrench + WASM |
796
+ |---------|----------------|------------------|
797
+ | Rendering | Canvas pixels | Native HTML/CSS |
798
+ | Text selection | Not available | Browser-native |
799
+ | Accessibility | Not available | Browser-native (semantic HTML) |
800
+ | Forms/inputs | Custom reimplementation | Browser-native |
801
+ | CSS | Not applicable | Full CSS support |
802
+ | Right-click, find-in-page | Not available | Browser-native |
803
+
804
+ Canvas-based approaches bypass the browser's built-in UI capabilities. Generating real HTML elements preserves them, at the cost of not having pixel-level rendering control (which matters for games, CAD, and other graphics-intensive applications).
805
+
806
+ ---
807
+
808
+ ## 10. What You Get and What You Don't
809
+
810
+ ### What you get
811
+
812
+ - Browser-native UI (real HTML/CSS) from any language that produces JSON
813
+ - Batched boundary crossing -- one message per render, not one per DOM op
814
+ - 9-verb protocol (bwserve) for remote UI control including screenshots
815
+ - ~40KB client library with 30+ components, CSS generation, theming
816
+ - No build step on the JS side
817
+ - Same application code can target in-page WASM, Worker, or native server
818
+ - Browser capabilities preserved: accessibility, text selection, CSS layout, forms
819
+ - Relaxed JSON (r-prefix) for C/C++ string-building ergonomics
820
+ - Debug tools: bwcli attach REPL, protocol logging, DOM inspection
821
+
822
+ ### What you don't get
823
+
824
+ - **No automatic change detection.** You must know what changed and say so. (This is usually trivial in WASM where you just ran the mutation.)
825
+ - **No type-safe TACO builder in Rust/C** -- you're building JSON. A helper crate/header can add convenience, but there's no compile-time guarantee that your TACO is valid. (bitwrench is lenient -- invalid fields are silently ignored.)
826
+ - **No client-side routing from WASM.** The browser-side `bw.router()` exists for JS, but a WASM server would manage navigation via bwserve's `replace` messages.
827
+ - **No two-way data binding.** User events come back as structured messages (element ID, event type, value). You update your state and send a patch. This is the expected pattern for a boundary architecture.
828
+ - **No animation primitives from WASM.** CSS animations and transitions work (they're in the browser), but you'd define them via `bw.css()` on the JS side or in a stylesheet, not from WASM.
829
+
830
+ ### The mental model
831
+
832
+ Think of it this way:
833
+
834
+ - **Your WASM module** is the **application**. It owns state, runs logic, makes decisions.
835
+ - **Bitwrench** is the **display server**. It takes rendering commands and presents them to the user.
836
+ - **The bwserve protocol** is the **wire format** between them.
837
+ - **The browser** is the **display hardware**. It provides pixels, layout, input devices, accessibility.
838
+
839
+ This separation -- application, window manager, display -- is common in GUI architectures (X11, Wayland, Windows GDI). The adaptation for the web platform is that the wire format is JSON and the display server is a browser tab.
840
+
841
+ ---
842
+
843
+ **Related:**
844
+ - [Thinking in Bitwrench](thinking-in-bitwrench.md) -- core philosophy and TACO patterns
845
+ - [bwserve](bwserve.md) -- full protocol reference and server API
846
+ - [App Patterns](app-patterns.md) -- five canonical app architectures
847
+ - [Embedded Tutorial](tutorial-embedded.md) -- ESP32/Arduino integration (same patterns, C focus)
848
+ - [Component Library](component-library.md) -- all BCCL `make*()` factories
849
+ - [State Management](state-management.md) -- levels 0-2, pub/sub, handles
850
+
851
+ *Bitwrench is maintained by [Manu Chatterjee](https://github.com/deftio) (deftio). BSD-2-Clause license.*