Host Services

Extension code does not talk to Charon’s server or its UI directly. It is handed a set of services by the host, and reaches everything through them - reading and writing game data, opening dialogs and Charon’s own wizards, showing notifications, navigating, and persisting UI state between sessions.

Which object carries them depends on what the extension is:

Extension kind

Where the services are

Custom action

context.services on the action context

Custom page

context.services on the page context

Schema editor

documentControl.services

Property editor

getRootDocumentControl(valueControl).services

The two sets overlap but are not identical: an action or a page gets the project-level services, a document gets the document-level ones. Both are declared Partial - the host populates only what makes sense in the context, so check every field before use.

const dialog = context.services.ui?.dialog;
if (!dialog) {
    return;
}

What Each Context Carries

Service

Action / page context

Document control

gameData

yes

yes

ui.dialog / ui.snackBar

yes

yes

ui.state

scoped to the package

scoped to the document or its schema

navigation

yes

-

userService / projectService / workspaceService

yes

-

undoRedo

-

yes

validationProvider

-

yes

translationLanguage

-

yes

serverApiClient

yes

yes

serverApiClient is the raw HTTP client behind everything else. It is deliberately untyped and its surface is not stable - use it only for something the typed services do not cover, and expect it to change.

Note

services.uiState still exists next to services.ui.state and is the very same object, kept for packages written before ui was introduced. New code should use ui.state.


Game Data

GameDataService is the project’s data, as the server sees it - not the document currently open in a form. Everything returns an observable.

Method

Purpose

getMetadata()

The project’s schemas and settings.

list(schema, parameters?)

Documents of a schema, with skip, take, query, select, filters, sorters, and path for sub-documents. Returns the documents, their metadata, and the total before paging.

find(schema, property, value)

One document by a unique property value.

query(requests)

Several such lookups in one round trip.

bulkChange(schema, documents, importMode, validationOptions?, dryRun?)

Create, update, or delete many documents of one schema.

import(collections, importMode?, schemas?, languages?, validationOptions?, dryRun?)

The same across several schemas at once, taking a whole game data document.

export(exportMode?, schemas?, properties?, languages?)

Read data out in one of the export modes.

validate(validationOptions)

Run validation and get the errors per document.

importMode is the same choice the import wizard offers - createAndUpdate, create, update, safeUpdate, replace, delete. exportMode likewise: normal, publication, localization, extraction.

Tip

dryRun: true on bulkChange and import validates the whole change and reports what would happen without writing anything. Every result carries per-document errors, so an extension can show the user what it is about to do before it does it.

Results carry a metadataHash and a revisionHash - the state of the schemas and of the data at the time of the call. Comparing them across two calls is how an extension notices that someone else changed the project underneath it.


Dialogs and Wizards

ui.dialog opens modals in the host’s own chrome.

Progress

showProgress(options) returns a handle for a long operation - update(percent, message?, color?), setFaulted(), and close(delayMs?). With cancellable: true the dialog can be dismissed and closedHandler(cancelled) fires. See Custom Actions for the full pattern.

Your Own Dialog

showCustom(selector, data?, options?) hosts a custom element your package registered, inside a Charon dialog:

const dialogRef = dialog.showCustom<MyResult>('ext-my-dialog', { itemId }, {
    title: 'Pick an item',
    width: '600px',
    disableClose: false,
});
const result = await firstValueFrom(dialogRef.afterClosed());

The element receives data and closes itself through dialogRef.close(result); afterClosed() emits that result, or undefined when the dialog was dismissed.

showCodeSnippet({ sourceCode }) is a ready-made read-only viewer with copy-to-clipboard, for showing a generated command line or a fragment of JSON.

Charon’s Wizards

An extension can open the editor’s own wizards rather than reimplementing them. Each returns a promise that resolves true when the wizard completed and false when it was cancelled:

Method

Opens

showImportWizard(options?)

Import, optionally pre-loaded with a file or text and a schema

showExportWizard(options?)

Export, optionally with a schema and format preselected

showImportLocalizationWizard(options?) / showExportLocalizationWizard(options?)

The translation round trip

showPublicationWizard(options?)

Publication

showSourceCodeGenerationWizard(options?)

Source code generation

showBackupWizard(options?) / showRestoreWizard(options?)

Backup and restore

All of them take loadPreferences and savePreferences, both defaulting to true - the wizard opens filled in the way the user last left it, and remembers what they do this time. Pass false when the extension is driving the wizard with its own values and should not disturb the user’s saved choices.


Notifications

ui.snackBar is the host’s transient notification strip, in matched sets of three - loadStarted / loadSucceed / loadFailed(error), and the same for save, reload, and delete. Using them keeps an extension’s feedback indistinguishable from the editor’s own, including how errors are rendered.



Persisted UI State

ui.state stores small pieces of state - a zoom level, a collapsed panel, a chosen layout - and gives them back the next time the extension runs. On an action or page context it is scoped to the extension package; on a document control it takes a scope first, 'document' or 'schema'.

state.save('projectPersonal', { zoom, collapsed });
const restored = state.load('projectPersonal');

The layer decides where the value lives and how far it travels. On load, the requested layer is checked first and lower-priority layers after it, so a personal value shadows a shared one:

Layer

Stored in

browserSession

Session storage - gone when the tab closes.

browserPersonal

Local storage, this user.

browserShared

Local storage, everyone using this browser.

projectPersonal

The server, this user, this project.

projectTeam

The server, everyone in the project.

workspacePersonal

The server, this user, across the workspace.

workspaceTeam

The server, everyone in the workspace.

default

Fallback, lowest priority.

Use a browser* layer for view state nobody else cares about, and a *Team layer for something the whole project should share - a canvas layout that everyone should see the same way, for instance. save with null clears the value.


Current User, Project, and Workspace

userService.currentUser$, projectService.currentProject$, projectService.currentBranch$, and workspaceService.currentWorkspace$ are observables of who and where. The project carries its branches, and the branch says whether it is the primary one - enough for an extension to warn before writing to production data, or to label what it produces.


See also