# GUIDE — States & language

**Status:** law — v1, 2026-08-03
**Applies to:** every surface, including the agent harness
**Parent:** [`GUIDE.interface-principles.md`](GUIDE.interface-principles.md)
**Vocabulary canon:** [`../_vocab/_VOCAB.md`](../_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`](../_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.
