hacifootprint
Actions

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 storeLiveAction.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 words

Presence 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 does

Then 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

  • busy is 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 redactedFields to 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 repeats container carries none, the same answer holds gives and for the same reason.
  • A per-card label does not reach that card's refusal either. Firing one card of a repeats container names the instance, and a per-instance setEnabled(false) really does refuse it — busy has 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 still TOOL_DISABLED, which is true, and the label is still on openWork()-style app state you hold. Say it on the base action if the agent should see it.

On this page