A Scala library in which an application is one program: it shows views and offers actions, and each face decides how — a desktop window, a terminal, a web page, HTML from a server, a REST API, an MCP server for an AI agent. The program names no face; a face is a handler of its one effect.
A program is a value. fill asks for a form made from a type, confirm for a yes or no, show tells; nothing says how. Which face runs it is said once, where it is run. The block is the README's, compiled and run in okay-faces' build (sbt docsCheck/test).
import okay.faces.*
import okay.effects.shape.Shape
case class Who(name: String)
val who = Shape.derived[Who].labelled("name", "Name") // one typed shape: the form, the JSON Schema, the decoder
val greeting: String ! Face = // Face alone is the row
fill[Who, Face](View.heading("Who are you?"), who).flatMap(w =>
confirm[Face](View.text(s"Greet ${w.name}?")).flatMap(yes =>
if yes then show[Face](View.text(s"Hello, ${w.name}!")).map(_ => "greeted") else pure("left")))
// every face is a handler of the one effect; the implementation is said once, where the program is run:
object AtTheTerminal:
import okay.effects.machine.Machine.self
import okay.faces.term.Terminal
def main(args: Array[String]): Unit = run(Terminal.console(greeting))
// or SwingFace.handle(greeting), McpFace.stdio(greeting), HtmlFace.serve(greeting, port, host),
// RestFace.serve(greeting, port, host), LaminarFace.mount(greeting) in a page
// the test face plays it by a script, and tells what was shown — the reference every other face agrees with:
import okay.effects.machine.Machine.self
import okay.faces.test.Plays
val plays = Plays()
plays.handle(greeting)(plays.Script.of(Chosen("submit", okay.effects.json.Json.obj("name" -> okay.effects.json.Json.Str("Ann"))),
Chosen.plain("yes"))) match
case plays.Played.Finished(views, a) => assert(a == "greeted" && views.last == View.text("Hello, Ann!"))
case other => assert(false, other.toString)
A form comes from one typed Shape: the same shape is the form a person fills, the JSON Schema an agent is given and the decoder of what comes back. A wrong value is refused with its problems and their paths, and the program stays on the same step.
Every face is a loop over one step machine — Waiting(view, actions, resume), Told(view, next), Done(a) — with the continuation kept. What a face adds is how it shows a view and how it reads a choice; the test face plays a program by a script, and every other face agrees with it.
| Face | Module | What it is |
|---|---|---|
| test | okay-faces-test | a script of choices drives the program; what was shown, asserted |
| terminal | okay-faces-term | a line dialogue on the console; ANSI when asked |
| Swing | okay-faces-swing | a window on the JVM, forms from the shape, dressed by a look |
| Laminar, DOM | okay-faces-laminar, -dom | Scala.js in a page — with Laminar, or with no library at all; the browser's back, focus, keys |
| remote | the same | the same page over a program on a server, the browser knowing no application |
| HTML | okay-faces-html | pages from the server, structural HTML5, no script; forms posted |
| REST | okay-faces-rest | the dialogue as a JSON API with links and affordances (HATEOAS) |
| MCP | okay-faces-mcp | the dialogue as tools for an AI agent, over stdio or HTTP with sessions |
How it looks is data: a Look of theme tokens and style rules, compiled by each face to CSS, Swing or ANSI. The examples in the repository — noughts and crosses, a to-do list — run on every face, each main one line.
The agent sees, at every step, exactly the actions a person would see — so it cannot do more than a person, and the boundary is the program, not a prompt.
The current step's actions are the tools, each with the JSON Schema of its shape, plus look and back. When the step changes, the list changes and the server says so (list_changed). For clients that read the list once, one tool act carries every action.
Each result is the next view twice: Markdown to read, and a structured object with the view and the actions available. A rejected call is an error result listing the problems with their paths; the step is unchanged.
stdio, or the protocol's Streamable HTTP: initialize opens a session (Mcp-Session-Id), DELETE ends it; answers as JSON, or as an event stream when the client takes one.
Elicitation — a question to the user in the middle of a call, such as a consent. The program's continuation is parked between HTTP requests, no thread kept; the call goes on when the answer comes.
A described effect becomes tools too (McpFace.domain): every operation {domain}_{operation} with its parameters' and result's schemas. MCP itself is okay-mcp: JSON-RPC as data, a server a program over a link.
Where a program runs is a deploying, not a face. One more main serves it on Cloudflare Workers, each session a Durable Object (module okay-faces-cloudflare):
Cloudflare.serve(Door.shell(…), Door.html(program), Door.rest(program), Door.mcp(program))
A session's continuation is code and is never stored. What is stored is data: the last point the program said it is at, and the requests since. A new object starts the program at the point and plays those few requests, and is where it was — a question asked, too. The thin page is told by its session's socket when the session moved elsewhere.
The other role: an effect described (okay-simple's Describe) is served with nothing more written. A type mapped by okay-store is described by its mapping, so it gets a REST API (RestDomains: the description at GET base, an operation at base/<name>) and MCP tools (McpFace.domain) — and is performed from afar by the same description (remote): a program in a page performs it, it happens on the server.
In a Worker or a Lambda, with the type's data in D1, Postgres or an object of its own: okay-serverless.