Design: the UIs of riff
This page is the design of the UI architecture of riff (#982). It
decides the core library, the list of UIs and their order. Each UI has
a child design of its own. Only the CLI is built: the work on the rest
starts with the core library (see The order). Until then,
riff --help does not show riff device, and riff with no command
prints the help. The requirements of the UIs
hold each decision.
| UI | How a person starts it | Toolkit | Child design |
|---|---|---|---|
| The CLI | riff COMMAND | clap | This page |
| The TUI | riff with no command, in a terminal | ratatui | #994 |
| The phone app | The app on an Android phone | Slint | #951: riff on a phone |
| The web UI | A browser | Slint, built to WebAssembly | #995 |
| The desktop GUI | An app on the desktop | Slint | #996 |
The rules for each UI
- All UI code is Rust.
- No JavaScript, no JavaScript framework (React, Vue and the like), no UI written in HTML and styled with CSS, and no npm tooling.
- A Rust app that builds to WebAssembly can run in a browser, when its Rust toolkit makes the small HTML, JavaScript or CSS that starts it.
- The phone app is for Android.
- riff-server stays on Google Cloud Run for now (#953).
The crates and the UIs
Each UI is a thin layer over one core library: the new crate
riff-client.
flowchart TB
subgraph T["in a terminal: the crate riff"]
CLI["riff COMMAND<br/>the CLI: plain lines, --format json"]
TUI["riff<br/>the TUI: ratatui"]
MCP["riff relay mcp<br/>the riff tools of a session"]
end
subgraph G["the graphical UIs: Slint"]
PH["the phone app<br/>Android"]
WEB["the web UI<br/>WebAssembly in a browser"]
DT["the desktop GUI"]
SC["the shared screens"]
PH --> SC
WEB --> SC
DT --> SC
end
CLI --> C
TUI --> C
MCP --> C
SC --> C
C["riff-client<br/>the client of riff-server: the sign-in, the tokens,<br/>the API calls, the watch stream<br/>the models of the views: the sessions, the threads,<br/>the waves, the machines"]
C --> K["riff-core<br/>the model that the clients and riff-server share"]
C -- "HTTPS" --> S["riff-server<br/>on Cloud Run"]
S --> K
D["the riff daemon<br/>of this machine"] -- "the container sessions" --> T
riff-corestays the model that the clients and riff-server share: the session URIs, the selectors, the wire types of the HTTP API, the device keys and the DPoP proofs, the signed messages, the records of the log and the build check.riff-clientholds the client of riff-server (#997): the sign-in, the tokens, the API calls and the watch stream. Its modules areapi,link,login,device,secretsandmarks. The browser step ofriff loginstays in the crateriff.riff-clientholds no terminal code. It builds for Linux, for Android and for WebAssembly (#999).riff-clientholds the models of the views (#998), as plain data with serde. See The models of the views.- A part that is not the same on each platform is a trait that each UI
gives:
riff_client::platform::Platform. An example is the store of the tokens and of the device key: the OS keyring on a machine, and the Keystore on Android.riffgives its parts at its start: the OS keyring, its local folders, and its lines on the terminal. - The socket of the riff daemon stays in the crate
riff. A terminal UI gives the container sessions of the daemon of its machine to the sessions model ofriff-client.
The models of the views
riff-client makes each model from the replies of riff-server, and
from what the UI gives. A UI only draws a model: the plain lines, the
colors and the ratatui widgets stay in the crate riff.
| Model | Module | Holds | Drawn by |
|---|---|---|---|
| The sessions | sessions | The sessions of riff-server and the container sessions of the riff daemon, one entry for each session ID | riff top, riff view |
| The threads | threads | One line for each message: the sender, the receivers, the mark verified, the kind and the body | riff read, riff tail, riff chat |
| The waves | waves | The open issues, the board of the current wave, the open waves | riff top, riff wave list |
| The machines | machines | The numbers of each host: the limit, the workers, the machine, the disk and the monitor | riff top |
flowchart LR
S["riff-server"] --> M["riff-client:<br/>the models"]
D["the riff daemon<br/>of this machine"] -- "the container sessions" --> R["riff"]
G["gh: the forge"] --> R
R -- "what the UI gives" --> M
M --> V["riff draws:<br/>plain lines, colors, widgets"]
A container session that is also a session of riff-server is one entry
of the sessions model, with the state of its container. Today riff top
gives the model no container session, and riff view gives it no
session of riff-server. So each command prints what it printed before.
One sessions view
Each UI shows the sessions in one view: the sessions on riff-server and
the container sessions of the daemons, in one list. Today two commands
show them: riff top (see
See what each session does)
and riff view (see Watch the riff).
They merge. The sessions tab of the TUI does what each of them does:
the live table, the board of each repository, and the attach to a
session.
The CLI
The CLI stays simple and complete. It serves scripts and agents (through the MCP server).
- Each command prints plain lines that are easy to
grep. - Each command that prints data takes
--format text(the default) or--format json. serde makes the JSON from the types of the data. riff topprints one look of the sessions and ends, asriff top --oncedoes today. The live view moves to the TUI. The TUI design can giveriff topa plain alias, for exampleriff stats.
The TUI
riff with no command starts the TUI in a terminal. With no terminal,
it prints the help of the CLI. riff --help stays the help of the
CLI, and says that riff with no command starts the TUI.
riff COMMAND stays the CLI. If a bare riff makes the CLI
awkward, the TUI is riff ui. The TUI design (#994) decides.
The TUI is a ratatui app with a tab for each area. Each action of the TUI calls the same code as its CLI command.
flowchart LR
R["riff"] --> TABS{"a tab for each area"}
TABS --> S["Sessions: the sessions on riff-server<br/>and the container sessions of the daemons"]
TABS --> CH["Chat: the thread chat"]
TABS --> W["Waves: the items, the claims,<br/>the pull requests, the verifies"]
TABS --> M["Machines: the hosts, the daemons,<br/>the limits"]
TABS --> DV["Devices: list, add with a QR code,<br/>remove"]
TABS --> RP["Reports: the usage, the audit,<br/>the log of a session"]
The graphical UIs
The phone app, the web UI and the desktop GUI use one Rust toolkit: Slint. They share their screens. Each child design checks that Slint does its platform today.
The web UI
The web UI runs on Google Cloud Run. This is the start, for the web design (#995) to check:
- A Cloud Run service of its own serves the web app. So a deploy of the UI does not restart riff-server.
- A router gives one address to the web app and to the API, for example the rewrites of Firebase Hosting, or a load balancer with a URL map. So the browser needs no CORS.
flowchart LR
B["a browser"] -- "one address" --> RT["the router"]
RT -- "the API: /v1/..." --> S["riff-server<br/>a Cloud Run service"]
RT -- "each other path" --> WA["the web app<br/>a Cloud Run service of its own"]
The web design compares this with a riff-server that serves the web app itself, and picks one.
The devices
Each UI that signs in is a device of the person: a machine, a phone, a browser or a desktop app with a sign-in of its own. The phone design (#951) has the commands of the devices, and builds them first: see Pair a phone.
| Command | Does |
|---|---|
riff device list | Lists each signed-in device: the machines, the phones, the browsers and the desktop apps. |
riff device add [NAME] | Pairs a new device with a QR code. Then the device signs in with Google, with the same account. |
riff device remove NAME | Ends the sign-in of that one device. |
- The kind of a device sets its scope when you add it: the chat only, or each tool. Each child design names its kind and the default scope of that kind.
- The TUI has a devices tab. It shows each device with its kind, its name, its scope and its last use. It adds a device, and draws the QR code with tui-qrcode. It removes a device.
- Each graphical UI has a devices view that lists and removes devices. It adds a device where it can show a QR code.
The order
flowchart LR
B["the core library riff-client,<br/>in small steps"]
B --> TUI["the TUI"]
B --> PH["the phone app"]
PH --> WEB["the web UI"]
WEB --> DT["the desktop GUI"]
- The core library moves out of the CLI, in small steps. Each step keeps each test and changes no command. Then the core library gets stable.
- After the core library, the UIs come in this order: the TUI, the phone app, the web UI, the desktop GUI. The TUI does not block the phone app: the two can be built at the same time. The build items of a UI come from its child design, after Mike signs it off.
The build items of the core library
Each item is an issue in the Backlog. The lead puts them in the waves.
| Item | What | Needs |
|---|---|---|
| #997 | The crate riff-client and its platform traits. The client of riff-server moves into it: the sign-in, the tokens, the API calls and the watch stream. | None |
| #998 | The models of the views move to riff-client: the sessions, the threads, the waves, the machines. | #997 |
| #999 | riff-client builds for Android and for WebAssembly, and the Gate checks both. | #997, #998 |
| #1000 | --format json for each command that prints data. | #998 |