Decision 0047
The face is two surfaces, software-rendered, and a client like any other
Architecture decision record 0047: The face is two surfaces, software-rendered, and a client like any other
Status: accepted, with its renderer clause amended by ADR-0088 · Date: 2026-09-05 · Refines: ADR-0019, ADR-0021
The shape of the face — three surfaces, each its own process, each a client of the same protocol — is what this record decided and it stands. The renderer does not:
tiny-skiawas chosen here so the face would come up on a machine whose driver is being repaired, and that machine has no compositor either, so the requirement was already met by the tty and the terminal interface. The cost was that the shell could never be translucent. ADR-0088 has the argument.
Context
ADR-0019 chose Niri and a shell of Tacid’s own in Iced, and left the shape of that shell open: a bar, a command surface, an agent panel, notifications, settings. Written as five surfaces it is a desktop environment, which is months of work and a second place where a machine is configured. The question this record answers is what the first face is, and what it may depend on.
Three constraints made the answer narrower than the list.
The first is that a client is a renderer. Everything the shell shows it must learn by asking the
daemon, and everything it changes it must change by proposing a transaction and bringing the
owner’s consent to the keeper, exactly as tacid does. A graphical surface that could do more
than the command line would be a second API to keep honest, and the day the two disagree the
owner is the one who finds out.
The second is what a shell drags in. Iced with its default renderer brings wgpu, and wgpu brings a graphics stack that has to work before a single pixel is drawn. A shell that needs a GPU is a shell that cannot come up in a virtual machine, on a server’s remote display, or on the first boot after a driver change — which is the one boot where a person most needs to see what their machine thinks is wrong. It is also several hundred crates in a workspace whose test run is currently seconds.
The third is the crash policy: any Tacid process dies alone, and the terminal and a tty always work. That applies to the shell’s own parts as much as to the daemons.
Decision
The face is three surfaces, each its own process, all drawn from the tokens and all speaking
the one protocol. Three rather than the five the plans wanted, and the third arrived only once
there was an event stream for it to render: a panel that polls the daemon is a worse tacid status, and a panel that renders what the machine publishes is the machine thinking out loud.
-
tacid-shell bar— a strip along the top that says what this machine is: its name, the generation running, the channel it follows, the autonomy level in force as a badge, the clock, and a badge when the machine runs code it built for itself. It refreshes every five seconds by asking the daemon, and when the daemon does not answer it says so rather than going blank. It never takes the keyboard. -
tacid-shell command— one box, opened by the compositor’sMod+Space, which the person types into without first deciding what kind of thing they are doing. A name that matches a desktop entry opens that program; anything else is said to the agent in a session, and the reply, what it would change, and — when the change needs the owner’s word — the two buttons that give or withhold it appear in the same box. Escape gives the screen back. -
tacid-shell panel— a column down the side, opened byMod+A, that fills in as the machine works: the tool it reached for before it ran, the change it wants and why, the question waiting for a yes, and what came of it. It is a renderer of the event stream and nothing more — it holds no state the daemon does not have, and the yes it sends is the same one the terminal sends, to the keeper, which decides whether this principal may give one. It is opened rather than started with the session, because a panel that is always there is a panel a person stops reading.
Notifications and settings remain deliberately absent: a settings application would be a second place where this machine is configured, and the genome is the first.
Software-rendered. iced is taken with default-features = false and the tiny-skia
renderer, so there is no wgpu, no Vulkan and no GPU driver on the path between the daemon’s
answer and the pixels. A bar and a text box do not need a graphics card, and the machine that
cannot start one is the machine that most needs its shell.
Its own workspace. crates/tacid-shell is excluded from the root workspace and built as its
own package, with tacid-protocol and tacid-design as path dependencies. The nervous system’s
test run does not wait for a renderer to compile, and a headless machine’s closure has no shell
in it. The path dependencies are what keep the two from drifting: the shell renders
TurnResult — the same structure the terminal renders — and draws design/tokens.toml, the
same file the terminal theme and the compositor’s borders are generated from.
The turn result moved to the protocol crate. It had lived in tacid-daemon, so a client had
to link the daemon to read a reply. It is the wire shape of session.ask and now lives beside
the method that returns it; the daemon re-exports it.
The session is declared, not configured. tacid.desktop.enable composes the compositor’s
configuration from the generated colours and Tacid’s own bindings — into /etc/niri/config.kdl
when this was written, and into /etc/hypr/hyprland.conf since
ADR-0053 settled on one
compositor — starts the bar at session start, and puts greetd in front with the smallest greeter
there is.
Rolling the machine back rolls the session back with it, because the session is part of the
generation and not a file in somebody’s home.
Consequences
- The shell is the fourth client of the same API, after
tacid, the terminal interface and the agent itself. Anything it can do can be done from a serial console, which is the test that keeps the protocol honest. - The bar says when the machine is waiting for an answer. A change waiting for a word is a
machine that has halted, and before this the only way to find out was to open the panel or run
tacid pending— which is to say, to already suspect. It is a fact about the machine like every other word on that line, and it is worded as one: “waiting for an answer” and not “waiting for you”, because the keeper holds what it holds and does not sort its queue by who is looking, and on a machine with two people the second wording would say it to somebody who cannot answer. Whether this person may is the panel’s answer and the terminal’s, where the question itself is. A count and not a summary, for the same reason a bar does not truncate a question. The count comes from the keeper rather than the daemon, in the order consent goes: the daemon is the process being authorised, and a number it reported would be a number it could be wrong about in its own favour. Amber rather than red, because a machine that stopped and asked is the design working, and painting that as a fault teaches a person to dread the thing the whole design is for. - Notifications as a surface of their own — a toast, the freedesktop protocol, other programs’ messages — are not here. What Tacid itself has to say fits in a word on a strip that is already on the screen, and a second always-mapped surface that is empty most of the time is a rectangle swallowing clicks for the minority of seconds it has anything in it.
- A machine with no screen builds no renderer.
tacid.desktop.enable = false— the default everywhere but the workstation profile — leaves the graphical closure out entirely. - Two processes rather than one means the command surface can crash without taking the bar with
it, and the bar can be restarted without the person losing what they were typing. It also
means the key binding is a
spawn, which is what a compositor is good at. - The dependency tree brought two pins with it: the software renderer needs its Wayland backend
feature to have any backend at all, and the layer-shell crate’s developer tools do not compile
against the newest
winitbeta, so the lock holds the one before it. Both are recorded in the shell’s ownCargo.lock, which is separate from the workspace’s. - Notifications and a settings surface are not written, and are not meant to be. The agent’s
panel now is: it renders the stream
tacid watchrenders, so a long turn’s tools can be watched on the screen as well as in a terminal. - What has been proven is that the workstation configuration evaluates with the session in it and that the shell builds and passes its own tests. It has not yet been started against a compositor: the graphical test that boots the session, reads the bar off the screen and opens the command surface is written down as the next piece of work, not claimed here.
Alternatives rejected
- wgpu, as
iceddefaults to. Faster for a surface that animates, and this one does not. The cost is a graphics stack between the owner and the one window that tells them what is wrong with their machine. - One process with two layer surfaces. Fewer moving parts, and a crash in the box takes the bar down with it. The crash policy is not a preference.
- A separate agent-panel window. It would make a person choose between talking to the machine and using it. One box removes the choice, which is the point of the command surface.
- libcosmic or another desktop’s shell toolkit. Someone else’s identity, someone else’s release cycle, and a settings surface Tacid did not design.
What the first boot of it found
Written before the session had ever started. It has now, and two things came back.
An emulator cannot show a Smithay compositor’s output. QEMU’s virtio-gpu without host GL is a
display-only device: no render node, so Mesa offers no allocator, and Smithay — which Niri was
built on — has no software renderer to fall back to the way wlroots has Pixman. Niri came up
fully, served Wayland, started the shell, and had no output to give a layer surface:
WARN niri::handlers::layer_shell: no output for new layer surface, closing. So the graphical
test proves the session on the real path and proves the drawing under a wlroots compositor with
the Pixman renderer, where the pixels can be read without a card. Which compositor accepts a
layer surface is not what is being tested; that the shell draws words is — and that separation is
what later made the compositor itself replaceable
(ADR-0053).
A shell whose surface is closed did not notice. The bar was still running five seconds after the compositor closed its surface, with nothing on the screen and nothing said. That is the exact failure this test was written to go looking for — a surface that is running and drawing nothing — and it was the shell’s, not the compositor’s. A session can restart a shell that exits; it can do nothing about one that sits there invisible.
Both surfaces now leave when the compositor takes theirs away, and say so on the way out. The
protocol’s zwlr_layer_surface_v1.closed arrives as an ordinary window event, so it is one more
subscription beside the clock and the escape key rather than anything special. For the command
surface it is the same answer as Escape from the other side: the box is gone, so the process that
drew it should be too.