When a control is busy
The third state — the app's own words for "this one is working right now", on the action row a model reads. A label, never a flag; it gates nothing, and no clock in this library will ever expire it.
The failure this prevents
A control a person looks at has three states, not two: it is clickable, it is switched off, or it is working — the spinner in the button, the greyed-out Save with Saving… under it.
Only the first two ever had a wire. So from the one reader that cannot see the screen, working and broken were the same picture — and the two moves an agent makes about broken are the two worst moves about working:
- fire it again, which submits the order twice, or
- tell the human it failed, about something that was two seconds from succeeding.
busy is the third state, on the row, in the app's own words.
The three wires
The same three enabled has, because an app that greys a button already knows this in the same
place it knows that.
At registration — the component mounts mid-flight and knows it:
.('compose', {
: { : () => },
: { : 'Saving your draft…' }, // your words, not ours
});Through the handle — the ordinary case, around the work itself:
const = .('compose', { : { } });
async function () {
.('save', 'Saving your draft…');
try {
await ();
} finally {
.('save', ); // undefined is the only way to stop saying it
}
}From a live store — LiveAction.busy, reconciled on the emission your app
already sends when the spinner comes up. No new subscription, no second channel.
A flip is world motion: it bumps the version and joins the served-structure fingerprint, exactly
as setEnabled does, so a plan made against the old row is caught as stale. Saying the same thing
twice is not motion; rewording the label is.
What the model gets
{ "action": "compose.save", "does": "Save the draft",
"busy": "Saving your draft…" } // ← the app's own wordsPresence is the whole claim. A key means the app said so. No key means this library does not
know — never not busy. An app that never wires this says nothing about any of its controls, and
a cheerful busy: false on all of them would be a claim about every session that was never asked.
A label, never a flag
There is deliberately no boolean form, and no declarative busyWhen.
A flag would say something is happening and leave the meaning to whoever renders it — which puts
this library in the business of authoring a sentence about a state only your app can describe. That
is the exact conflation the field exists to end, so the value is your label and nothing else. A
boolean, a number, an empty string: refused at every one of the three doors, with one dev warning
per action, and the row keeps saying nothing rather than saying a guess. A refused label never
clears a standing one — undefined is the clear, and nothing else is.
busyWhen is missing for the same reason. A condition can prove a state; it cannot write
prose. enabledWhen needs no words, so it has a
declarative form; this one would have to invent them.
And nothing is read off the screen — no aria-busy, no spinner-hunting. That is the
sensor's own law, and it holds here for the reason it holds for
holds: a plausible-looking wrong reading, on a row a model reads, is
indistinguishable from a right one.
It does not gate the fire
Busy is what your app said, not a door your app shut. A control that is busy and not disabled still fires — this library never invents a gate you did not declare.
If you mean and nobody may press it, you already have the wire that says so:
.('save', false); // the door
.('save', 'Saving…'); // and why it looks the way it doesThen the fire is refused as TOOL_DISABLED exactly as it always was — no new refusal word was
minted. The refusal carries the label as data, and one authored sentence beside the one it
already had:
The app also says it is working on this control right now — its own label for that is on this result as busy. Working is not broken and not done, and it is not given here as the cause of anything else. Nothing here will time it out. Wait and call whats_here again, or ask did_it_work about a fire you already made — do not fire again to find out.
It rides alongside, never over: a disabled-and-busy control has had two true things said about it by your app, and off because busy is an inference neither of them made. The sentence says so out loud rather than leaving the hole a reader would fill in itself.
The ceiling belongs to the caller
There is no timer on busy. Nothing in this library expires it, and there is no state it could
decay into if there were — it has been a while is not evidence of done and not evidence of
failed. A clock is never a verdict.
So a busy that outlives anyone's patience is answered by the row still saying busy and
did_it_work still saying still-pending. That pair is the truth.
If you are waiting on this, you own the ceiling — stop whenever you like, and report unfinished. Never done, never failed.
Honest limits
busyis a reading of your app, and only as true as what you reported. The library does not check it against anything — it cannot, and it says the app says wherever it speaks.- The label is data, not instruction. It never enters an authored sentence,
groundTruth(), or the facts block — the same firewall every runtime string in this library sits behind. - Capped, not redacted. It crosses under the same 200-character law an app's error text crosses
under. It is a bare string, and a redaction path names a field inside a value, so there is
nothing here for
redactedFieldsto name. Write labels a stranger may read: never interpolate a secret, a customer's name, or the payload into one. - One label per served action. A row that stands for many cards of a
repeatscontainer carries none, the same answerholdsgives and for the same reason. - A per-card label does not reach that card's refusal either. Firing one card of a
repeatscontainer names the instance, and a per-instancesetEnabled(false)really does refuse it —busyhas no matching per-instance door, so a label set on the card is absent from the refusal even though the caller named it. That is a gap, stated rather than papered over: the refusal is stillTOOL_DISABLED, which is true, and the label is still onopenWork()-style app state you hold. Say it on the base action if the agent should see it.
What a control holds
The draft already in the box, on the action row the model reads — declared by your app, read late, never scraped, and governed by the same redaction list as the payload it is about to become.
Confirms & receipts
A high-effect ask carries receipts — what will happen, why it's fireable, where the human is — and by default the agent's own `confirm: true` crosses it; `requireHumanApproval` makes the crossing require an approval a person recorded.