GUIDE — States & language#
Status: law — v1, 2026-08-03 Applies to: every surface, including the agent harness Parent: GUIDE.interface-principles.md Vocabulary canon: ../_vocab/_VOCAB.md
On a phone, copy is the interface. There isn't room for an illustration to carry the meaning — the words do it.
The rule for every state#
No dead ends. Every state says what's true and what to do next.
A state that only reports a condition has done half its job. "No results" is a report. "No relays match these filters — clear filters" is a state.
Empty states#
Three different empties, three different messages. Collapsing them is the most common failure.
| Kind | Means | Says |
|---|---|---|
| Never had any | The collection is new to this user | What this is for + the first action |
| Filtered to nothing | Content exists, filters exclude it | What's hidden + clear filters |
| All done | There were items, they're handled | Acknowledge it — don't imply something is missing |
The has-content guard is mandatory. A user with 50 streams must never be shown "Create your first stream". If the intro copy can render over a non-empty collection, that's a defect, not a copy nit.
Format, in order: one line of what's true · one line of why it matters or what goes here · one action. Two sentences and a button. Longer explanation goes behind a link.
Error states#
Say what to do, not what failed.
| Instead of | Write |
|---|---|
| "Error 403: Forbidden" | "You don't have access to this relay. Ask the owner to share it." |
| "Failed to fetch" | "Couldn't reach Relay. Check your connection and try again." — with a Try again button |
| "Invalid input" | "That Code doesn't look right. Codes are letters and numbers, like DF2RPFW8RY." — never name a character count; the mint length changes (6 → 10 under wide_mint) and both lengths stay live |
| "Something went wrong" | Almost anything more specific |
Rules:
- The action is in the message. If retry is possible, the retry control is right there.
- Never blame the user. "That Code has already been claimed" — not "you entered an invalid code".
- Error codes are for support, not for the message. If one is needed for diagnosis, put it in small type below or behind a copy control.
- Preserve their work. An error never discards what someone typed.
- Inline beats toast for field-level problems — next to the field, not floating away after three seconds.
Permission, gate & scope states#
These are their own category, and the most likely to leave someone stuck.
| State | Must say |
|---|---|
| Approval pending | What was requested, of whom, and that they'll be notified |
| Not connected | Why access is blocked and the exact next step to request it |
| Plan-limited | What the limit is, where they are against it, and what changes if they upgrade |
| Feature-gated | That it isn't available to them — hide-don't-disable is the standing rule; a deep link redirects rather than showing a dead control |
| Org scope mismatch | Which org they're in, which the object belongs to, and how to switch |
Fail closed, but never silently. Showing less is right; showing less with no explanation is a dead end.
Expiry & staleness#
Relay's objects have lifecycles, and every terminal state explains itself:
- Spent Code — "This Code has already been claimed" + what to do (request a new one).
- Expired relay — when it expired and whether it can be revived.
- Stale stream item — how long since it moved, without implying a fault.
- Revoked — that it was deliberately withdrawn, not broken.
Time is honest everywhere — relative and ticking, exact on press. Never make someone do arithmetic on a timestamp.
Loading#
- Skeletons over spinners. Show the shape of what's coming.
- Never block the whole screen for a partial update.
- Optimistic where the outcome is predictable (mark read, star, file) — apply immediately, reconcile after, and have a real failure path that explains the revert.
- Slow is a state. Past a few seconds, say so — silence reads as broken.
Voice#
Plain, direct, unhurried. We are talking to someone competent who is busy.
| Do | Don't |
|---|---|
| "Couldn't reach Relay." | "Oops! Something went wrong 😅" |
| "Your Code is ready." | "Success! Your Code has been successfully generated!" |
| "Nothing here yet." | "It's quiet in here…" |
| "Ask the owner to share it." | "Please contact your administrator to request the necessary permissions." |
- No exclamation marks in system messages. No apology theatre. No cuteness in failure.
- Second person, active voice. "You haven't sent any relays" beats "no relays have been sent".
- Sentence case for everything except proper nouns and Codes.
- Never surface internal identifiers to a user — no
pc_ids, no UUIDs, no table names.
Vocabulary — locked#
| Use | Never |
|---|---|
| Code | PIN, passcode, key |
| relay (user-facing) | transfer (that's the API layer) |
| Stream | Pulse — anywhere a user can see |
| Series, Session | thread, chain, conversation |
| receive / claim | download, import |
| handoff | export, backup |
Claim and handoff are opposite directions and must never be described as one another. Canon lives in _vocab/_VOCAB.md; the KB mirrors it.
Length at 390px#
Write for the column:
| Element | Budget |
|---|---|
| Title | 2 lines |
| Button | 1–2 words |
| Empty / error state | 2 sentences + action |
| Tooltip / helper | 1 line |
| Toast | 1 line, no action buried in it |
If it doesn't fit, cut meaning-free words first — "currently", "please", "in order to".
Checklist#
- Three empties distinguished; has-content guard in place
- Every error says what to do and keeps the user's work
- Retry present wherever retry is possible
- Gate states name the next step
- Time relative and ticking, exact on press
- No internal identifiers surfaced
- Vocabulary checked against
_VOCAB.md - Everything fits its budget at 390px
Framework note#
Everything except the vocabulary table is generic — the three-empties model, the error formula, and the gate-state table are framework-candidate for Launchpad. The vocabulary is Relay's alone.