# Custom Contexts

The default `PluginContext`, `GlobalPluginContext`, and `SessionPluginContext` give you a registry, a bus, and some framework-level plumbing. That is enough for small apps. Once your domain has opinions, you usually want the context to carry them directly instead of resolving the same services over and over.

Custom contexts are how you do that.

Reach for this after the plugin shape is already clear. Contexts are for
shared domain facts that many plugins need: the current document, current user,
tool registry, app shell, or routing table.

## When you need one

Use the default contexts if plugins only ever interact with the registry and the bus. If your plugins keep reaching for the same piece of state (the current user, the active document, a tool registry, a routing table), promoting it to a field on a custom context makes every plugin simpler.

Rough rule: if three plugins want the same thing, put it on the context.

## Subclassing

Subclass `SessionPluginContext` (or `GlobalPluginContext`, or both) to add fields.

```dart
extractRegion(customContextSource, 'session-plugin-context-subclass')
```

Session plugins can now type their context directly:

```dart
extractRegion(customContextSource, 'session-plugin-typed-context')
```

No casting, no re-resolving. `context.document` and `context.user` are always there.

## Override `copyWith()` on every subclass

Every subclass MUST override `copyWith()` to return its own type. The framework calls `copyWith()` on the live context to snapshot pre-reconcile state and pass it to `Plugin.onPluginSettingsChanged(oldContext, newContext)` as `oldContext`. Without an override, virtual dispatch falls through to the parent's `copyWith`, which constructs the parent type. Any plugin that overrides `onPluginSettingsChanged` with a covariant subtype will then throw `TypeError` at runtime when `oldContext` arrives as a base-type instance.

```dart
class EditorSessionContext extends SessionPluginContext {
  final Document document;
  final User user;

  EditorSessionContext({
    required this.document,
    required this.user,
    required super.registry,
    required super.bus,
    required super.globalBus,
    super.extras,
  });

  @override
  EditorSessionContext copyWith({
    ServiceRegistry? registry,
    Map<String, Object>? extras,
    EventBus? bus,
    EventBus? globalBus,
    Document? document,
    User? user,
  }) {
    return EditorSessionContext(
      document: document ?? this.document,
      user: user ?? this.user,
      registry: registry ?? this.registry.copy(),
      bus: bus ?? this.bus,
      globalBus: globalBus ?? this.globalBus,
      extras: extras ?? this.extras,
    );
  }
}
```

The annotation `@mustBeOverridden` on the parent's `copyWith()` causes the analyzer to warn if you forget. The runtime additionally asserts `oldContext.runtimeType == liveContext.runtimeType` during reconciliation in debug mode, so a missed override surfaces immediately in tests rather than at the first covariant `onPluginSettingsChanged` call.

## Telling the runtime about it

The runtime does not know your custom context type unless you hand it a factory.

| Scope | Where to supply the factory |
|---|---|
| Global | `runtime.init(globalContextFactory: ...)` |
| Session | `runtime.createSession(contextFactory: ...)` |

```dart
extractRegion(runtimeTestSource, 'runtime-test-session')
```
**Forget the factory and the runtime throws:** If a plugin expects a custom context type but you forgot to pass the factory, the runtime throws `StateError` at init or `createSession`. The default factories only build the base types, and silently downcasting would hide the misconfiguration until a plugin tried to read a field that did not exist. The loud failure is the better tradeoff.

## A realistic global context

A global context usually carries application-level singletons that do not belong in the registry: the top-level app object, a telemetry client, a feature flag service.

```dart
extractRegion(customContextSource, 'global-plugin-context-subclass')
```

Global plugins consume it exactly like session plugins consume the session context.

```dart
extractRegion(customContextSource, 'global-plugin-typed-context')
```

## Mixing scopes

A session plugin can declare a custom session context while the global context stays the default, and vice versa. The two are independent. A plugin that extends `SessionPlugin<EditorSessionContext>` is happy as long as its session's factory produces that type. Whatever the global scope looks like is not its concern.

## Factories on the runtime

`PluginRuntime` exposes both factories directly.

```dart
extractRegion(runtimeTestSource, 'runtime-test-runtime')
```

No subclassing needed.

## Do not over-model

Two small cautions.

**Do not put everything on the context.** If only one plugin uses a field, that field belongs in that plugin's services, not on the context. The context is a shared scratchpad for things multiple plugins want.

**Do not mirror the registry.** If a field on the context is already resolvable from the registry, you are probably duplicating information that will drift. Pick one home.

Most teams converge on a small, stable custom context with three to five fields. Anything past that usually wants to be a service, a capability, or a session-scoped service.

## Related reading

[Plugins](https://plugin-kit.saad-ardati.dev/concepts/plugins/)
  [Runtime](https://plugin-kit.saad-ardati.dev/concepts/runtime/)
  [Plugin Services](https://plugin-kit.saad-ardati.dev/concepts/plugin-services/)