···11+# Widget Support Plan of Record
22+33+*Created: 2026-02-12*
44+55+## Goals
66+77+Add Jupyter-style interactive widget support to js_top_worker, enabling OCaml
88+code running in the toplevel to create interactive UI elements (sliders, buttons,
99+dropdowns, etc.) that communicate bidirectionally with the frontend.
1010+1111+## Design Principles
1212+1313+1. **Zero new dependencies in the worker** - Every dependency compiled into the
1414+ worker can conflict with libraries the user wants to load at runtime. Widget
1515+ support must not add any new OCaml dependencies to the worker.
1616+1717+2. **Broad OCaml version compatibility** - The project currently targets OCaml
1818+ >= 4.04. New features must not raise this floor unnecessarily.
1919+2020+3. **Build on the message protocol, not RPC** - The worker already uses the
2121+ message-based protocol (`message.ml` / `js_top_worker_client_msg.ml`). Widget
2222+ communication extends this protocol rather than the legacy JSON-RPC layer.
2323+2424+4. **Remove, don't accumulate** - The legacy rpclib-based communication layer
2525+ should be removed as part of this work, reducing the dependency footprint.
2626+2727+## Why Not CBOR?
2828+2929+The architecture document previously listed CBOR as a planned transport format.
3030+After investigation, we've decided against it:
3131+3232+- **Dependency risk**: Even the lightweight `cbor` opam package brings in
3333+ `ocplib-endian`. Any dependency in the worker namespace can conflict with
3434+ user-loaded libraries.
3535+- **Unnecessary complexity**: The existing message protocol uses `Js_of_ocaml`'s
3636+ native JSON handling (`Json.output` / `Json.unsafe_input`), which has zero
3737+ additional dependencies.
3838+- **Binary data via Typed Arrays**: For binary payloads (images, etc.),
3939+ `js_of_ocaml`'s `Typed_array` module provides native browser typed array
4040+ support without any extra libraries.
4141+- **JSON is the browser's native format** - No encoding/decoding overhead when
4242+ passing structured data via `postMessage`.
4343+4444+## Communication Architecture
4545+4646+### Current State (Two Parallel Layers)
4747+4848+```
4949+1. Legacy RPC (to be removed):
5050+ Client (js_top_worker_client.ml) <-> JSON-RPC <-> Server (Toplevel_api_gen)
5151+ Dependencies: rpclib, rpclib-lwt, rpclib.json, ppx_deriving_rpc
5252+5353+2. Message protocol (to be extended):
5454+ Client (js_top_worker_client_msg.ml) <-> JSON messages <-> Worker (worker.ml)
5555+ Dependencies: js_of_ocaml (already required)
5656+```
5757+5858+### Target State
5959+6060+```
6161+Client (js_top_worker_client_msg.ml) <-> JSON messages <-> Worker (worker.ml)
6262+ | |
6363+ |-- Request/Response (existing: eval, complete, errors, ...) |
6464+ |-- Push messages (existing: output_at streaming) |
6565+ |-- Widget messages (NEW: comm_open, comm_update, comm_msg, ...) |
6666+```
6767+6868+All communication uses the existing `message.ml` protocol extended with widget
6969+message types. No new serialization libraries.
7070+7171+## Widget Protocol Design
7272+7373+### Message Types (Worker -> Client)
7474+7575+```
7676+CommOpen { comm_id; target; state } -- Widget created by OCaml code
7777+CommUpdate { comm_id; state } -- Widget state changed
7878+CommClose { comm_id } -- Widget destroyed
7979+```
8080+8181+### Message Types (Client -> Worker)
8282+8383+```
8484+CommMsg { comm_id; data } -- Frontend event (click, value change)
8585+CommClose { comm_id } -- Frontend closed widget
8686+```
8787+8888+### Widget State Format
8989+9090+Widget state is a JSON object with well-known keys, following the Jupyter widget
9191+convention where practical:
9292+9393+```json
9494+{
9595+ "widget_type": "slider",
9696+ "value": 50,
9797+ "min": 0,
9898+ "max": 100,
9999+ "step": 1,
100100+ "description": "Threshold",
101101+ "disabled": false
102102+}
103103+```
104104+105105+The `widget_type` field replaces Jupyter's `_model_module` / `_model_name` /
106106+`_view_module` / `_view_name` quartet, since we don't need the npm module
107107+indirection - our widget renderers are built into the client.
108108+109109+### Alignment with Jupyter Protocol
110110+111111+We adopt the **concepts** from the Jupyter widget protocol but simplify the
112112+implementation:
113113+114114+| Jupyter Concept | Our Equivalent |
115115+|-----------------|----------------|
116116+| comm_open | CommOpen message |
117117+| comm_msg method:"update" | CommUpdate message |
118118+| comm_msg method:"custom" | CommMsg message |
119119+| comm_close | CommClose message |
120120+| _model_module + _model_name | widget_type string |
121121+| buffer_paths (binary) | Typed_array via js_of_ocaml |
122122+| Display message | CommOpen includes display flag |
123123+124124+We do **not** implement:
125125+- echo_update (single frontend, no multi-client sync needed)
126126+- request_state / request_states (state is authoritative in worker)
127127+- Version negotiation (internal protocol, not cross-system)
128128+129129+## OCaml Widget API
130130+131131+User-facing API available as an OCaml library in the toplevel:
132132+133133+```ocaml
134134+module Widget : sig
135135+ type t
136136+137137+ (** Create a widget. Returns it and displays it. *)
138138+ val slider : ?min:int -> ?max:int -> ?step:int ->
139139+ ?description:string -> int -> t
140140+141141+ val button : ?style:string -> string -> t
142142+143143+ val text : ?placeholder:string -> ?description:string -> string -> t
144144+145145+ val dropdown : ?description:string -> options:string list -> string -> t
146146+147147+ val checkbox : ?description:string -> bool -> t
148148+149149+ val html : string -> t
150150+151151+ (** Read current value *)
152152+ val get : t -> Yojson.Safe.t (* or a simpler JSON type *)
153153+154154+ (** Update widget state *)
155155+ val set : t -> string -> Yojson.Safe.t -> unit
156156+157157+ (** Register event handler *)
158158+ val on_change : t -> (Yojson.Safe.t -> unit) -> unit
159159+ val on_click : t -> (unit -> unit) -> unit
160160+161161+ (** Display / close *)
162162+ val display : t -> unit
163163+ val close : t -> unit
164164+end
165165+```
166166+167167+**Important**: This API library (`widget` or similar) runs *inside* the toplevel
168168+and must have minimal dependencies. It communicates with the frontend by pushing
169169+messages through the same channel as `Mime_printer`.
170170+171171+## Code Removal Plan
172172+173173+### Files to Remove
174174+175175+| File | Reason |
176176+|------|--------|
177177+| `idl/transport.ml`, `transport.mli` | JSON-RPC transport wrapper |
178178+| `idl/js_top_worker_client.ml`, `.mli` | RPC-based Lwt client |
179179+| `idl/js_top_worker_client_fut.ml` | RPC-based Fut client |
180180+| `idl/_old/` directory | Historical RPC reference code |
181181+182182+### Dependencies to Remove
183183+184184+| Package | Used By |
185185+|---------|---------|
186186+| `rpclib` | transport.ml, RPC clients |
187187+| `rpclib-lwt` | impl.ml (IdlM module) |
188188+| `rpclib.json` | transport.ml, RPC clients |
189189+| `ppx_deriving_rpc` | toplevel_api.ml code generation |
190190+| `xmlm` | transitive via rpclib |
191191+| `cmdliner` | transitive via rpclib |
192192+193193+### Files to Refactor
194194+195195+| File | Change |
196196+|------|--------|
197197+| `lib/impl.ml` | Remove `IdlM` / `Rpc_lwt` usage, use own types |
198198+| `idl/toplevel_api.ml` | Keep type definitions, remove RPC IDL machinery |
199199+| `idl/toplevel_api_gen.ml` | Replace with hand-written types (no ppx_deriving_rpc) |
200200+| `idl/dune` | Remove rpclib library deps and ppx rules |
201201+| `lib/dune` | Remove rpclib-lwt dep |
202202+| `test/node/*.ml` | Migrate from RPC Server/Client to message protocol |
203203+| `test/browser/client_test.ml` | Use message-based client |
204204+| `example/unix_worker.ml` | Use message protocol over Unix socket |
205205+| `example/unix_client.ml` | Use message protocol over Unix socket |
206206+207207+### Impact on `toplevel_api_gen.ml`
208208+209209+The generated file is 92k+ lines (from ppx_deriving_rpc). The types it defines
210210+are used extensively in `impl.ml` and `worker.ml`. The plan:
211211+212212+1. Extract the **type definitions** into a new lightweight module (no ppx)
213213+2. Hand-write any needed serialization for the message protocol
214214+3. Remove `ppx_deriving_rpc` dependency entirely
215215+4. Delete `toplevel_api_gen.ml`
216216+217217+## Testing Strategy
218218+219219+### Existing Test Infrastructure
220220+221221+| Backend | Location | Framework |
222222+|---------|----------|-----------|
223223+| Unit tests | `test/libtest/` | ppx_expect |
224224+| Node.js | `test/node/` | js_of_ocaml + Node |
225225+| Unix (cram) | `test/cram/` | Cram tests with unix_worker |
226226+| Browser | `test/browser/` | Playwright + Chromium |
227227+228228+### Widget Testing Approach
229229+230230+#### 1. Unit Tests (`test/libtest/`)
231231+232232+- Widget state management logic
233233+- Message serialization/deserialization for widget messages
234234+- CommManager state tracking (open/update/close lifecycle)
235235+236236+#### 2. Node.js Tests (`test/node/`)
237237+238238+- Widget creation produces correct CommOpen messages
239239+- Widget state updates produce CommUpdate messages
240240+- Event handler registration and dispatch
241241+- Widget close cleanup
242242+- Multiple simultaneous widgets
243243+- Widget interaction with regular exec output (mime_vals + widgets)
244244+245245+#### 3. Cram Tests (`test/cram/`)
246246+247247+- Unix worker handles widget messages over socket
248248+- Widget lifecycle via command-line client
249249+- Widget messages interleaved with regular eval output
250250+251251+#### 4. Browser Tests (`test/browser/`)
252252+253253+- **End-to-end widget rendering**: OCaml creates widget -> message sent ->
254254+ client renders DOM element -> user interaction -> event sent back -> OCaml
255255+ handler fires
256256+- **Widget types**: Test each widget type (slider, button, text, dropdown,
257257+ checkbox, html)
258258+- **State synchronization**: Frontend changes propagated to worker and back
259259+- **Multiple widgets**: Several widgets active simultaneously
260260+- **Widget cleanup**: Closing widgets removes DOM elements
261261+- **Integration with existing features**: Widgets alongside code completion,
262262+ error reporting, MIME output
263263+264264+### Test Utilities
265265+266266+A shared test helper module for widget testing:
267267+268268+```ocaml
269269+(* test/test_widget_helpers.ml *)
270270+val assert_comm_open : worker_msg -> comm_id:string -> widget_type:string -> unit
271271+val assert_comm_update : worker_msg -> comm_id:string -> key:string -> unit
272272+val assert_comm_close : worker_msg -> comm_id:string -> unit
273273+val simulate_event : comm_id:string -> data:string -> client_msg
274274+```
275275+276276+## Example Widgets
277277+278278+### Priority 1: Core Widgets (Implement First)
279279+280280+These are the most commonly used Jupyter widgets and cover the fundamental
281281+interaction patterns:
282282+283283+| Widget | State | Events | Jupyter Equivalent |
284284+|--------|-------|--------|--------------------|
285285+| IntSlider | value, min, max, step | on_change(int) | IntSlider |
286286+| Button | description, style | on_click | Button |
287287+| Text | value, placeholder | on_change(string) | Text |
288288+| Dropdown | value, options | on_change(string) | Dropdown |
289289+| Checkbox | value, description | on_change(bool) | Checkbox |
290290+| HTML | value (html string) | none | HTML |
291291+292292+### Priority 2: Composition Widgets
293293+294294+| Widget | Purpose | Jupyter Equivalent |
295295+|--------|---------|-------------------|
296296+| HBox / VBox | Layout containers | HBox / VBox |
297297+| Output | Capture stdout/display | Output |
298298+| FloatSlider | Decimal slider | FloatSlider |
299299+300300+### Priority 3: Domain-Specific Widgets
301301+302302+| Widget | Purpose | Inspired By |
303303+|--------|---------|-------------|
304304+| Plot | Simple 2D charts (SVG) | bqplot (simplified) |
305305+| Table | Data grid display | ipydatagrid (read-only) |
306306+| Image | Display image bytes | Image widget |
307307+308308+### Example: Interactive Slider
309309+310310+```ocaml
311311+(* User code in toplevel *)
312312+let threshold = Widget.slider ~min:0 ~max:100 ~description:"Threshold" 50;;
313313+314314+Widget.on_change threshold (fun v ->
315315+ let n = Widget.Int.of_json v in
316316+ Printf.printf "Threshold changed to: %d\n" n
317317+);;
318318+```
319319+320320+### Example: Button with Output
321321+322322+```ocaml
323323+let count = ref 0;;
324324+let label = Widget.html (Printf.sprintf "<b>Count: %d</b>" !count);;
325325+let btn = Widget.button "Increment";;
326326+327327+Widget.on_click btn (fun () ->
328328+ incr count;
329329+ Widget.set label "value"
330330+ (`String (Printf.sprintf "<b>Count: %d</b>" !count))
331331+);;
332332+```
333333+334334+### Example: Linked Widgets
335335+336336+```ocaml
337337+let slider = Widget.slider ~min:0 ~max:255 ~description:"Red" 128;;
338338+let preview = Widget.html {|<div style="width:50px;height:50px"></div>|};;
339339+340340+Widget.on_change slider (fun v ->
341341+ let r = Widget.Int.of_json v in
342342+ Widget.set preview "value"
343343+ (`String (Printf.sprintf
344344+ {|<div style="width:50px;height:50px;background:rgb(%d,0,0)"></div>|} r))
345345+);;
346346+```
347347+348348+## Implementation Phases
349349+350350+### Phase 1: RPC Removal & Type Cleanup
351351+352352+Remove the legacy RPC layer and establish clean type definitions.
353353+354354+### Phase 2: Widget Message Protocol
355355+356356+Extend `message.ml` and `worker.ml` with widget message types.
357357+358358+### Phase 3: Widget Manager (Worker Side)
359359+360360+Implement the comm manager that tracks widget state and routes events.
361361+362362+### Phase 4: OCaml Widget API
363363+364364+Create the user-facing `Widget` module available in the toplevel.
365365+366366+### Phase 5: JavaScript Widget Renderer
367367+368368+Implement widget rendering in the JavaScript client.
369369+370370+### Phase 6: Testing & Examples
371371+372372+Full test coverage across all backends, example widgets, documentation.
373373+374374+## Open Questions
375375+376376+1. **JSON representation in OCaml API**: Use `Yojson.Safe.t`? A custom minimal
377377+ JSON type? Raw `Js.Unsafe.any`? (Yojson adds a dependency; custom type is
378378+ more work; Unsafe.any is untyped.)
379379+380380+2. **Widget library loading**: Should the Widget module be preloaded in the
381381+ worker, or loaded on demand via `#require "widget"`?
382382+383383+3. **Layout model**: How much of Jupyter's CSS-based layout model to support?
384384+ Full flexbox control per-widget, or simpler HBox/VBox only?
385385+386386+4. **Persistence**: Should widget state survive cell re-execution? Jupyter
387387+ widgets are destroyed and recreated; we could do the same or preserve state.