Skip to content

System dialogs API

client.system.dialogs shows the desktop's own message boxes and file pickers, so an app can ask a question or let the user choose a file without drawing its own modal — and without being trusted to read the filesystem itself.

Availability

MethodPermissions
messageBoxsystem.dialogs
openFilesystem.dialogs, files.read
saveFilesystem.dialogs, files.read
system.files.saveTextAssystem.dialogs, files.read, files.create; files.edit as well to allow replacing an existing file

The file methods browse the connected workspace, so they need a connected host with file capabilities. messageBox works anywhere.

Interface

ts
interface MessageBoxOptions {
  title: string;
  message: string;
  kind?: "info" | "warning" | "error";
  buttons?: readonly { id: string; label: string; destructive?: boolean }[];
  defaultId?: string;
  cancelId?: string;
}

interface OpenFileOptions { title?: string; directory?: string; kind?: "file" | "directory"; multiple?: boolean; extensions?: readonly string[] }
interface SaveFileOptions { title?: string; directory?: string; name?: string }
interface FileSaveSelection { parent: string; name: string; existing?: Readonly<FileEntry> }

interface SystemDialogs {
  messageBox(options: MessageBoxOptions, control?: { signal?: AbortSignal }): Promise<string | null>;
  openFile(options?: OpenFileOptions, control?: { signal?: AbortSignal }): Promise<readonly FileEntry[] | null>;
  saveFile(options?: SaveFileOptions, control?: { signal?: AbortSignal }): Promise<FileSaveSelection | null>;
}

// client.system.files
saveTextAs(options: SaveFileOptions & { text: string; allowReplace?: boolean }, control?: { signal?: AbortSignal }): Promise<TextDocument | null>;

Every method resolves to null when the user dismisses the dialog. messageBox resolves to the id of the button that was chosen.

Examples

Ask before discarding work:

ts
const answer = await client.system.dialogs.messageBox({
  title: "Discard changes?",
  message: "Your note has unsaved changes. Discarding them cannot be undone.",
  kind: "warning",
  buttons: [
    { id: "discard", label: "Discard", destructive: true },
    { id: "keep", label: "Keep editing" },
  ],
  defaultId: "keep",
  cancelId: "keep",
});
if (answer === "discard") reset();

Let the user pick files to work with:

ts
const chosen = await client.system.dialogs.openFile({
  title: "Choose a log file",
  kind: "file",
  multiple: false,
  extensions: ["log", "txt"],
});
if (chosen?.length) await readWith(client.files, chosen[0]);

A selection is a location, not permission to write: you still need files.read to read it, and the file APIs still check revisions.

Save text somewhere the user chooses, in one step:

ts
const saved = await client.system.files.saveTextAs({
  title: "Export notes",
  name: "notes.md",
  text: document.getText(),
});
if (saved) show(`Saved as ${saved.name}`);

saveTextAs creates the file and returns the resulting document. Replacing an existing file happens only when your app holds files.edit and does not pass allowReplace: false; the desktop confirms the replacement with the user.

Errors

CodeMessageMeaning
invalidOptions must be an object.Wrong argument type.
invalidInvalid option: <key>An unknown field.
invalidTitle and message are required.Empty messageBox content.
invalidText is required.saveTextAs without text.
deniedThe extension does not hold the required permission.A missing grant, for example files.create.
unavailableThis service is currently unavailable.No connected file service for the file dialogs.
abortedThe signal fired, or the window closed with the dialog open.

Lifecycle and permissions

Dialogs belong to the window that opened them: they close with it, and cancelling the signal dismisses them. While one is open the desktop marks your window busy, so quitting and disconnecting wait.

Keyboard focus returns to the element that had it before the dialog opened, so a dialog in the middle of a form does not lose the user's place.

These dialogs are the only way an app reaches the filesystem outside the paths it was given: there is no "browse anywhere" API, and no local filesystem access at all. What the user picks in the workspace is what your app receives.

ShellCanvas documentation