kinogaki serve, kinogaki run

The two faces: serve and run

A project authors its view once. `kinogaki serve` ships it to a browser as WebAssembly. `kinogaki run` draws it in a native window. Same view vocabulary, same C++ widgets, two targets.

One view, two faces

A project authors its view once, a pure function of the store, and runs it two ways. kinogaki serve is the web face: a browser, over a WebSocket. kinogaki run is the native face: an OS-level window on the device. Both call the same view function and render it through the same C++ widgets. The view vocabulary is identical, so the output matches.

The project view

kinogaki init scaffolds a project with one editable view in app/main.py:

from kinogaki import board

def view(store, status=""):
    return board.board_view(store, status)

view(store, status) returns a kinogaki.view tree, which becomes a Document of widgets. It starts as the built-in issue board; you edit it to shape your app. Both faces resolve this one function and render whatever it returns.

kinogaki serve: the web face

serve runs a local server. It ships the project's view to the browser as a Prism Document over a WebSocket, and the browser draws it with Kinogaki UI compiled to WebAssembly on a <canvas> over WebGL2. This is the server-driven-UI loop: the server pushes views and diffs, the client reconciles and sends events back. It is the same widget and Canvas code that runs natively, compiled to a different target.

kinogaki run: the native face

run opens a Cocoa/Metal window on the device and draws the same view through kinogaki.ui, the native binding of the same C++ widgets. No browser, no socket: the view renders in process. The window gains the full widget set the web face has, because it is the same set. run is macOS only today, for the backend reason in platforms; serve is the cross-platform way to render the same view anywhere.

Why the two never diverge

There is one view vocabulary and one render path. A widget is a typed Element at a Path; WidgetView::setView turns that tree into drawn widgets. The only things that differ between the faces are the backend beneath (WebGL2 in the browser, Metal on the device) and the transport (a socket for the web, an in-process call for native). The view code never knows which face it is feeding, which is why proving the board in the browser also proves it natively, and why one app/main.py is enough for both.