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

Reference / UI

Dialog.Open / Dialog.Ask / Dialog.Confirm / Dialog.Discard

Dialog.Open(Component(args), unitOfWork: Inherit|Root) · Dialog.Ask<TEnum>(…) · Dialog.Confirm(answer?) · Dialog.Discard()

Opens a component in an overlay above the current screen, with a unit of work you choose: `Inherit` makes its edits a savepoint of the work already open, so Confirm hands them to the surrounding page and that page's Save decides; `Root` gives the dialog its own, so its Confirm is the save. `Dialog.Ask<TEnum>` waits and returns the answer the dialog supplies with `Dialog.Confirm(value)`.

stable4 examples compiled by CIuiauthoringdata

Summary#

A dialog is an ordinary component shown in an overlay above the current screen. What makes it a dialog is not how it looks — you draw that yourself — but the unit of work it runs in, which you choose when you open it:

  • unitOfWork: Inherit — the dialog's edits are a savepoint of whatever unit of work is already open. Confirm hands them to that unit of work: the page goes dirty, and nothing is saved until the page's own Save.
  • unitOfWork: Root — the dialog owns its unit of work. There is nothing to hand its changes to, so its Confirm is the save.

Dialog.Open shows a dialog and moves on. Dialog.Ask<TEnum> shows one and waits for an answer.

Signature#

Dialog.Open(SomeComponent(prop: value), unitOfWork: Inherit);   // show it; don't wait
var answer = Dialog.Ask<Decision>(SomeComponent(…), unitOfWork: Root);   // show it; wait for the answer

Dialog.Confirm();          // inside the dialog: accept, and close
Dialog.Confirm(value);     // …and hand `value` back to whoever called Dialog.Ask
Dialog.Discard();          // inside the dialog: throw its changes away, and close

Description#

Choosing the unit of work — the one decision#

unitOfWork: is required and has no default. That is deliberate: the wrong choice loses work silently, and nothing about a component says which it should be.

InheritRoot
whose work does it edit?the surrounding page'sits own
what does Confirm do?promotes into the page's unit of workcommits
after Confirm, is it saved?no — the page is dirty; its Save decidesyes
what does Discard do?drops the savepoint; the page never knewdrops everything the dialog did
when to reach for itediting something the page is already aboutsomething unrelated to the page — account settings, preferences

Two examples of the difference, in words. A form about a project, with a button that opens a dialog editing that project's data: Inherit, so pressing OK marks the project dirty and the page's Save is still what persists it. The avatar in a nav rail opening general settings that have nothing to do with the screen behind: Root, so Save in the dialog saves, and closing the page never had anything to do with it.

"Can a Root dialog edit a row the PAGE loaded?" — yes, and this is the question people actually ask. The table above answers by intent; the mechanical worry underneath it is whether the two units of work fight over the same row. They do not. An entity is one row, and a Root dialog handed one edits it and commits it on Confirm — the page's own queries then read the committed value back like any other change.

So both choices WORK for an edit, and the difference is when it is saved:

RootInherit
pressing OKsaves, then and theremarks the page dirty; the page's Save persists it
reach for it whenthe press IS the act — "Lend", "Rename", a switch in a settings sheetthe page has its own Save and this edit belongs to it

A page with no Save button and a dialog on Inherit is the shape that loses work: OK promotes the edit into a unit of work nothing ever commits, and the change is gone at the next navigation with no error anywhere. If the page does not save, the dialog must.

A dialog opened from a dialog#

Inherit means whatever unit of work is currently open — not "the page". So a dialog opened from inside another dialog savepoints off that dialog, and its changes ride the outer one's Confirm. Closing the outer dialog without confirming discards the inner one's work too, which is what you want: the user never left the sheet they were in.

Waiting for an answer — Dialog.Ask<TEnum>#

Dialog.Ask<TEnum> shows a dialog and waits. The dialog answers with Dialog.Confirm(value), and that value is what Ask evaluates to.

The type argument is an enum you declare. A dialog asks a question, and a question has a known set of answers — which is exactly what an enum is. It also means the caller's == Decision.Delete is checked when you compile rather than compared against a string nobody declared. A dialog dismissed with Dialog.Discard() answers nothing, so treat "no answer" as its own case.

Ask works from a server function too, which is what makes a per-row question possible: a function can read rows, loop over them, ask about each one, and carry on with the answers. The run suspends at each question, the browser shows the dialog, and the run resumes where it left off with its data intact. The flow must have been started by someone using the app — a scheduled or workflow-driven run has nobody to ask.

