tui4s
Guide

The same app on the web

The assistant in a browser, from the same shared code, as static files.

The web main gives the layers of the browser and mounts the shared UI with kyo-ui's UI.runMount:

AssistantWebApp.scala
    run {
      val screen =
        UIWindow.size.map((w, h) => Surface.cells(w, h, Theme.web.font.size))
      for
          mode    <- AssistantWeb.mode
          storage <- AssistantWeb.storage(mode)
          keys     = AssistantWeb.keys(storage)
          _       <- UI.runStylesheet(Look.stylesheet(Look.Web))
          ui      <- Memo.run(
                       Env.runLayer(
                         Layer(storage),
                         Layer(keys: Secrets),
                         Wiring.conversations,
                         AssistantWeb.log,
                         Wiring.providers(
                           AssistantWeb.live,
                           FakeProvider.make(),
                           mode,
                         ),
                         AssistantWeb.surface(keys),
                       )(AssistantUi.run(mode, Kyo.unit, screen)),
                     )
          _       <- UI.runMount(ui, "#app")
      yield ()
      end for
    }

Try it

The demo opens the app with ?demo: it reads no key, it uses only the fake provider (no network), and it keeps its storage in memory, so each load starts again. It runs in a sandboxed frame, apart from the storage of this site. Do not type a real key into it.

Build it

bash scripts/web-build.sh                         # dist/assistant: index.html and main.js
python3 -m http.server -d dist/assistant 8000     # a static server for your own use
node scripts/web-smoke.mjs                        # the smoke test (Playwright and Chromium)

The web app is static files: one page and one script. There is no server of tui4s and no link to the terminal app. modules/assistant/js has the interpreters of the browser: FetchHttp (the HTTP seam on fetch), BrowserKeys (the keys in localStorage) and AssistantWeb (the layers).

One look for both platforms

The settings of the assistant switch the look on both platforms. Each look is a block for [data-theme="<name>"]:

Settings.scala
    /** The colours of all the looks for a terminal (`TuiApp.stylesheet`): the
      * root of the app selects one with its `data-theme`.
      */
    val palette: Stylesheet = Theme.palette(Terminal.theme, Web.theme)

    /** All of all the looks for the web (`UI.runStylesheet`); `first` is the
      * look of the page outside the app.
      */
    def stylesheet(first: Look): Stylesheet =
      Theme.stylesheet(first.theme, values.filterNot(_ == first).map(_.theme)*)

The sizes of the shared UI are in cells in a terminal, and tui4s reads one em as one cell. A browser reads one em as the size of the font. So the UI takes its sizes from Surface, a platform service: surface.rows(n) is n cells in a terminal and n lines of text in a browser.

Your key

Your key stays in this browser. Extensions and anyone with this profile can read it. The page keeps the key in localStorage (one entry for each provider) and sends it only to the host of its provider (the page policy lets it call no other host). The page never shows the key, never writes it into the page (the key field is not bound to a value), and never logs it. For Anthropic, the page sends the header anthropic-dangerous-direct-browser-access: a browser call to the API needs it, and its risk is the one above (a script or an extension in the page can read the key). Use a key with a low spend limit, and push Forget when you are done. The key belongs to the origin that serves the page (for example localhost:8000): each page of that origin can read it. Use that origin only for this app, or push Forget before you serve another page there.

The keys of the web

A browser acts on most Ctrl chords on Windows and Linux, and kyo-ui cannot stop that. So each command has an Alt chord: Alt-P (the commands), Alt-N (a new chat), Alt-, (the settings), Alt-? (the help). Enter sends. Shift-Enter starts a new line. Esc stops a reply and closes the settings, the help and the command palette.

Copyright © 2026