Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / UI

Pending

automatic busy spinner · save.Pending · Pending.Any · Pending.Count · app.Ui { PendingDelayMs, PendingMinShowMs }

When a control's action waits on the server, the platform shows a busy spinner and disables the control — but only after a short delay, so a fast action never flashes one. It is automatic: you write nothing. To say something specific while one particular verb runs, read `save.Pending`; for a page-wide affordance (a top progress bar) read the `Pending` ambient. Tune the timing on `app.Ui`.

stable2 examples compiled by CIuipendingbusyloading

Summary#

When a control's action waits on the server — an onClick/onEnter that calls a server function and hasn't come back yet — the platform gives the user feedback automatically: after a short delay the control shows a busy spinner and disables itself until the action finishes. You write nothing; every button that hits the server gets it.

The delay is deliberate. Most actions are fast, and a spinner that flashes for 40ms is worse than none — it reads as a glitch. So the spinner appears only once an action has run longer than the threshold (200ms by default), and once shown it stays visible for a short minimum so it can't blink off the instant the answer arrives. A fast action shows nothing.

Disabling the control while it is busy also blocks an accidental double-submit — a second click on a half-second "Save" does nothing rather than firing the action twice.

When you want to say something specific while one particular verb runs — a button that reads "Reading…" rather than just spinning — read that verb's own flag: Read.Pending is true while an invocation of Read is in flight. It follows the one action it names, so a page that saves while a read is running does not relabel the read button.

For a page-wide busy affordance — a top progress bar, a dimmed overlay — read the Pending ambient: Pending.Any is true while any action is in flight, Pending.Count is how many. That is the one thing you author; the per-control spinner needs no code at all.

Three grains, and picking the wrong one is the usual mistake:

You wantReadGrain
A spinner on the control that was pressednothing — it is automaticthat control
A label or field that speaks for ONE verbsave.Pendingthat action
A top bar / overlay for the whole pagePending.Any · Pending.Countthe page

Signature#

// Automatic — no code. A control whose action suspends on the server shows a delayed spinner + disables itself.

save.Pending     // true while an invocation of THIS action is in flight (a bool) — per-verb
Pending.Any      // true while at least one action is in flight (a bool) — for your own global busy surface
Pending.Count    // how many actions are in flight right now (an int)

// Tune the timing (and, later, supply your own spinner) on app.Ui:
app.Ui = new AppUi {
  PendingDelayMs   = 200,   // show the spinner only after an action runs this long (0 → the 200ms default)
  PendingMinShowMs = 300,   // once shown, keep it at least this long so it can't blink off (0 → the 300ms default)
};

Description#

The busy affordance is mechanism, not chrome. The platform draws a plain spinner (.osy-spinner) beside the control's label and applies the ordinary disabled styling; an app that wants a different look styles it in its own CSS, the same way it styles any other control state.

It is entirely client-side. The first server-rendered paint is never busy (there is nothing pending yet), so a page reading Pending.Any renders its idle state on the server and lights up only once the user starts something.

Which events count: the spinner is for activation events — onClick, onEnter, onSubmit, and an Upload's onUploaded. Typing and focus events (onInput, onChange, onBlur) do not spin the field — a keystroke that runs a check should not make the input look busy. The Pending ambient counts every in-flight action regardless.

Pending.Any/Pending.Count update the instant an action begins and settles — the ambient is the raw in-flight count, not the delayed spinner. So a top bar bound to Pending.Any appears immediately; if you want it to respect the same "don't flash" delay, gate it on your own timer.

<verb>.Pending is available on every action and method a component declares, and reads as an ordinary bool anywhere an expression is legal — a ternary, a prop value, an if. Like the ambient it flips the instant the action begins, with no delay: it is what you reach for when you would rather say "Reading…" than show a generic spinner, and you want that word to appear at once.

It counts every invocation of that action, not only the one a control activated — an action called from another action is still running, and a label claiming otherwise would be wrong in the one case you most want to explain. It also clears when an action fails: the work is over, it just went badly, and a button stuck reading "Reading…" forever is worse than one that goes back to normal while your failure surface says what happened.

Actions are serialised per page, so an action can sit queued behind another. .Pending is true for that wait too — from the user's side the work has already started, and the label is what tells them so.

A misspelling is a compile error naming what exists (save.Pendign"a verb has no member 'Pendign' (Pending)"), rather than a value that silently reads as nothing.

PendingDelayMs / PendingMinShowMs are app-wide timing, read from app.Ui. Leave them out (or set 0) and the platform defaults apply. A negative value is a compile error.

Examples#

The common case is nothing — the spinner is automatic:

component InvitePage() {
  action Accept() { var t = AcceptInvite(token, email, password); Session.SignIn(t); }
  render {
    // No pending code. `Accept` calls a server function, so this button spins + disables while it runs.
    Pressable(onClick: Accept) { Text("Accept invitation"); }
  }
}

AcceptInvite here is a server function reached by a signed-out visitor, so a working version of this also needs it marked [AuthMethod] and wired into app.AuthBootstrap — see [AuthMethod] — a function an unauthenticated visitor may call. The point above is only that the spinner costs no code either way.

A button that says what it is doing, using the action's own flag:

component ExpenseLineRow(ExpenseLine line) {
  action Read() { ReadReceipt(line); }
  render {
    Pressable(onClick: Read) {
      // Follows `Read` alone — another action finishing elsewhere on the page leaves this label untouched.
      Text(Read.Pending ? "Reading…" : "Read receipt");
    }
  }
}

The same flag on a control that takes a busy prop, and to keep a form from being resubmitted:

component LoginCard() {
  action SignIn() { Session.SignIn(Authenticate(email, password)); }
  render {
    Button("Sign in", onPress: SignIn, busy: SignIn.Pending, disabled: email == "");
  }
}

A page-wide top progress bar, using the Pending ambient:

theme App { Colors { Accent = "#0077B6"; } }   // `Accent` is your own token, not a platform one

[Layout]
component Shell() {
  render {
    if (Pending.Any) { Box(h: "2px", bg: Colors.Accent, w: "100%"); }   // a thin bar while anything is in flight
    Outlet();
  }
}

Tuning the timing for a whole app:

app.Ui = new AppUi {
  PendingDelayMs   = 120,   // this app's actions are usually instant — show the spinner sooner when they aren't
  PendingMinShowMs = 400,   // …but once it shows, hold it a beat longer
};

See also#

  • UI surfaces (app.Ui) — the app.Ui block that tunes the timing (and nominates the app's system surfaces).
  • Connection — the sibling surface for the server-dropped case (a full connection-loss overlay).
  • component — the components and controls the spinner attaches to.

Related

UI surfaces (app.Ui)

`app.Ui` nominates the app's own components for the "system surfaces" the platform would otherwise render a bare…

Connection

The live state of the browser's link to the server, and the two verbs that recover it. A component you nominate as your…

component

The one archetype for all UI: a bounded reactive unit — typed props, reactive members (fields, `live`…