@openthink/ui-leaf 0.5.0 → 0.6.1

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.
package/README.md CHANGED
@@ -78,7 +78,11 @@ Three reasons:
78
78
 
79
79
  ## How it works
80
80
 
81
- `mount()` compiles your view file once with `Bun.build`, injects data into `window.__UI_LEAF__`, starts a `Bun.serve` HTTP server, and opens the user's default browser. Mutations from the view POST back to a localhost endpoint with a per-launch random token; the runtime dispatches them to the handlers you registered. Browser tab close → heartbeat stops → server shuts down → `view.closed` resolves and your CLI continues.
81
+ `mount()` compiles your view file once with `Bun.build`, injects data into `window.__UI_LEAF__`, starts a `Bun.serve` HTTP server, and opens the user's default browser. Mutations from the view POST back to a localhost endpoint with a per-launch random token; the runtime dispatches them to the handlers you registered.
82
+
83
+ **Heartbeat and lifecycle.** The browser tab sends periodic heartbeats while the page is open. When heartbeats stop (tab closed or navigated away), `mount()` emits a `disconnected` event but the server stays running — the port, token, and in-memory data are all preserved. When a tab reconnects (heartbeat resumes), a `reconnected` event fires. Any data updates or view swaps that arrive during the disconnected window take effect on the next load. The mount terminates only on an explicit caller close (`view.close()` or the inbound `{type:"close"}` message), a signal (SIGINT/SIGTERM), or an internal error — and `view.closed` resolves to the reason (`"caller"`, `"signal"`, or `"error"`).
84
+
85
+ Multi-tab note: `disconnected` fires only when **all** open tabs go silent — the heartbeat is a single high-water mark across tabs, so closing one tab while another is open emits no event.
82
86
 
83
87
  The transport is HTTP + JSON over loopback. The token is in `window.__UI_LEAF__.token`, served inline in the compiled HTML — so the token only protects against drive-by cross-origin requests in the user's browser, not against other processes on the same machine. Any local process that can reach `127.0.0.1:<port>` can fetch `GET /`, grep the token out, and call `/mutate` or `/api/data` with it; treat any local process you don't trust as having the same access as the view. View bundling resolves React from `ui-leaf`'s installed location, so your project doesn't need to install React.
84
88
 
@@ -237,12 +241,22 @@ What the consumer CLI is responsible for (out of ui-leaf's scope):
237
241
  {"type":"result","id":1,"value":{"ok":true}}
238
242
  {"type":"error","id":2,"message":"…"}
239
243
  ```
244
+ - **Or live-update / control messages:**
245
+ ```json
246
+ {"version":"1","type":"update","data":{}}
247
+ {"version":"1","type":"view","source":"<tsx>"}
248
+ {"version":"1","type":"patch","data":{},"view":{"source":"<tsx>"}}
249
+ {"version":"1","type":"reopen"}
250
+ {"version":"1","type":"close"}
251
+ ```
240
252
  - **stdout (line-delimited JSON):**
241
253
  - `{"type":"ready","url":"http://127.0.0.1:54321","port":54321}` — emitted once when the dev server is up
242
254
  - `{"type":"mutate","id":1,"name":"refresh","args":{}}` — emitted when a view triggers a mutation; respond on stdin
243
- - `{"type":"closed"}` — emitted on natural close (browser tab closed, heartbeat timeout)
255
+ - `{"type":"disconnected"}` — browser tab stopped heartbeating; mount stays alive
256
+ - `{"type":"reconnected"}` — browser reconnected after a disconnect
257
+ - `{"type":"closed","reason":"caller"}` — mount terminated; reason is `caller` | `signal` | `error`
244
258
  - `{"type":"error","message":"…"}` — emitted on internal failure
245
- - **Lifecycle:** binary exits 0 on natural close, 1 on internal error; closing stdin from the parent triggers shutdown.
259
+ - **Lifecycle:** binary exits 0 on `closed`, 1 on internal error. The mount does **not** auto-terminate when the browser disconnects — only an explicit `{type:"close"}` message, stdin close, SIGINT/SIGTERM, or internal error terminates it. Closing stdin from the parent triggers a `caller` close.
246
260
 
247
261
  ### Minimal Bash example (read-only view, no mutations)
248
262
 
@@ -250,8 +264,10 @@ What the consumer CLI is responsible for (out of ui-leaf's scope):
250
264
  CONFIG='{"view":"spec","viewsRoot":"/abs/path/to/views","data":{"markdown":"# hi"},"port":0}'
251
265
  echo "$CONFIG" | ui-leaf mount
252
266
  # → {"type":"ready","url":"http://127.0.0.1:54321","port":54321}
253
- # (browser opens; user closes tab)
254
- # → {"type":"closed"}
267
+ # (browser opens; user closes tab → disconnected; mount stays running)
268
+ # → {"type":"disconnected"}
269
+ # (send {"type":"close"} on stdin to terminate)
270
+ # → {"type":"closed","reason":"caller"}
255
271
  ```
256
272
 
257
273
  ### Worked example with mutations
@@ -273,10 +289,18 @@ Child → parent stdout:
273
289
  Parent → child stdin (after handling the mutation):
274
290
  {"type":"result","id":1,"value":{"count":1}}
275
291
 
276
- (user closes tab)
292
+ (user closes tab → mount stays running)
293
+
294
+ Child → parent stdout:
295
+ {"type":"disconnected"}
296
+
297
+ (parent decides to shut down)
298
+
299
+ Parent → child stdin:
300
+ {"type":"close"}
277
301
 
278
302
  Child → parent stdout:
279
- {"type":"closed"}
303
+ {"type":"closed","reason":"caller"}
280
304
  ```
281
305
 
282
306
  Each pending mutation has a unique `id`. Multiple mutations can be in flight concurrently — match `result`/`error` responses by id.
@@ -285,8 +309,8 @@ Each pending mutation has a unique `id`. Multiple mutations can be in flight con
285
309
 
286
310
  - **Pass `viewsRoot` as an absolute path.** No `cwd/views` default games when invoked from another process.
287
311
  - **Pass `port: 0`.** ui-leaf asks the OS for a free port and reports it back in the `ready` event. Lets you run concurrent views without collision.
288
- - **Lower `heartbeatTimeoutMs`** (e.g. 5000) so orphaned ui-leaf children exit fast if your parent process dies. The default 75000 is tuned for human-direct use (survives one browser background-tab throttle) and is too long when a parent process is supervising.
289
- - **Kill the child on parent shutdown** rather than relying on heartbeat — `kill <pid>` from the parent. Closing stdin also triggers a clean shutdown.
312
+ - **Lower `heartbeatTimeoutMs`** (e.g. 5000) to get faster `disconnected` events. Note that heartbeat timeout no longer terminates the mount the mount only terminates on an explicit `{type:"close"}` message, stdin close, or signal. If you want fast shutdown on tab close, listen for `disconnected` and send `{type:"close"}` on stdin.
313
+ - **Kill the child on parent shutdown** rather than relying on heartbeat — `kill <pid>` from the parent, or close stdin (which triggers a `caller` close).
290
314
  - **Declare every mutation name** the view will call in the `mutations: []` array. The binary only routes mutations whose names appear in the list; calls to undeclared names get a 404 from `/mutate` with the standard "no mutation handler registered for X" error, and the view's `mutate()` promise rejects.
291
315
 
292
316
  ### Driving from Node via `mount()` directly
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openthink/ui-leaf",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "Customizable browser views, on demand, for any CLI.",
5
5
  "license": "MIT",
6
6
  "author": "Matt Pardini",