What the platform draws — nothing#

The platform contributes exactly two things: a host element that is already an overlay above the page, and the unit of work. It draws no backdrop, panel, buttons or animation.

That is why a dialog can look like anything — a small confirmation, or a full settings surface with its own nav rail. Your component owns the dim, the panel, its width, its corners and its shadow, exactly as it owns any other screen. You do not need to position it: being above the page is the platform's job.

Two things you owe it, and neither is visual

Drawing it yourself means you also own what makes it a dialog rather than a box that happens to float. Both of these are invisible on screen and both fail loudly later, so they are the first two lines of the panel, not a polish pass:

entity Locomotive { [MaxLength(80)] string Name; bool Retired; }

[AllowAnonymous]                                    // ① or the fetch 403s — see below
[Render(CSR)]
component ConfirmRetire(Locomotive engine) {
  // ③ a handler is an ACTION of this component, passed by NAME. `onPress: Dialog.Discard`
  //    reads perfectly and does not compile — see below.
  action Cancel() { Dialog.Discard(); }
  action Retire() { engine.Retired = true; Dialog.Confirm(); }

  render {
    Stack(role: UiRole.Dialog, label: "Retire this engine?") {  // ② or no test can reach inside it
      Text("Retire this engine?", fontSize: FontSize.Heading);
      Row(gap: 2, justify: Justify.End) {
        Button("Cancel", onPress: Cancel);
        Button("Retire", onPress: Retire, tone: Tone.Danger);
      }
    }
  }
}

[Page("/engines")]
[AllowAnonymous]
[Render(CSR)]
component Engines() {
  var engines = Locomotive.ToList();
  action Ask(Locomotive e) { Dialog.Open(ConfirmRetire(engine: e), unitOfWork: Root); }
  render { Stack(gap: 2) { foreach (var e in engines) { Button(e.Name, onPress: () => Ask(e)); } } }
}

[AllowAnonymous] on the component itself, in an app with no sign-in. Dialog.Open fetches its component as its own entry point, so the page's marker does not cover it. Without one the fetch answers 403 and the button appears to do nothing — there is no error on screen, because the failure is in a request the page made on your behalf. The lint ui-dialog-target-unreachable now names this before you run.

role: UiRole.Dialog and label: on the panel. The role is what an assistive reader announces, and the label is the container's NAME — which is what within: selects. Omit them and Assert.Dialog(…) reports "a dialog IS open … but nothing in it carries an accessible name", and every Ui.Click("Save", within: "…") inside it has nothing to match.

onPress: takes an ACTION of this component, by name. onPress: Dialog.Discard is the spelling everyone reaches for and it does not compile — Dialog is also a member of the UiRole enum, so the refusal you get talks about UiRole.Dialog, which is not the problem and cannot be the fix. Give the dialog a one-line action and pass that: action Cancel() { Dialog.Discard(); }onPress: Cancel.

This page taught the broken spelling until 2026-09-01, in a syntax fence the compile gate does not check. Eval run 16 copied it into FOUR dialogs and spent fourteen calls — a quarter of the run — hunting a Dialog namespace that does not exist. The fence above is COMPILED now, so it cannot say that again.

Measured, and this section exists because the page caused it. Two eval runs read this page, drew their own panel exactly as it invited, and shipped the same defect — one of them three times in one file, then spent eight consecutive lookups discovering the 403. A third run skipped the page, ran osy kit Dialog, used the kit control, and its first osy check passed. The kit's source carries both facts; this page did not, so the runs that did the diligent thing were the ones that got it wrong. If you do not want to own these two, use the kit control (below) — it sets both for you.

The kit's Dialog control is a different thing#

The bundled UI kit ships a control also called DialogDialog(title, onDismiss, subtitle), osy kit Dialog. It is not what this page documents, and the two are not layers of one mechanism:

Dialog.Open(…) — this pagethe kit's Dialog(…) control
what it isan effect that opens a component in the platform's overlaymarkup — a Scrim and a panel, drawn in the page's own tree
how it appearsyou call ityou render it conditionally: if (confirming) { Dialog("Delete?", onDismiss: Cancel) { … } }
unit of workits own, and you choose it: Inherit or Rootnone — being [Composable] it has no mount of its own, so there is nothing to savepoint. Whatever you edit inside it is edited in the page's unit of work
how it closesDialog.Confirm() / Dialog.Discard()the onDismiss action you passed, which flips your own flag

So the kit control is a panel that looks like a dialog; it does not give you the scope this page is about. If the edits inside it must be discardable, or must not touch the page until they are confirmed, open a component with Dialog.Open and choose its unit of work — a kit Dialog around the same fields cannot do it, open or closed.

