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

Samples

dialog-demo

THE CANONICAL DIALOG SAMPLE (D60/D212). Everything a dialog can be, in one small app:

5 source files1 test file

The dialog-demo sample, running.
Running, signed in, compiled from the source below.

Get it

$ osy init dialog-demo
$ osy launch

The app

app.osy6 lines
// THE CANONICAL DIALOG SAMPLE (D60/D212). Everything a dialog can be, in one small app:
app DialogDemo {
  model "model/**/*.osy";
  tests "tests/**/*.test.osy";
}
model/auth.osy66 lines
// The smallest real login, on the `demo/dropdown-demo` shape. This demo WRITES (that is the whole subject), and the
// deny-all endpoint gate correctly refuses an anonymous write — so a login is not incidental scaffolding here.

[Role] enum AppRole { Authenticator, Member }

entity RoleGrant {
  User Grantee;
  [Required] AppRole Level;
  security {
    allow create when IsAuthenticator;
    allow read when IsAuthenticated;
  }
}

policy IsAuthenticator => RoleGrant.Any(g => g.Grantee == user && g.Level == AppRole.Authenticator);

[Principal] entity User {
  [Required, MaxLength(200), Unique] string Email;
  [MaxLength(200)] string PasswordHash;
  security {
    deny read PasswordHash when !IsAuthenticator;
    allow read, create when IsAuthenticator;
    allow read when IsAuthenticated;
  }
}

[AuthMethod]
string Signup(string email, string password) {
  var u = new User { Email = email, PasswordHash = Security.HashPassword(password) };
  var g = new RoleGrant { Grantee = u, Level = AppRole.Member };
  return Security.IssueJwt(u.Id, u.Email);
}

[AuthMethod]
string Login(string email, string password) {
  var u = User.Where(x => x.Email == email).FirstOrDefault();
  if (u == null) { Security.VerifyPassword(password); return ""; }
  if (Security.VerifyPassword(password, u.PasswordHash)) { return Security.IssueJwt(u.Id, u.Email); }
  return "";
}

app.AuthBootstrap = new AuthBootstrap {
  Role = AppRole.Authenticator, Login = Login, Signup = Signup, LoginPage = LoginPage,
};

[Page("/login")]
[Render(CSR)]
[AllowAnonymous]
[Title("Sign in")]
component LoginPage() {
  string email = "";
  string password = "";
  action CreateAccount() { Session.SignIn(Signup(email, password)); }
  render {
    Row(align: Align.Center, justify: Justify.Center, w: "100%", h: "100vh", bg: Colors.Bg) {
      Stack(gap: 3, w: "320px", p: 5, bg: Colors.Surface, rounded: Radius.Card, borderW: 1, border: Colors.Border, shadow: Shadow.Dialog) {
        Text("Dialog demo", fontSize: FontSize.Display, fontWeight: FontWeight.Medium, color: Colors.OnSurface);
        Text("Any email and password — this is a sample.", fontSize: FontSize.Caption, color: Colors.Subtle);
        Input(value: email, label: "Email", placeholder: "you@example.com");
        Input(value: password, label: "Password", type: "password", placeholder: "password");
        Osyrin.Button("Create account", onClick: CreateAccount);
      }
    }
  }
}
model/dialogs.osy134 lines
// ── THE DIALOGS ──────────────────────────────────────────────────────────────────────────────────────────────────

// A scrim + centred panel, so the three dialogs below do not each restate it. This is app code — you are meant to
// change it, or not use it at all.
// ⚑ The width is a BOOL, not a string. A style prop's unit is applied when the component compiles, so a width
// arriving as a per-render value has no unit to apply — the compiler says so rather than emitting a broken rule.
// Two literal token references is the honest way to say "two sizes".
// ⚑ `role: UiRole.Dialog` + `label:` ARE THE PANEL'S JOB, and without them a modal is announced as nothing at all: someone
// not looking at the screen is never told a dialog opened, never told what it is about, and has no boundary telling
// them where it ends. The overlay and the scrim are visual facts only.
[Composable]
component Panel(bool wide, string title) {
  render {
    Row(align: Align.Center, justify: Justify.Center, w: "100%", h: "100%", bg: Colors.Scrim, p: 4) {
      if (wide) {
        Stack(gap: 4, w: Length.Wide, p: 5, bg: Colors.Surface, rounded: Radius.Card, borderW: 1, border: Colors.Border, shadow: Shadow.Dialog,
              role: UiRole.Dialog, label: title) {
          Text(title, fontSize: FontSize.Section, fontWeight: FontWeight.Medium, color: Colors.OnSurface);
          Slot();
        }
      }
      else {
        Stack(gap: 4, w: Length.Narrow, p: 5, bg: Colors.Surface, rounded: Radius.Card, borderW: 1, border: Colors.Border, shadow: Shadow.Dialog,
              role: UiRole.Dialog, label: title) {
          Text(title, fontSize: FontSize.Section, fontWeight: FontWeight.Medium, color: Colors.OnSurface);
          Slot();
        }
      }
    }
  }
}

