Quasar plugins are trusted UI extensions loaded by the Quasar web host. They are separate from Magnetar Dedicated Server plugins. The first target plugin is the Entity Viewer: a heavy, mostly independent UI surface that should live outside the Quasar core repository while still feeling native inside Quasar.
- Keep Quasar core small while allowing large feature surfaces to ship independently.
- Let plugins add nav items, routable Razor pages, dialogs, dashboard panels, and entity/server actions.
- Let plugins replace, wrap, or inline selected built-in Quasar pages, components, and smaller named UI regions through explicit extension targets.
- Give UI plugins a generic request/event channel to companion Magnetar plugins running inside managed Dedicated Servers.
- Keep the UI visually consistent by making MudBlazor the default component library and theme surface for all Quasar plugins.
Quasar- Loads trusted Quasar UI plugins.
- Owns auth, layout, routing, MudBlazor theme, and the agent WebSocket.
- Exposes
Quasar.Plugin.Abstractions.
quasar-hub- Holds reviewed XML manifests, similar to MagnetarHub.
- Manifests point to the individual plugin repositories and pinned commits.
- It does not host plugin source code.
quasar-plugin-template- Starting point for new Quasar UI plugin repositories.
- Shows manifest, project layout, MudBlazor usage, nav contributions, page contributions, and companion-channel usage.
- Plugin repositories
- Host source for one Quasar UI plugin.
- May also host shared DTO/contracts used by companion Magnetar plugins.
Quasar UI plugins are loaded into the Quasar process during startup. They are trusted code and have the same process privileges as Quasar. The first version should require a Quasar restart after install, update, enable, or disable.
Runtime hot-unload is not a first target. Blazor routable components, static asset mounts, and dependency injection registrations are easier to reason about when plugin lifetime is tied to the Quasar host lifetime.
Dynamic enable/disable can still be user-friendly because Quasar already has a bootstrap/launcher flow. The UI can update plugin configuration, ask the worker to restart, and let Bootstrap bring the worker back with a clean plugin assembly set.
Quasar must have a way to start without loading dynamic UI plugin assemblies. This prevents one bad plugin from permanently bricking the web UI.
Safe-boot triggers:
- command line:
--safe-mode - environment variable:
QUASAR_SAFE_MODE=1 - environment variable:
QUASAR_DISABLE_UI_PLUGINS=1 - install-root marker file:
{Quasar install directory}/ui-plugins.safe-mode - automatic Bootstrap fallback after repeated worker startup failures or quick crashes
Safe-boot behavior:
- do not load plugin assemblies
- do not mount plugin static assets
- do not register plugin endpoints
- ignore plugin route/nav/patch contributions
- keep core Quasar pages available
- show safe-mode status on
/settings/ui-plugins - expose recovery controls for the marker-file safe boot path
Plugin activation should be last-known-good:
- User installs/enables/updates a plugin.
- Quasar writes enabled/disabled state to
{Quasar install directory}/ui-plugins.state.json. - Quasar restarts through Bootstrap.
- The new worker loads plugins.
- After startup and health checks pass, Quasar marks the plugin set active.
- If startup fails repeatedly, Bootstrap creates the safe-mode marker and starts Quasar without dynamic UI plugins.
Disabling a plugin is then config-first: mark disabled, restart worker, load a fresh process without that plugin.
Each plugin repository should contain a quasar-plugin.json manifest. Quasar
uses this after resolving a plugin from quasar-hub.
Example:
{
"id": "cometworks.entityviewer",
"displayName": "Entity Viewer",
"version": "0.1.0",
"entryAssembly": "CometWorks.EntityViewer.Quasar.dll",
"entryType": "CometWorks.EntityViewer.Quasar.EntityViewerQuasarPlugin",
"projectPath": "src/CometWorks.EntityViewer.Quasar/CometWorks.EntityViewer.Quasar.csproj",
"staticAssets": "src/CometWorks.EntityViewer/wwwroot",
"stylesheets": [
"quasar-plugin.css"
],
"quasarVersion": ">=1.1.0",
"companionPlugins": [
{
"id": "cometworks.entityviewer",
"projectPath": "src/CometWorks.EntityViewer.Magnetar/CometWorks.EntityViewer.Magnetar.csproj",
"entryAssembly": "CometWorks.EntityViewer.Magnetar.dll"
},
"GridBackups"
]
}companionPlugins accepts either a string id or an object. A string names an
external Magnetar plugin that the UI plugin can call through the companion
channel. An object with projectPath is owned by the UI plugin package: Quasar
builds that Magnetar companion project during UI plugin install and deploys it
automatically to managed servers while the UI plugin is enabled. entryAssembly
is optional; when omitted, Quasar derives {project file name}.dll.
The quasar-hub XML manifest remains the public catalog entry. The package
manifest is the plugin repository's runtime/build descriptor.
Quasar discovers reviewed UI plugin packages from
https://github.com/CometWorks/quasar-hub on the main branch. The hub follows
the MagnetarHub pattern: XML files under Plugins/ point at individual plugin
repositories and pinned commits. Quasar only shows entries whose PluginKind is
QuasarUiPlugin.
The /settings/ui-plugins page now manages the QuasarHub catalog:
- refreshes and caches the catalog in
{Quasar install directory}/Caches/ui-plugin-hub-catalog.json - checks QuasarHub on Quasar startup and every 15 minutes so installed package update availability stays current
- automatically installs or updates reviewed hub entries that opt into
ImplicitLoading; disabled installed plugins stay disabled across implicit updates, while explicitly removed plugins stay removed - shows installed, update-available, recommended, hidden, and invalid package states
- enables or disables installed packages for the next restart
- downloads the pinned GitHub source archive over HTTPS; Git is not required
- prefers a matching .NET SDK on
PATH, then Quasar's private managed SDK, before building source packages - builds the declared plugin project with
dotnet build - passes
QuasarPluginAbstractionsAssemblyto the build so the plugin compiles against the contract DLL loaded by the running Quasar worker - builds any owned Magnetar companion projects declared in
companionPluginsobjects, passingMagnetarProtocolAssemblyand the resolvedDS64directory so they compile against the protocol and Dedicated Server assemblies used by the Quasar-managed host; if Quasar cannot resolve a valid DS64 directory, the companion project's ownDS64fallback remains in effect - removes local plugin packages and records an opt-out from implicit reinstall
- links back to the plugin repository and QuasarHub
Installed UI plugin source packages live under:
{Quasar install directory}/Plugins/{catalog id}
Installer staging lives under:
{Quasar install directory}/Caches/ui-plugin-installer
Enabled/disabled state and explicit implicit-install opt-outs live under:
{Quasar install directory}/ui-plugins.state.json
The first installer supports root quasar-plugin.json package manifests. Build
configuration comes from QUASAR_UI_PLUGIN_BUILD_CONFIGURATION,
Quasar:Plugins:BuildConfiguration, or Debug in development and Release
otherwise.
The installer passes the running worker's physical
Quasar.Plugin.Abstractions.dll to adapter builds as
QuasarPluginAbstractionsAssembly. Release packaging keeps that DLL beside the
single-file worker so Bootstrap-managed installs can build UI plugins from
QuasarHub without needing a NuGet package.
The ASP.NET Core runtime is enough to run Quasar, but not enough to compile
source-built UI plugins. Quasar checks for a compatible .NET 10 SDK on PATH
first. If one is present, Quasar uses it and never downloads a private copy. If
not, Quasar checks its managed SDK directory:
{Quasar data}/ManagedRuntime/Tools/DotNetSdk/{pinned version}
When an administrator starts an install or update and neither SDK is available,
the UI offers three outcomes: download the pinned private SDK, install the SDK
manually through the operating system's package manager, or cancel. The private
archive has a platform-specific pinned URL and SHA-512, is verified before
extraction, and is used without changing the process PATH or installing system
packages. Downloads are staged below
{Quasar data}/ManagedRuntime/Cache/DotNetSdk and only begin after explicit
approval. The pinned SDK is currently 10.0.111 for Windows and Linux on x64 and
Arm64.
Owned companion build output is written under
{installed package}/.quasar/companions/{companion id}. Quasar passes
MagnetarProtocolAssembly from the running worker or staged Agent/ directory
and builds companions for x64, matching Space Engineers Dedicated Server.
When a managed server is prepared, enabled UI plugin companions are copied into
that server's Magnetar Local folder and their entry assemblies are added to
the generated Magnetar Current.xml profile. Shared loader/protocol files such
as Magnetar.Protocol.dll, Quasar.Agent.dll, 0Harmony.dll, and
PluginSdk.dll are not copied from companion output; Quasar deploys its own
agent/protocol files.
At startup, Quasar resolves plugin dependencies from the plugin's shadow-copied
build output and records contribution enumeration failures as plugin load errors
instead of letting one plugin crash the worker during catalog construction.
Install, update, and remove operations change files on disk only. The active
plugin assembly set is still loaded at Quasar worker startup, so each operation
requires a Quasar restart before it takes effect. When that restart is triggered
from the UI Plugins page, the browser shows restart progress and polls
/api/health until the replacement worker is ready. The worker writes
Updates/worker-restart-request.json; Bootstrap consumes that request, drains
the current worker through /api/internal/drain, and starts the same active
release again so the plugin assembly set is rebuilt from disk. The restart
watcher is stored in browser session storage, so refreshing the page resumes the
same progress overlay and health polling. Managed Space Engineers server
processes stay detached during the Quasar worker restart and are adopted as
running by the replacement worker when their process id is still alive.
Removing a hub plugin records its catalog ID in ui-plugins.state.json, so an
ImplicitLoading entry is not downloaded again. A later manual install clears
that opt-out.
QuasarHub descriptors can set <ImplicitLoading>true</ImplicitLoading> for
reviewed plugins that should be present by default. Quasar installs or updates
those entries during hub refresh, except in safe mode. A first implicit install
is enabled for the next restart; if an already-installed plugin was explicitly
disabled, implicit updates keep it disabled. Explicitly removed entries are
skipped until an operator installs them again.
Descriptors can independently set <Recommended>true</Recommended> to feature
an opt-in plugin without installing it automatically. Recommended entries sort
near the top of /settings/ui-plugins and display a recommendation badge. The
Entity Viewer uses this state and is no longer implicitly installed for every
Quasar instance.
Quasar exposes the initial contract project as Quasar.Plugin.Abstractions.
The core entry point is:
public interface IQuasarPlugin
{
string Id { get; }
string DisplayName { get; }
void ConfigureServices(IServiceCollection services, QuasarPluginContext context);
void ConfigureEndpoints(IEndpointRouteBuilder endpoints, QuasarPluginContext context);
IEnumerable<Assembly> GetRazorAssemblies();
IEnumerable<QuasarNavItem> GetNavItems();
IEnumerable<QuasarExtensionContribution> GetExtensions();
}The first contract surface includes:
IQuasarPluginQuasarPluginContextQuasarPluginManifestQuasarNavItemQuasarNavZonesQuasarExtensionContributionQuasarExtensionTargetsQuasarPatchModeQuasarPolicyNamesIQuasarCompanionChannel- companion request/response envelopes
Service registration happens before WebApplication.Build(). Endpoint
registration happens after the app is built. Razor assemblies are passed to the
Blazor router and Razor component endpoint builder so plugin pages can be routed
normally.
QuasarPluginContext.InstallDirectory is the resolved Quasar install root. It
honors QUASAR_INSTALL_DIR, QUASAR_INSTALL_DIR_FILE, and development
.quasar-install-dir files through Quasar's normal path resolver. Plugins that
need durable Quasar-owned files should base them from this property instead of
reading those environment variables directly. PluginDirectory remains the
package manifest directory, and CacheDirectory remains the shadow-copy load
directory.
The Quasar host has an initial no-op catalog at
Quasar.Services.Plugins.QuasarUiPluginCatalog. It provides the stable
integration seam for:
- safe-mode detection
- plugin DI registration
- plugin endpoint registration
- plugin static asset mounting
- sidebar navigation rendering
- router
AdditionalAssemblies - Razor component endpoint
AddAdditionalAssemblies
The catalog also performs first-pass local package discovery and shadow-copy
loading. It scans plugin package roots for quasar-plugin.json, loads the
declared entry assembly from a cache folder, and exposes any load failures in
startup logs and the /settings/ui-plugins page.
The /settings/ui-plugins page also shows loaded plugin entry and static asset
summaries. Full entry, directory, and static asset diagnostics live behind each
row's paths menu; hover reveals full values and file or directory metadata, while
copy buttons still copy the full value. The page also shows declared stylesheet
paths, nav contributions, extension contributions, and startup load errors. It
can create or clear the safe-boot marker, disable a loaded package for the next
restart, and manage QuasarHub-installed packages.
Configuration/environment knobs:
Quasar:Plugins:DirectoryQuasar:Plugins:BuildConfigurationQUASAR_UI_PLUGIN_DIRQUASAR_UI_PLUGIN_DIRSQUASAR_UI_PLUGIN_BUILD_CONFIGURATION
Default plugin root:
{Quasar install directory}/Plugins
Shadow-copy root:
{Quasar install directory}/Caches/ui-plugins/{pluginId}/{entryAssemblyHash}
Plugin navigation items are rendered by the main Quasar sidebar. Supported zones:
QuasarNavZones.MainQuasarNavZones.SettingsQuasarNavZones.Admin
QuasarNavZones.Admin inherits QuasarPolicyNames.CanManageSecurity when the
plugin item does not specify its own policy. Navigation policy checks only hide
links; plugin pages and endpoints must still enforce their own authorization.
Extension contributions are rendered through named outlets. The first active hosted targets are:
QuasarExtensionTargets.EntitiesPageQuasarExtensionTargets.EntityActionsQuasarExtensionTargets.EntityViewerColumnHeaderQuasarExtensionTargets.EntityViewerColumnCellQuasarExtensionTargets.PluginsPage
EntitiesPage wraps the body of the /entities page, so a plugin can render
before/after it, wrap it with a component that accepts ChildContent, or replace
it entirely. Wrap contributions receive ChildContent; when a wrapper is not
authorized, Quasar renders the original child content. EntityActions renders
inside each entity row action cell. EntityViewerColumnHeader and
EntityViewerColumnCell expose the dedicated viewer button column so a viewer
plugin can add the viewer header/button and fullscreen dialog without replacing
the whole entities page. Their component parameters are intentionally primitive:
AgentIdServerNameEntityIdEntityTypeEntitySubTypeEntityDisplayNameOwnerSteamIdCanViewEntity
This lets viewer plugins render a button and open their own dialog/static asset route without taking a dependency on Quasar page internals. Quasar core does not ship a fallback browser viewer route.
PluginsPage renders at the bottom of /settings/ui-plugins. It receives
parameters named UiPlugins, LoadedPlugins, and LoadErrors so plugins can
append diagnostics or management panels without replacing the core status page.
Plugins must be able to register their own services during Quasar startup.
ConfigureServices() is the intended hook for this.
Allowed registrations:
- singleton state owned by the plugin
- scoped UI/session services
- transient helpers
- typed HTTP clients
- hosted/background services, when clearly bounded and cancellable
- companion-channel clients wrapping
IQuasarCompanionChannel
Rules:
- Plugin DI registration happens only during startup, before
WebApplication.Build(). - Enabling, disabling, installing, or updating a plugin requires a worker restart so DI returns to a clean state.
- Plugin singletons live for one Quasar worker process lifetime.
- Safe boot skips plugin
ConfigureServices(), so plugin services are not loaded when recovering from a bad plugin. - Plugins should namespace their service types and options to avoid collisions.
- Plugins should not replace Quasar core services unless Quasar exposes an explicit replacement/decoration target.
- Plugins should prefer
TryAdd*or plugin-specific wrapper services when sharing infrastructure.
Recommended plugin layout:
Plugin.Ui/
Components, pages, UI services, singleton state
Plugin.Quasar/
Thin adapter implementing IQuasarPlugin and calling AddPluginUi()
PreviewHost/
Local MudBlazor app that calls the same AddPluginUi()
This keeps component previews honest: the preview host and Quasar adapter use the same plugin services.
Quasar currently uses one Blazor Router in Components/Routes.razor. The
plugin loader should collect plugin Razor assemblies and expose them to the
router:
<Router AppAssembly="typeof(Program).Assembly"
AdditionalAssemblies="@PluginCatalog.RazorAssemblies"
NotFoundPage="typeof(Pages.NotFound)">Quasar should also pass plugin assemblies to Razor component endpoint mapping in
Program.cs.
Plugins can then provide normal routable Razor components:
@page "/entity-viewer"Replace hardcoded nav markup with a contribution model. Core Quasar pages should also become built-in contributions so plugin and core nav use the same path.
public sealed record QuasarNavItem(
string Text,
string Href,
string Icon,
string Zone,
int Order,
string? Policy);Suggested zones:
nav.mainnav.settingsnav.adminnav.hidden
The nav renderer applies auth policies, sort order, and MudBlazor MudNavLink
styling. Plugins should provide Material icon names from MudBlazor when
possible.
Quasar should support explicit patch targets instead of unsafe global monkey patching. Built-in pages/components that can be replaced get stable target keys.
public sealed record QuasarComponentPatch(
string TargetKey,
Type ComponentType,
QuasarPatchMode Mode,
int Priority,
string PluginId,
string? Policy);
public enum QuasarPatchMode
{
Replace,
Before,
After,
Wrap
}Route-level replacement should use route hosts. For example, the current
/entities route becomes a thin wrapper:
@page "/entities"
<QuasarPageHost PageKey="quasar.page.entities"
DefaultComponent="typeof(EntitiesPage)" />The existing page body moves into EntitiesPage.razor without an @page
directive. A plugin can replace quasar.page.entities without competing for the
same route.
Interior extension targets use a component host:
<QuasarComponentHost TargetKey="quasar.component.entity-actions"
DefaultComponent="typeof(EntityActions)" />Fine-grained inline regions are also feasible, but only where Quasar core places a named outlet. Razor components are compiled, so Quasar should not try to patch arbitrary markup or component declarations after compilation. Instead, core components can expose stable region keys around small declarations that plugins are allowed to extend or replace:
<QuasarExtensionOutlet TargetKey="quasar.entities.toolbar-actions"
Parameters="@ToolbarParameters">
<MudButton StartIcon="@Icons.Material.Filled.Refresh"
OnClick="LoadAsync">
Refresh
</MudButton>
</QuasarExtensionOutlet>Useful inline region shapes:
- toolbar button groups
- filter controls
- summary chips
- row action buttons
- table columns or cell fragments
- details tabs
- empty/error/loading states
- dialog footers
Each region must document its parameter contract. Small region parameters should
prefer primitives, immutable DTOs, callbacks, and RenderFragment values over
page-private model types. This keeps plugins decoupled from the internal shape of
the Razor component.
For declaration replacement, the default declaration becomes the outlet's child
content. A plugin can use Before/After to inline adjacent UI, Wrap to
decorate the declaration through ChildContent, or Replace to suppress the
default declaration and render its own component. This gives most of the power of
specific Razor declaration replacement while keeping the patch surface explicit,
reviewable, and testable.
Conflict rules:
- One
Replacewinner per target. - Highest priority wins.
- Same priority replacement conflict is a startup error or disables both replacements with a clear diagnostic.
BeforeandAftercontributions are ordered by priority.- The plugin manager must show which plugin patches which target.
- Fine-grained targets follow the same conflict rules as page/component targets.
The first useful targets:
quasar.page.entitiesquasar.page.pluginsquasar.page.analyticsquasar.dashboard.panelsquasar.component.entity-actionsquasar.component.entity-details-tabsquasar.component.server-detail-actionsquasar.entities.toolbar-actionsquasar.entities.summary-chipsquasar.entities.table-columnsquasar.entities.viewer-column-headerquasar.entities.viewer-column-cellquasar.entities.empty-state
Plugins can ship static assets under their own package. Quasar should mount each plugin's static asset root under a deterministic path:
/_quasar/plugins/{pluginId}/
The Entity Viewer can keep its JavaScript/Three.js-heavy surface in its own
repository and serve it from that plugin path. Quasar core no longer copies
viewer assets into Quasar/wwwroot; the QuasarHub installer downloads the pinned
viewer repository archive, builds the adapter project, and loads the package from
the Quasar install directory.
Plugins can also ask Quasar to inject package stylesheets into the host page by declaring manifest-relative paths:
"stylesheets": [
"styles.css"
]Relative stylesheet paths resolve under staticAssets and are emitted as:
/_quasar/plugins/{pluginId}/{stylesheet}
Absolute http://, https://, or root-relative paths are allowed for trusted
plugins. Injected stylesheets load after Quasar core CSS, so plugin CSS must use
narrow selectors and MudBlazor/Quasar CSS variables instead of global resets.
UI plugins often need live server data from companion Magnetar plugins. Quasar exposes a generic channel instead of feature-specific Quasar endpoints.
public interface IQuasarCompanionChannel
{
Task<TResponse> SendAsync<TRequest, TResponse>(
string serverId,
string companionPluginId,
string operation,
TRequest request,
CancellationToken cancellationToken);
}The channel rides on the existing Quasar.Agent WebSocket transport. Quasar sends
ServerCommandType.PluginRequest to the selected agent; the command payload is a
generic CompanionPluginRequest with JSON owned by the caller and target
plugin. The agent dispatches the request to a loaded Magnetar plugin that
implements Magnetar.Protocol.Bridge.IQuasarCompanionRequestHandler and returns
a CompanionPluginResponse.
Envelope fields:
pluginIdoperationschemaVersioncorrelationIdpayloadJson
Quasar performs web auth at the UI/plugin endpoint layer and binds requests to a connected managed server. Companion plugins handle only their own operation names and own the typed request/response DTOs for those operations.
UI-plugin-owned companions are local plugins from Quasar's perspective, not
MagnetarHub selections. Enabling a UI plugin enables its owned companion for all
managed servers prepared after the next Quasar restart. Disabling the UI plugin
removes that companion from subsequently generated Magnetar profiles, although
old copied DLLs can remain inert in the server's Local folder.
Entity Viewer is the first Quasar UI plugin.
Entity Viewer owns:
- entity-list viewer button contribution
- fullscreen viewer shell
- static viewer assets
- audit/timeline UI
- typed request DTOs for viewer-specific operations
- companion Magnetar plugin handlers for viewer scene capture
GridBackups owns:
- backup files
- backup retention
- backup/restore events
- grid state serialization
- companion-channel handlers for grid audit, backup list, snapshot metadata, and future snapshot content
Entity Viewer should call its Magnetar companion plugin and GridBackups through the companion channel. Quasar core does not ship a viewer scene API or viewer-specific scene DTOs. GridBackups should not reference the Entity Viewer UI assembly. If shared DTOs are useful, put them in a small contracts assembly that both can reference.
Quasar plugins should use MudBlazor for all normal UI:
- navigation
- buttons
- icon buttons
- dialogs
- tables/data grids
- forms
- tabs
- menus
- alerts
- cards/papers
- progress indicators
Custom HTML/CSS is fine for domain-specific surfaces such as a Three.js viewer, but the surrounding controls, dialogs, filters, and settings must use MudBlazor and the current Quasar theme. Plugins should avoid competing CSS frameworks, hard-coded color systems, and standalone app shells.
Quasar should pass its MudBlazor theme and layout expectations through the plugin abstractions. Plugins should prefer theme tokens and MudBlazor spacing/color primitives over custom palettes.
Quasar UI plugins are not sandboxed. They can execute arbitrary .NET code inside the Quasar process. Therefore:
- load only reviewed plugins from
quasar-hubby default - pin plugin commits
- record package hashes after build/download
- show requested nav, endpoint, static asset, and patch contributions before enabling a plugin
- require admin approval for plugins that replace built-in pages
- make update diffs visible before applying them
- Add
Quasar.Plugin.Abstractions. - Add plugin manifest loader and local plugin catalog.
- Add plugin Razor assembly registration.
- Convert nav to contribution-driven rendering.
- Add static asset mounting.
- Add page/component extension hosts.
- Add fine-grained inline region outlets for stable declarations.
- Add first patch targets on Entities and Dashboard.
- Add generic companion data channel through Quasar.Agent.
- Use Entity Viewer from its external Quasar UI plugin repository.
- Add GridBackups companion handlers for audit/grid/snapshot requests.