package miaou-driver-web
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>
Miaou web driver (xterm.js over WebSocket)
Install
dune-project
Dependency
Authors
Maintainers
Sources
v0.5.2.tar.gz
md5=60a3b9f181f24572a06a9492532bfdda
sha512=fcc35a275066be2900e6201782faf47503076fa4640f08cf78067835a6f447b74613009e55b2ac799adb7ca46f1bffa261fc5971753f2cc3c6bef327511c7ef6
doc/src/miaou-driver-web.driver/web_viewer.ml.html
Source file web_viewer.ml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198(*****************************************************************************) (* *) (* SPDX-License-Identifier: MIT *) (* Copyright (c) 2026 Mathias Bourgoin <mathias.bourgoin@atacama.tech> *) (* *) (*****************************************************************************) (** Standalone viewer-only HTTP+WebSocket server. Serves the xterm.js viewer page over HTTP and broadcasts raw ANSI frames to all connected viewer WebSockets. Designed to run alongside the headless driver so a human can observe an AI agent's TUI session in a browser. *) [@@@warning "-32-34-37-69"] type t = { mutex : Mutex.t; mutable viewers : Web_websocket.t list; port : int; mutable rows : int; mutable cols : int; mutable last_frame : string option; } let create ~port = { mutex = Mutex.create (); viewers = []; port; rows = 24; cols = 80; last_frame = None; } let add_viewer t ws = Mutex.lock t.mutex ; t.viewers <- ws :: t.viewers ; Mutex.unlock t.mutex let remove_viewer t ws = Mutex.lock t.mutex ; t.viewers <- List.filter (fun v -> v != ws) t.viewers ; Mutex.unlock t.mutex let lf_to_crlf s = let buf = Buffer.create (String.length s + 64) in String.iter (fun c -> if c = '\n' then Buffer.add_string buf "\r\n" else Buffer.add_char buf c) s ; Buffer.contents buf let dims_msg ~rows ~cols = Printf.sprintf {|{"type":"dimensions","rows":%d,"cols":%d}|} rows cols let broadcast t ~rows ~cols data = (* Cursor home + clear screen + data with \n → \r\n for xterm.js *) let frame = "\027[H\027[2J" ^ lf_to_crlf data in Mutex.lock t.mutex ; let dims_changed = t.rows <> rows || t.cols <> cols in t.rows <- rows ; t.cols <- cols ; t.last_frame <- Some frame ; t.viewers <- List.filter (fun ws -> if Web_websocket.is_closed ws then false else begin (try if dims_changed then Web_websocket.send_text ws (dims_msg ~rows ~cols) ; Web_websocket.send_text ws frame with _ -> ()) ; not (Web_websocket.is_closed ws) end) t.viewers ; Mutex.unlock t.mutex let url t = Printf.sprintf "http://127.0.0.1:%d/viewer" t.port (* HTTP response helpers (duplicated from Web_driver to avoid coupling) *) let serve_response conn ~status ~content_type body = let len = String.length body in let headers = Printf.sprintf "%s\r\n\ Content-Type: %s\r\n\ Content-Length: %d\r\n\ Connection: close\r\n\ \r\n" status content_type len in Eio.Flow.write (conn :> Eio.Flow.sink_ty Eio.Resource.t) [Cstruct.of_string (headers ^ body)] let serve_200 conn ~content_type body = serve_response conn ~status:"HTTP/1.1 200 OK" ~content_type body let serve_404 conn = serve_response conn ~status:"HTTP/1.1 404 Not Found" ~content_type:"text/plain" "Not Found" let extract_path request_line = let raw = match String.split_on_char ' ' request_line with | _ :: path :: _ -> path | _ -> "/" in match String.split_on_char '?' raw with first :: _ -> first | [] -> "/" let start ~sw ~net ~port () = let t = create ~port in let socket = Eio.Net.listen net ~sw ~reuse_addr:true ~backlog:5 (`Tcp (Eio.Net.Ipaddr.V4.any, port)) in Eio.Fiber.fork ~sw (fun () -> let rec accept_loop () = let conn, _addr = Eio.Net.accept ~sw socket in Eio.Fiber.fork ~sw (fun () -> try let br = Eio.Buf_read.of_flow ~max_size:(1024 * 1024) conn in let request_line = Eio.Buf_read.line br in let headers = Web_websocket.parse_headers br in let path = extract_path request_line in match path with | "/" | "/viewer" -> ( serve_200 conn ~content_type:"text/html" Web_assets.viewer_html ; try Eio.Flow.close conn with _ -> ()) | "/client.js" -> ( serve_200 conn ~content_type:"application/javascript" Web_assets.client_js ; try Eio.Flow.close conn with _ -> ()) | "/ws/viewer" -> ( let sink = (conn :> Eio.Flow.sink_ty Eio.Resource.t) in let write s = Eio.Flow.write sink [Cstruct.of_string s] in match Web_websocket.upgrade headers ~write with | None -> ( serve_404 conn ; try Eio.Flow.close conn with _ -> ()) | Some ws -> ( (* Send init: hide cursor, clear screen, home *) let init = "\027[?25l\027[2J\027[H" in Web_websocket.send_text ws init ; (* Send role message so client.js knows it's a viewer *) let role_msg = {|{"type":"role","role":"viewer"}|} in Web_websocket.send_text ws role_msg ; (* Send current dimensions so client resizes xterm.js *) Mutex.lock t.mutex ; let rows = t.rows in let cols = t.cols in let cached = t.last_frame in Mutex.unlock t.mutex ; Web_websocket.send_text ws (dims_msg ~rows ~cols) ; (* Replay last frame so viewer doesn't start blank *) (match cached with | Some frame -> ( try Web_websocket.send_text ws frame with _ -> ()) | None -> ()) ; add_viewer t ws ; (* Drain incoming messages until close *) (try let rec loop () = match Web_websocket.recv_text ws br with | None -> () | Some _ -> loop () in loop () with _ -> ()) ; remove_viewer t ws ; try Eio.Flow.close conn with _ -> ())) | _ -> ( serve_404 conn ; try Eio.Flow.close conn with _ -> ()) with | Eio.Io _ -> () | End_of_file -> () | _ -> ()) ; accept_loop () in accept_loop ()) ; t
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>