[Render(CSR)]
component EditProject(Project project) {
  action Ok()     { Dialog.Confirm(); }
  action Cancel() { Dialog.Discard(); }
  render {
    Panel(wide: false, title: "Edit project") {
      Text("OK hands these changes to the page. Nothing is saved until you press Save there.",
           fontSize: FontSize.Caption, color: Colors.Subtle);
      Stack(gap: 2) {
        Text("Name", fontSize: FontSize.Caption, color: Colors.Subtle);
        Input(value: project.Name, label: "Name", placeholder: "Project name");
      }
      Stack(gap: 2) {
        Text("Notes", fontSize: FontSize.Caption, color: Colors.Subtle);
        Input(value: project.Notes, label: "Notes", placeholder: "What is it for?");
      }
      Row(gap: 2, justify: Justify.End) {
        Osyrin.Button("Cancel", onClick: Cancel);
        Osyrin.Button("OK", onClick: Ok);
      }
    }
  }
}

[Render(CSR)]
component ConfirmDelete(Project project) {
  action Yes() { Dialog.Confirm(Answer.Delete); }
  action No()  { Dialog.Confirm(Answer.Keep); }
  render {
    Panel(wide: false, title: "Delete this project?") {
      Text("'" + project.Name + "' will be removed. This one really does save immediately.",
           fontSize: FontSize.Caption, color: Colors.Subtle);
      Row(gap: 2, justify: Justify.End) {
        Osyrin.Button("Keep it", onClick: No);
        Osyrin.Button("Delete", onClick: Yes);
      }
    }
  }
}

[Render(CSR)]
component Settings(Preferences prefs) {
  string pane = "Profile";
  action ShowProfile()  { pane = "Profile"; }
  action ShowDisplay()  { pane = "Display"; }
  action Save()   { Dialog.Confirm(); }
  action Close()  { Dialog.Discard(); }

  string reset = "";
  action ResetName() {
    if (Dialog.Ask<Answer>(ConfirmReset(), unitOfWork: Inherit) == Answer.Delete) {
      prefs.DisplayName = "";
      reset = "cleared";
    }
  }

  render {
    Panel(wide: true, title: "Settings") {
      Text("Its own unit of work — Save here persists on its own, without touching the page behind.",
           fontSize: FontSize.Caption, color: Colors.Subtle);
      Row(gap: 4) {
        Stack(gap: 1, w: Length.Rail) {
          Osyrin.Button("Profile", onClick: ShowProfile);
          Osyrin.Button("Display", onClick: ShowDisplay);
        }
        Stack(gap: 3, grow: 1) {
          if (pane == "Profile") {
            Text("Display name", fontSize: FontSize.Caption, color: Colors.Subtle);
            Input(value: prefs.DisplayName, label: "Display name", placeholder: "How you appear");
            Osyrin.Button("Reset name…", onClick: ResetName);
            if (reset != "") { Text("Name cleared — still not saved until you press Save.", fontSize: FontSize.Caption, color: Colors.Subtle); }
          }
          if (pane == "Display") {
            Text("Density", fontSize: FontSize.Caption, color: Colors.Subtle);
            Input(value: prefs.Density, label: "Density", placeholder: "comfortable / compact");
          }
        }
      }
      Row(gap: 2, justify: Justify.End) {
        Osyrin.Button("Close", onClick: Close);
        Osyrin.Button("Save", onClick: Save);
      }
    }
  }
}

[Render(CSR)]
component ConfirmReset() {
  action Yes() { Dialog.Confirm(Answer.Delete); }
  action No()  { Dialog.Confirm(Answer.Keep); }
  render {
    Panel(wide: false, title: "Clear your display name?") {
      Text("This is a dialog opened from a dialog. It savepoints off the SETTINGS sheet, so closing settings "
           + "without saving discards this too.", fontSize: FontSize.Caption, color: Colors.Subtle);
      Row(gap: 2, justify: Justify.End) {
        Osyrin.Button("Keep it", onClick: No);
        Osyrin.Button("Clear", onClick: Yes);
      }
    }
  }
}
model/home.osy97 lines
// Seeds the sample rows on first run.
void SeedIfEmpty() {
  if (Project.Any()) { return; }
  new Project { Name = "Apollo",  Notes = "Re-entry guidance",     Priority = Priority.High };
  new Project { Name = "Gemini",  Notes = "Rendezvous practice",   Priority = Priority.Normal };
  new Project { Name = "Voyager", Notes = "Grand tour, no return", Priority = Priority.Low };
  UnitOfWork.Commit();
}