Saving a Root dialog#

A Root dialog's Confirm is the only thing that can persist it. A root dialog that edits data and has no reachable Dialog.Confirm() can therefore only ever be discarded — every edit thrown away, with nothing reporting it. osy lint reports that shape as data-root-dialog-cannot-confirm; either give it a Confirm, or open it with Inherit so its edits ride the surrounding page's save.

Examples#

An Inherit dialog editing a row the page is about. OK makes the page dirty; the page's own Save is what persists it:

entity Project { [MaxLength(80)] string Name; }

[Render(CSR)]
component EditProject(Project project) {
  action Ok()     { Dialog.Confirm(); }
  action Cancel() { Dialog.Discard(); }
  render {
    Stack(gap: 2) {
      Input(value: project.Name, placeholder: "Project name");
      Button("Cancel", onPress: Cancel);
      Button("OK", onPress: Ok);
    }
  }
}

[Page("/projects")]
[Render(CSR)]
component Projects() {
  var projects = Project.ToList();
  action Edit(Project p) { Dialog.Open(EditProject(project: p), unitOfWork: Inherit); }
  action Save() { UnitOfWork.Commit(); }
  render {
    Stack(gap: 2) {
      foreach (var p in projects) { Button(p.Name, onPress: () => Edit(p)); }
      Button("Save", onPress: Save);
    }
  }
}

Asking a question and acting on the answer. The answer type is an enum you declare:

enum Decision { Delete, Keep }

entity Note { [MaxLength(80)] string Title; }

[Render(CSR)]
component ConfirmDelete(Note note) {
  action Yes() { Dialog.Confirm(Decision.Delete); }
  action No()  { Dialog.Confirm(Decision.Keep); }
  render {
    Stack(gap: 2) {
      Text("Delete " + note.Title + "?");
      Button("Keep it", onPress: No);
      Button("Delete", onPress: Yes);
    }
  }
}

[Page("/notes")]
[Render(CSR)]
component Notes() {
  var notes = Note.ToList();
  action Remove(Note n) {
    if (Dialog.Ask<Decision>(ConfirmDelete(note: n), unitOfWork: Inherit) == Decision.Delete) {
      n.Delete();
      UnitOfWork.Commit();
    }
  }
  render {
    Stack(gap: 2) { foreach (var n in notes) { Button(n.Title, onPress: () => Remove(n)); } }
  }
}

A Root dialog — settings that have nothing to do with the page behind, so they own their unit of work and their Save really saves:

entity Preferences { [MaxLength(40)] string DisplayName; }

[Render(CSR)]
component Settings(Preferences prefs) {
  action Save()  { Dialog.Confirm(); }
  action Close() { Dialog.Discard(); }
  render {
    Stack(gap: 2) {
      Input(value: prefs.DisplayName, placeholder: "How you appear");
      Button("Close", onPress: Close);
      Button("Save", onPress: Save);
    }
  }
}

[Page("/settings")]
[Render(CSR)]
component SettingsPage() {
  var prefs = Preferences.FirstOrDefault();
  action Open() { Dialog.Open(Settings(prefs: prefs), unitOfWork: Root); }
  render { Stack(gap: 2) { Button("Settings…", onPress: Open); } }
}

Drawing the look. The platform positions the overlay; everything you can see is yours:

render {
  Row(align: Align.Center, justify: Justify.Center, w: "100%", h: "100%", bg: Colors.Scrim) {
    Stack(gap: 4, w: "420px", p: 5, bg: Colors.Surface, rounded: Radius.Lg, borderW: 1, border: Colors.Border, shadow: Shadow.Deep) {
      // …your dialog…
    }
  }
}

See also#

  • creating & saving data — the unit of work a dialog savepoints, and what UnitOfWork.Commit() / Discard() do
  • component — a dialog is an ordinary component; this is what one is
  • routes and pages — the other way to hold work in progress: a route, with its own unit of work
  • style props — the style props the panel above uses
  • osy kit Dialog — the kit's same-named CONTROL, which draws a panel and carries no unit of work (see above)

Related

creating & saving data

A UI `action` creates, updates and deletes data by writing `new Entity { … }`, assigning fields, and calling…

component

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

routes and pages

How a component becomes a page: it declares a route with `[Page("/catalog/{slug}")]`, and navigating to a matching path…

on mount / on unmount

`on mount { … }` runs a block ONCE, the first time a component appears — before its first paint; `on unmount { … }`…