Skip to main content
An empty transcript says one of two things: a one-line welcome, or an “Add an API key to start” panel with an Open settings button when the engine in force needs a key that is definitely not set. emptyState(input, strings) is a pure decision — given whether there are rows, whether the engine needs a key, and whether one is present, it returns the view to paint, or null to paint nothing.

Quick Start

1

Fresh install, no key

2

Key configured, no messages yet

3

Any transcript row


How It Works

The whole page is four rules, checked in this order. emptyState returns an EmptyStateView, or null meaning “paint nothing”.

Rule 1 — A transcript wins over everything

hasRows is checked first and alone. Any row at all — restored history or a live turn — returns null, and the panel is not painted.
A “you need a key” panel sitting above a conversation the user is having is a worse defect than the blank rectangle, and it is reachable: the engine can be switched to one needing a key mid-chat.

Rule 2 — No key is only a problem when something wants one

keyRequired comes from the engine actually in force. The remote engine authenticates at its own server, so a remote-engine user is never shown the “add a key” panel.
Sending a remote-engine user to paste an OpenAI key would send them to configure a credential nothing on the device reads.

Rule 3 — An unresolved key check reads as “fine”, not as “missing”

KeyPresence is a three-value type — "present" | "absent" | "unknown" — and unknown resolves to the welcome copy, not the guidance.
SecretsPort.has() is asynchronous, and the first paint happens before the answer lands. Guessing "absent" during that window would accuse every configured user of not having set a key, on every launch, and then take it back a frame later.

Rule 4 — The action is part of the state

action is non-null exactly when kind === "needs-key". The pairing is the invariant — a button with no reason and a reason with no button are both bugs.
The label comes from strings.recoveryLabel("settings")“Open settings”, so the button reads the same everywhere in the app that offers this route.

Exported types


User interaction flow

1

A new user opens the app

The chat screen shows the “Add an API key to start” heading and an “Open settings” button — not a raw SDK error. Tapping the button lands on Settings.
2

They paste a key and return

Back on Chat, the panel now reads “Ask something to begin.” with the one-sentence description of what the app does. The transition from “you cannot yet” to “start typing” is complete.

Strings

The four user-visible strings are constants — not functions — read straight from the en table. The action button label is not a new string — it is strings.recoveryLabel("settings")“Open settings”, reused so the app names the destination the same way everywhere.
No suggestion chips, on purpose. Example prompts were rejected: a chip is read once by a user who has already understood what a message box is, then in the way of every new chat afterwards, at the cost of a translatable string per example. What the new user lacks is not inspiration — it is the key, and that is what the space is spent on.

API Keys

Where the “no key” guidance now appears, and how the key is stored.

i18n & A11y

The empty-chat strings and the polite live-region announcement.