[Page("/")]
[Layout(Shell)]
[Render(CSR)]
[Title("Projects")]
component Home() {
  live var projects = Project.OrderBy(p => p.Name).ToList();
  string note = "";

  action Edit(Project p) {
    Dialog.Open(EditProject(project: p), unitOfWork: Inherit);
    note = "";
  }

  action Save() {
    UnitOfWork.Commit();
    note = "Saved.";
  }

  action Revert() {
    UnitOfWork.Discard();
    note = "Reverted — the dialog's edits went with it.";
  }

  // DELIBERATELY the odd one out on this page, which is the third of its three unit-of-work stories: `Save` and
  // `Revert` above hold the page's edits until you press them, and this one persists the moment you confirm. The
  // warning is right that pressing a per-row control writes on its own — here that IS the lesson, so it is
  // suppressed rather than designed away. ⚠ Confirming in a dialog does NOT make it exempt: "the user confirmed" is
  // another proxy for agency, and the rule takes none.
  action Remove(Project p) {
    if (Dialog.Ask<Answer>(ConfirmDelete(project: p), unitOfWork: Inherit) == Answer.Delete) {
      p.Delete();
      UnitOfWork.Commit();
      note = "Deleted.";
    }
    else { note = "Kept it."; }
  }

  render {
    Stack(gap: 4, p: 6, maxW: "820px") {
      Text("Projects", fontSize: FontSize.Display, fontWeight: FontWeight.Medium, color: Colors.OnBg);
      Text("Three dialogs, three unit-of-work stories. Edit one and watch nothing save until you press Save.",
           fontSize: FontSize.Caption, color: Colors.Subtle);

      Stack(gap: 2) {
        foreach (var p in projects) {
          Row(gap: 3, align: Align.Center, p: 3, bg: Colors.Surface, rounded: Radius.Card, borderW: 1, border: Colors.Border) {
            Stack(gap: 1, grow: 1) {
              Text(p.Name, fontWeight: FontWeight.Medium, color: Colors.OnSurface);
              Text(p.Notes, fontSize: FontSize.Caption, color: Colors.Subtle);
            }
            Osyrin.Button("Edit…", onClick: () => Edit(p));
            Osyrin.Button("Delete…", onClick: () => Remove(p));
          }
        }
      }

      Row(gap: 2) {
        Osyrin.Button("Save", onClick: Save);
        Osyrin.Button("Revert", onClick: Revert);
      }
      if (note != "") { Text(note, fontSize: FontSize.Caption, color: Colors.Subtle); }
    }
  }
}

[Layout]
component Shell() {
  live var prefs = Preferences.FirstOrDefault();

  on mount { SeedIfEmpty(); }

  action OpenSettings() {
    if (prefs == null) { new Preferences { DisplayName = "", Density = "comfortable", ShowNotes = true }; UnitOfWork.Commit(); }
    Dialog.Open(Settings(prefs: prefs), unitOfWork: Root);
  }

  render {
    Stack(w: "100%", minH: "100vh", bg: Colors.Bg) {
      Row(align: Align.Center, p: 4, gap: 3, borderW: 1, border: Colors.Border, bg: Colors.Surface) {
        Text("Dialog demo", fontWeight: FontWeight.Medium, color: Colors.OnSurface);
        Box(grow: 1);
        Osyrin.Button("Settings…", onClick: OpenSettings);
      }
      Outlet();
    }
  }
}
model/theme.osy42 lines
// Every colour, radius and shadow the dialogs use, named once. A dialog is mostly LOOK, and a sample whose look is
// hard-coded hex teaches the wrong thing — `osy lint` says so too (`ui-raw-style-literal-repeated`).
theme Studio {
  Colors {
    Bg           = Modes.Of(light: "#F6F7F9", dark: "#0E1116");
    OnBg         = Modes.Of(light: "#16181D", dark: "#E6EAF0");
    Surface      = Modes.Of(light: "#FFFFFF", dark: "#171B21");
    OnSurface    = Modes.Of(light: "#1A1F26", dark: "#E6EAF0");
    Border       = Modes.Of(light: "#DDE1E6", dark: "#2A2F37");
    Muted        = Modes.Of(light: "#EFF1F5", dark: "#232833");
    Subtle       = Modes.Of(light: "#6B7280", dark: "#8A94A6");
    Accent       = "#4F46E5";
    OnAccent     = "#FFFFFF";
    Danger       = "#B42318";
    Scrim        = Modes.Of(light: "rgba(15,23,42,0.55)", dark: "rgba(2,6,12,0.66)");
  }
  Radius { Control = "8px"; Card = "14px"; Pill = "999px"; }
  Shadow { Dialog = "0 12px 32px rgba(15,23,42,0.18)"; }
  ZIndex { Overlay = "50"; }
  FontSize { Caption = "13px"; Body = "15px"; Section = "18px"; Display = "24px"; }
  FontWeight { Medium = "600"; }
  Length { Narrow = "420px"; Wide = "560px"; Rail = "150px"; }
}

enum Priority { Low, Normal, High }

enum Answer { Delete, Keep }

entity Project {
  [MaxLength(80)] string Name;
  [MaxLength(200)] string Notes;
  [Required] Priority Priority;
  security { allow read, create, update, delete when IsAuthenticated; }
}

entity Preferences {
  [MaxLength(40)] string DisplayName;
  [MaxLength(40)] string Density;
  bool ShowNotes;
  security { allow read, create, update when IsAuthenticated; }
}