Plugin Extension Points

Extension points let a plugin add a component or a standard control to a location owned by Sero. The host owns the location, layout, lifecycle, and validation. A plugin cannot invent a new host surface.

Main app, components, and controls

These manifest concepts are separate:

Manifest fieldOwnerPurpose
sero.app.componentPluginThe plugin's main app surface.
sero.app.contributes.componentsPluginExtra federated React components mounted in host-defined locations.
sero.app.contributes.controlsHostStandard controls rendered by Sero and backed by a plugin action.

Every contribution needs an id that is unique inside its extension point. The same ID can appear in two different extension points. Sero combines it with the app ID and extension point as <app-id>:<extension-point>:<contribution-id>, which gives every mounted contribution a distinct identity.

Supported extension points

Extension pointKindPoint-specific fieldsHost composition
ui.global-search.panelComponentdescription?One panel or tabbed panels
ui.explorer.viewComponentlabel?, icon?Explorer activity item and main view
ui.titlebar.controlComponentNoneInline title-bar controls
ui.chat.model-extensionComponentmodelsAction beside the chat model selector
ui.chat.tool-resultComponentNoneView in the details of a tool call
ui.admin.model-settingsComponentname, description?, icon?Admin Model subsection
ui.dashboard.widgetComponentname, defaultSize?, minSize?, maxSize?, description?, remote?Dashboard grid
workspace.create.optionControlswitch control and tool actionCreate New Workspace form

All component entries require id, extensionPoint, and component. The component value identifies a Module Federation exposed-module key. For example, "KnowledgeSearch" maps to the "./KnowledgeSearch" key in the plugin's exposes configuration. The source module must have a default React component export. Dashboard sizes default to 2 × 2 when omitted.

Set "remote": true on a dashboard widget to also show it in web-remote, the browser client. See Remote widgets for what a widget may use there.

An Admin model-settings contribution must also have a non-empty name. Admin uses this provider-neutral name in its Model subsection selector. The contributed component owns all provider labels, controls, and state.

A chat model-extension contribution must list at least one model. Each model needs a non-empty provider, api, and modelId. Sero mounts the component only when all three values match the selected model. It does not infer support for other models from a provider or model name.

A chat tool-result contribution is shown only for a tool result that names it. The tool puts seroToolResultView: { appId, contributionId } in the result details. Sero then mounts the component in the tool-call details with the props { sessionId, toolCallId, details, isError } (type ChatToolResultViewProps in @sero-ai/common). Sero keeps the normal result text. When no contribution matches, or the component fails, the user sees only the normal result.

Component example

This app has one main component and contributes four additional components:

{
  "sero": {
    "app": {
      "id": "knowledge",
      "name": "Knowledge",
      "icon": "waypoints",
      "stateFile": ".sero/apps/knowledge/state.json",
      "ui": "./dist/ui/remoteEntry.js",
      "component": "KnowledgeApp",
      "contributes": {
        "components": [
          {
            "id": "global-search",
            "extensionPoint": "ui.global-search.panel",
            "component": "KnowledgeSearch",
            "description": "Search indexed knowledge"
          },
          {
            "id": "summary",
            "extensionPoint": "ui.dashboard.widget",
            "component": "KnowledgeSummaryWidget",
            "name": "Knowledge summary",
            "defaultSize": { "w": 2, "h": 2 },
            "minSize": { "w": 1, "h": 1 },
            "maxSize": { "w": 4, "h": 3 }
          },
          {
            "id": "model-settings",
            "extensionPoint": "ui.admin.model-settings",
            "component": "KnowledgeModelSettings",
            "name": "Knowledge provider",
            "description": "Knowledge model defaults"
          },
          {
            "id": "chat-model-action",
            "extensionPoint": "ui.chat.model-extension",
            "component": "KnowledgeModelAction",
            "models": [
              {
                "provider": "openai",
                "api": "openai-responses",
                "modelId": "gpt-5.4"
              }
            ]
          }
        ]
      }
    }
  }
}

Each contributed component is wrapped in the standard app runtime context and plugin style scope. The host can unmount it when the surface closes or changes. Keep state that must survive an unmount in plugin-owned state.

Control example

Controls are data, not arbitrary React components. The first supported control is switch. The first supported action invokes an app-local extension tool.

{
  "contributes": {
    "controls": [
      {
        "id": "workspace-indexing",
        "extensionPoint": "workspace.create.option",
        "control": {
          "type": "switch",
          "label": "Enable indexing",
          "defaultValue": true
        },
        "action": {
          "type": "tool",
          "tool": "enable_index",
          "params": { "mode": "full" }
        }
      }
    ]
  }
}

After Sero creates the workspace, it adds workspaceId, workspaceName, and workspacePath to the action arguments. Host values override static params. A failed optional action does not undo a workspace that was created successfully. Declare appAgent.invokeTool in requiredHostCapabilities when the plugin depends on this action bridge.

Validation and compatibility

Electron validates all contribution data before the renderer receives it:

  • components and controls must be arrays when present
  • required strings must be non-empty
  • IDs must be unique inside one extension point
  • the extension point, control type, and action type must be host-defined
  • only point-specific allowlisted fields are retained

An unknown or malformed optional entry is ignored and recorded as a diagnostic. It does not disable unrelated plugin features. Federated components are still subject to the plugin runtime ABI check.

Extension points are optional by default. If the plugin cannot work without a point, such as ui.admin.model-settings or ui.chat.model-extension, add it to sero.plugin.requiredHostCapabilities. The host then rejects installation on builds that do not provide the point. Use sero.plugin.minSeroVersion for other release-level requirements.

Legacy manifests

Electron still normalises these old fields for installed external plugins:

Compatibility fieldCanonical extension point
searchui.global-search.panel
explorerViewui.explorer.view
titlebarui.titlebar.control
widgets[]ui.dashboard.widget
workspaceCreationworkspace.create.option

Explicit canonical entries take precedence so a plugin can carry both forms during migration without rendering duplicates. New plugins must use sero.app.contributes.

Static and runtime widgets

A static widget is a federated component contribution to ui.dashboard.widget. Use this when the widget is always available with the plugin.

useWidgetRegistration() is a separate app-runtime API. Use it when widget availability is decided at runtime. Its registration is sticky for the current renderer session, even if the component that called the hook unmounts. For manual lifecycle control, registerWidget() returns an unregister function. Both paths use the same host-owned widget chrome and dashboard components.

Remote widgets

Web-remote is the browser client. It runs your widget in a browser tab, with no Electron preload and no file system. A widget only appears there when its contribution sets "remote": true.

Opt in when the widget reads its state and shows it. Do not opt in when the widget needs anything below.

What a remote widget may use

APIWorks in a browserNotes
useAppState() readyesThe host reads the same state file the desktop reads.
useAppState() writeyesSame atomic write, same etag check as the desktop.
Live state updatesyesThe host watches the file and pushes each change.
useAppInfo()partlyappId and workspaceId are set. workspacePath is empty.
useAgentPrompt()yesThe prompt goes to the conversation the person is on.
Everything else on window.seronoEditors, models, tools and app control are absent.

Rules

  • A remote widget must not read workspacePath. A browser has no file system, so the value is empty there.
  • A remote widget must survive a missing API. Check before you call, and show something useful when an API is absent.
  • Widget assets are served from the gateway with a signed ticket. Nothing is served for a widget that did not opt in.

See also