Skip to content

Overlay Panels ​

What it is ​

A personal plugin — or a plugin a server admin installed — can add panels to the Dissent desktop app's in-game overlay. When the user opens the overlay over a game your plugin targets, a Plugins launcher lists your panels; the user opens the ones they want, drags them where they like, and the overlay remembers that per game.

Panels are drawn only while the overlay is open. There are no always-on plugin HUD elements.

Declaring panels ​

Add an overlay block and the overlay:context permission to the manifest:

json
{
  "context": ["personal"],
  "declared_permissions": ["storage:user", "game:saves", "overlay:context"],
  "overlay": {
    "api": 1,
    "games": ["slay-the-spire-2"],
    "surfaces": [
      { "id": "wiki", "title": "Wiki", "layer": "panel", "anchor": "right", "size": [440, 760] },
      { "id": "deck", "title": "Deck", "layer": "panel", "anchor": "left",  "size": [460, 760] }
    ]
  }
}

The node validates the block when the plugin is added, and refuses a manifest that breaks any rule:

FieldRule
apiThe overlay contract you wrote against. Optional — absent means 1. A version the node does not support is refused, so declare the one you tested. A desktop app older than your api lists your panels as "needs a newer Dissent" instead of loading them
games1–16 catalog ids (e.g. slay-the-spire-2), never exe names
surfaces1–8 entries; no other fields are allowed in the block
surfaces[].idlowercase letters, digits and hyphens, 1–64 characters, unique
surfaces[].title1–60 characters; shown in the launcher and the panel's title bar
surfaces[].layer"panel" — the only layer plugins may use
surfaces[].anchortop-left, top, top-right, left, center, right, bottom-left, bottom, bottom-right
surfaces[].size[width, height], width 240–1600, height 160–1200

Check it before you publish

A block the node refuses fails the re-add, and users keep your previous manifest. A small test that checks these rules against your manifest.json catches that before it happens.

One page, many panels ​

Every surface loads your manifest's single url. The overlay sends each frame its own init:

javascript
const ctx = await Dissent.init();
ctx.placement; // 'overlay'
ctx.surface;   // 'wiki' or 'deck' — which panel this frame is
ctx.game;      // 'slay-the-spire-2'

What your panel is told it may do ​

javascript
ctx.permissions;  // ['storage:user', 'game:saves', …] — granted AND still declared
ctx.capabilities; // ['storage:get', …, 'game.saves.currentRun', 'overlay.context'] — calls that will be allowed
ctx.overlayApi;   // 1 — the overlay contract this desktop app implements

Check ctx.capabilities before offering a feature, rather than calling and handling a refusal. These three fields, theme updates in panels, and hiding panels with a newer api arrive with the first desktop release after v1.2.233; on an older desktop app they are absent. There is no user identity in a personal plugin: ctx.user is always null.

Wait for init before you draw

The overlay posts dissent:init when the frame finishes loading. If your page mounts a default view before init arrives, a Deck panel will briefly show your Wiki — and download whatever the Wiki needs. Decide the view from ctx.surface after Dissent.init() resolves.

Panels from a server plugin ​

A plugin installed to a server can declare the same overlay block. Two rules differ:

  • Every member decides for themselves. Installing the plugin switches nothing on. Each member turns your panels on in My plugins → Over your game, and until they do, your panels appear nowhere — not drawn, and not listed in the launcher. Say so in your plugin's own description; an admin who expects the panel to appear for everyone will report it as broken.
  • You get a channel. The member chooses which channel your plugin speaks in (automatically, when the plugin is in only one). That is where realtime publishes, and events from it arrive in your panel as they do in the app. A plugin installed in no channel still draws its panels and simply has no realtime.

Everything else is identical, including the lane below.

The overlay lane — what a panel may do ​

The full, frozen list of what a panel can rely on is the Overlay Contract. This section is the short version.

The same list whether the plugin was installed personally or by an admin. A panel over a fullscreen game is where the user has the least context and the chrome is smallest, so what runs there is decided by where it is drawn, not by how it was installed:

PermissionIn a panel
storage:user, storage:local✅
game:saves✅
overlay:context✅
net:direct✅ — desktop only, and it leaves the user's own computer
friends:link✅
realtime✅ — server installs, in the member's chosen channel
fetch:external, bot:post, members:read, media:embed, files:*, clips:gallery, voice, wallet❌

A refused call answers <action> is not available in the overlay. The same plugin may use those capabilities normally on its in-app page — only the panel is restricted.

fetch:external is the one most likely to surprise: it is the node fetching on your behalf, which spends the node's bandwidth and shows the node's address to the site. Use net:direct (desktop) and cache what you fetch, or do the fetching on your in-app page.

What the host draws ​

The overlay draws the chrome around your panel: its title, your plugin's name, the node it came from, a PLUGIN label and a close button. Your page draws only inside the content area and cannot paint over the chrome.

Lifecycle ​

The user…Your frame
opens your panel from the launcherloads, receives init
closes the overlay to go back to the gamestays loaded, hidden — state such as a search survives
reopens the overlayis shown again; overlay.context.changed fires
clicks × on your panelis unloaded
quits the gameis unloaded

Pressing Esc inside your panel closes the overlay — the SDK forwards it to the host.

Reacting when the overlay opens ​

javascript
Dissent.overlay.onChange((o) => {
  if (!o?.panelOpen) return;
  searchInput.focus();   // ready to type
  refreshFromSaves();    // the game may have moved on while the overlay was closed
});

Dissent.overlay.context() answers { game, surface, width, height, panelOpen }, or null outside the overlay. It never includes the game's executable, window position, or anything about other applications.

No save-change cue in the overlay

Dissent.gameSaves.onChange fires on your in-app page only. Overlay panels do not receive it — re-read saves when the overlay opens, as above. The game saves on room changes, which cannot happen while the user is looking at the overlay.

Theme ​

Panels receive the app's theme in init, and dissent:theme-update (the SDK's theme:change event) when the user changes it while your panel is loaded.

Frame budget ​

Your panel shares the screen with a running game.

  • Draw once. Static SVG or DOM for charts; no requestAnimationFrame loops, no canvas redraws, no timers that repaint.
  • A short CSS transition on a user action is fine; a continuous animation is not.
  • Load art lazily, and release object URLs you create when the view that made them goes away.

Calls from a panel ​

Panel calls go through the desktop app's main window, which answers with the same permissions and storage as your in-app page. Results must be JSON. A call that is not answered in 10 seconds fails back to your plugin rather than hanging.

Testing without a game ​

The desktop app is the only overlay host, but you can exercise the overlay code path in a browser: load your page in a frame, send it the overlay init above, answer overlay.context, and post { type: 'dissent:event', event: 'overlay.context.changed', data: null }. Size the frame to your surface's size — a compact layout that only works at full width is the most common panel bug.

Example ​

STS2 Companion (Slay the Spire 2) ships a Wiki panel that focuses its search when the overlay opens, and a Deck panel that re-reads the run in progress each time — both from the same plugin.html that renders the full app page.