# Dialog API

This page is the curated reference for Plugin Kit Dialog. It covers two packages and the line between them is the architectural point of the dialog: declaration types live in `plugin_kit` (Dart-only) so non-Flutter packages can declare configurable services without taking a Flutter dependency, while the actual UI, controller, theme, and visuals live in `plugin_kit_dialog` (Flutter).

For an end-to-end walkthrough, see the [Plugin Kit Dialog guide](https://plugin-kit.saad-ardati.dev/guides/plugin-kit-dialog/). This page is the API.

## Package split at a glance

| Type | Package | Why it lives there |
|---|---|---|
| `UiConfigurableCapability` | `plugin_kit` | Declaration; const-constructable from Dart-only code. |
| `ConfigField` and subclasses | `plugin_kit` | Declaration; no Flutter types in the public surface. |
| `ConfigFieldHandle` | `plugin_kit` | Read/write contract for renderers; `Object?`-based. |
| `showPluginKitDialog`, `PluginKitDialog`, `PluginKitDialogBody` | `plugin_kit_dialog` | Material widgets. |
| `PluginKitDialogController` | `plugin_kit_dialog` | `ChangeNotifier`-backed draft. |
| `PluginKitDialogTheme`, `buildPluginKitDialog{Dark,Light}Theme` | `plugin_kit_dialog` | `ThemeExtension` and `ThemeData` builders. |
| `PluginKitVisualsPlugin`, `PluginKitVisual` | `plugin_kit_dialog` | Host-app overrides for icon, color, label across plugin, namespace, and service axes. |

## Declarative types (in `plugin_kit`)

These four exports are everything you need to declare a configurable service. None of them imports Flutter.

### `UiConfigurableCapability`

```dart
extractRegion(uiConfigurableCapabilitySource, 'ui-configurable-capability-ui-configurable-capability')
```

Attach this capability to a service registration to make it editable in the Services tab. `label` is the section title rendered above the fields; `description` is an optional one-line subtitle; `fields` is rendered top-to-bottom in the card.

A service may attach multiple `UiConfigurableCapability` instances; each becomes its own sub-section under the same service card. Capability resolution otherwise follows the rules described on the [Capability](https://plugin-kit.saad-ardati.dev/reference/service-registry-and-capabilities/) reference page.

```dart
extractRegion(dialogSnippets, 'dialog-reference-service-namespace')
```

### `ConfigField` (sealed base)

```dart
extractRegion(configFieldSource, 'config-field-config-field')
```

Every field carries the same four slots. `key` is a dotted path under `ServiceSettings.config` (`'model'`, `'provider.name'`, `'limits.max_tokens'`); the dotted form writes to nested maps when the dialog saves. `defaultValue` is what the per-field reset button restores to. `ConfigNode` reads only stored settings values and returns `null` for absent keys. See [`ConfigNode`](https://plugin-kit.saad-ardati.dev/concepts/configuration/) for the read-side contract.

The `sealed` modifier means the dialog's renderer dispatch is exhaustive; you cannot add new field types without declaring a new renderer for them through [`ExtensionConfigField`](#extensionconfigfield).

### `TextConfigField`

```dart
extractRegion(configFieldSource, 'config-field-text-config-field')
```

Single-line text input. `placeholder` shows when no value is set.

### `MultilineConfigField`

```dart
extractRegion(configFieldSource, 'config-field-multiline-config-field')
```

Multi-line editor sized between `minLines` and `maxLines`. `moustacheTags` declares insertable tag chips shown under the editor; tapping a chip inserts the tag at the caret. Useful for system-prompt fields that accept template variables like `{{user_name}}` or `{{tool_list}}`.

### `PasswordConfigField`

```dart
extractRegion(configFieldSource, 'config-field-password-config-field')
```

Obscured input with a show/hide toggle. Same shape as `TextConfigField`; rendered with the entry hidden by default.

### `NumberConfigField`

```dart
extractRegion(configFieldSource, 'config-field-number-config-field')
```

Numeric input with two render modes. When `style` is null (the default), the dialog auto-picks: slider when both `min` and `max` are non-null, otherwise text input. Set `style` explicitly to force a mode.

When `isInteger` is true, values are stored as `int`, parsing strips decimals, and the slider step defaults to `1` if `step` is null.

### `NumberFieldStyle`

```dart
extractRegion(configFieldSource, 'config-field-number-field-style')
```

Forces one render mode for `NumberConfigField`. With `textInput`, any `min`/`max` clamp the parsed value rather than constraining a slider track.

### `DropdownConfigField<T>`

```dart
extractRegion(configFieldSource, 'config-field-dropdown-config-field')
```

Typed dropdown over `List<DropdownOption<T>>`. The generic flows through to the saved value: a `DropdownConfigField<String>` stores `String` in the config map.

### `DropdownOption<T>`

```dart
extractRegion(configFieldSource, 'config-field-dropdown-option')
```

A single selectable option. Positional constructor, value first, label second.

### `BoolConfigField`

```dart
extractRegion(configFieldSource, 'config-field-bool-config-field')
```

Switch input. Carries no extra state beyond the four base slots.

### `GroupConfigField`

```dart
extractRegion(configFieldSource, 'config-field-group-config-field')
```

Visual sub-section. Renders `label` as a sub-heading and indents `children` beneath it. `key` is the group's write target, and nested fields read and write their own dotted keys inside the map stored at that `key`.

`GroupConfigField` does not accept `defaultValue` (`super.defaultValue` is omitted): groups have no value of their own.

### `ExtensionConfigField`

```dart
extractRegion(configFieldSource, 'config-field-extension-config-field')
```

Escape hatch for custom renderers without taking a Flutter dependency at the declaration site. The dialog looks up a Flutter-side renderer registered under `rendererKey` and forwards `args` to it. See [Custom renderers](#custom-renderers) below.

### `ConfigFieldHandle`

```dart
extractRegion(configFieldSource, 'config-field-config-field-handle')
```

The opaque value handle a renderer receives. `value` reads and writes the current working value; setting it dirties the draft. `isOverridden` is true when the working value differs from the field's declared default. `reset()` restores the value to `defaultValue`.

Use this from custom renderers; the built-in renderers consume it internally.

## Flutter entry points (in `plugin_kit_dialog`)

Three widgets and one controller. Pick the entry point that matches how much chrome you want to bring yourself.

### `showPluginKitDialog`

```dart
extractRegion(pluginKitDialogSource, 'plugin-kit-dialog-show-plugin-kit-dialog')
```

The default entry point: opens a Material `Dialog` containing a `PluginKitDialog`, awaits user interaction, and resolves with the saved `RuntimeSettings` or `null` on cancel. The function builds its own `PluginKitDialogController` internally; you do not need to manage one.

`onSave` is the host-side persistence callback. It runs *before* the dialog closes, so the dialog can reflect any error you choose to surface and so you can write to disk or push to `runtime.updateSettings(...)` while the user is still looking at the spinner.

```dart
extractRegion(pluginKitDialogUtilsSource, 'utils-save-callback')
```

The `Future` return is awaited end-to-end; throwing from `onSave` surfaces a SnackBar inside the dialog and leaves the dialog open with the draft intact.

`theme` is a `PluginKitDialogTheme` (a `ThemeExtension`), not a `ThemeData`. The dialog merges it onto the host's existing theme extensions inside the `showDialog` builder so the dialog route picks it up even though it lives under the root navigator. Pass `null` to inherit the host's theme as-is.

`barrierDismissible: true` is the default. Barrier taps still follow this flag while a save is in flight. System-back is blocked while saving.

### `PluginKitDialog`

```dart
extractRegion(pluginKitDialogSource, 'plugin-kit-dialog-plugin-kit-dialog')
```

The Material dialog shell with rounded corners, a `maxWidth` of 920, and a `maxHeight` of 90% of the screen. Use this when you want to drive the dialog yourself: own the controller, decide when to push and pop, intercept cancel for your own confirm flow. The runtime is read off `controller.runtime`.

### `PluginKitDialogBody`

```dart
extractRegion(pluginKitDialogBodySource, 'plugin-kit-dialog-body-plugin-kit-dialog-body')
```

The header-plus-tab-content body without the surrounding `Dialog` chrome. Use this when you want a custom container: a side panel, a route, a dedicated page in a desktop layout. Pass the same runtime as `controller.runtime`; plugin metadata and no-op pruning defaults are sourced from the controller runtime.

### `PluginKitDialogController`

```dart
class PluginKitDialogController extends ChangeNotifier {
  PluginKitDialogController({
    required this.runtime,
    required RuntimeSettings initialSettings,
  });

  final PluginRuntime runtime;
  PluginKitDialogDraft get draft;
  bool get isDirty;
  bool get isSaving;
  set isSaving(bool value);
  bool get showAllServices;
  set showAllServices(bool value);

  void setPluginEnabled(PluginId pluginId, bool enabled);
  void setServiceField({
    required Pin scopedKey,
    required String fieldKey,
    required Object? value,
  });
  void setServiceEnabled(Pin scopedKey, bool enabled);
  void setServicePriority(Pin scopedKey, int? priority);
  void resetField(Pin scopedKey, String fieldKey);
  void resetService(Pin scopedKey);
  void resetPlugin(PluginId pluginId);
  void resetAll();
  void replaceWorking(RuntimeSettings parsed);
  void markSaved();
}
```

`ChangeNotifier`-backed mutable container for the working draft. Construct with a `runtime` and the current `initialSettings`; the controller seeds an internal draft and tracks every mutation against it.

`draft.working` is the current edited `RuntimeSettings`; `draft.active` is the baseline you constructed from. `isDirty` flips true as soon as `working` diverges from `active` and back to false on `markSaved` (which the dialog body invokes automatically after `onSave` resolves successfully).

`isSaving` is owned by the dialog body, which flips it around its `await onSave(...)` so the header can render an inline spinner and the body can dim the tab area. Treat the setter as read-only from app code; it is public only so the body can drive it.

`showAllServices` is a UI-only flag for the Advanced tab JSON preview to show defaults alongside overrides.

The mutators are designed to be safe to call repeatedly. Setting a field to a value that matches the defaults plus baseline collapses the override to nothing (no-op deletion), so saving from a dirty-then-reset draft produces minimal `RuntimeSettings`.
**Mutator key shape:** `setServiceField`, `setServiceEnabled`, `setServicePriority`, `resetField`, and `resetService` all take a `Pin scopedKey`. Build it via `Pin('main_agent', ['agent', 'temperature'])`, `Pin.wildcard(['agent', 'temperature'])`, or the typed-chain helpers; this is the same `Pin` shape `RuntimeSettings.services` is keyed by.

## Visuals (`PluginKitVisualsPlugin`)

Visuals are a Flutter-only concern (icons and colors), so the canonical attachment path is a single locked `GlobalPlugin` that carries host-app overrides keyed by the three axes the dialog renders: plugins, namespace headers, and individual service cards.

### `PluginKitVisualsPlugin`

```dart
class PluginKitVisualsPlugin extends GlobalPlugin {
  static const Namespace pluginVisualNamespace = Namespace('plugin_visual');
  static const Namespace namespaceVisualNamespace = Namespace('namespace_visual');
  static const Namespace serviceVisualNamespace = Namespace('service_visual');
  static const int dialogVisualsAdapterPriority = Priority.elevated; // 1000
  static const id = PluginId('plugin_kit_visuals');

  final Map<PluginId, PluginKitVisual> pluginVisuals;
  final Map<Namespace, PluginKitVisual> namespaceVisuals;
  final Map<ServiceId, PluginKitVisual> serviceVisuals;

  PluginKitVisualsPlugin({
    this.pluginVisuals = const {},
    this.namespaceVisuals = const {},
    this.serviceVisuals = const {},
  });
}
```

Locked `GlobalPlugin` (not user-toggleable) that registers host-app visual overrides. Three independent maps so a Flutter host can decorate plugins owned by Dart-only packages without those packages depending on Flutter.

The plugin registers each visual at `Priority.elevated` (1000), above the default registry priority `Priority.normal` (500), so host overrides beat anything a Flutter plugin self-attaches at the default. When two host plugins decorate the same key, the standard registry priority and registration order rules apply.

Unknown keys are accepted silently. A `PluginId`, `Namespace`, or `ServiceId` that no current plugin registers is retained without warning, so the visual picks up automatically when the matching plugin gets enabled later. Resolution happens at the consumption site (the dialog's chip builder, the services tab card list); the visuals plugin itself never inspects the registry on attach. If you need to detect leftover or typo'd keys, resolve at the consumption site.

### `PluginKitVisual`

```dart
class PluginKitVisual {
  final String? label;
  final String? description;
  final Widget? icon;
  final Color? color;

  const PluginKitVisual({
    this.label,
    this.description,
    this.icon,
    this.color,
  });
}
```

Same value object across all three axes. Every field is optional; the dialog falls back to derived defaults for anything you leave null (raw `pluginId` or namespace name or service id, theme primary color, default namespace or service icon when visuals omit one).

`icon` is a full `Widget`, wrapped by the dialog in an `IconTheme` keyed to the resolved accent so a plain `Icon(Icons.psychology)` inherits color and size automatically. The accent color follows a service-to-namespace-to-owning-plugin-to-theme cascade resolved per render.

```dart
extractRegion(dialogSnippets, 'dialog-visuals-plugin')
```

## Theming (`PluginKitDialogTheme`)

The dialog reads chrome (radii, surfaces, typography) off the host's `ThemeData` and pulls domain-semantic accents off a `PluginKitDialogTheme` extension.

### `PluginKitDialogTheme`

```dart
class PluginKitDialogTheme extends ThemeExtension<PluginKitDialogTheme> {
  final Color stableAccent;
  final Color experimentalAccent;
  final Color agentAccent;
  final Color statActiveBackground;
  final Color statStableBackground;
  final Color statExperimentalBackground;

  const PluginKitDialogTheme({
    required this.stableAccent,
    required this.experimentalAccent,
    required this.agentAccent,
    required this.statActiveBackground,
    required this.statStableBackground,
    required this.statExperimentalBackground,
  });

  static PluginKitDialogTheme dark();
  static PluginKitDialogTheme light();
  static PluginKitDialogTheme of(BuildContext context);

  @override
  PluginKitDialogTheme copyWith({...});

  @override
  PluginKitDialogTheme lerp(...);
}
```

A `ThemeExtension` with six accent slots that Material's `ColorScheme` cannot supply: stable plugin tier, experimental plugin tier, agent-config tier, plus three matching stat-chip background tints. Everything else (badge surfaces, text styles, JSON-preview surfaces) is derived from `Theme.of(context)`.

`PluginKitDialogTheme.dark()` and `.light()` are the canonical defaults. `PluginKitDialogTheme.of(context)` returns the registered extension if one exists, otherwise picks dark or light from `Theme.of(context).brightness`. `copyWith` and `lerp` are the standard `ThemeExtension` overrides.

```dart
extractRegion(dialogSnippets, 'dialog-show-dialog-themed')
```

### `buildPluginKitDialogDarkTheme` and `buildPluginKitDialogLightTheme`

```dart
ThemeData buildPluginKitDialogDarkTheme();
ThemeData buildPluginKitDialogLightTheme();
```

Full Material 3 `ThemeData` builders that match the screenshots in the guide. Use one as your `MaterialApp.theme` if you want the dialog to look the way it does in the demo without hand-tuning your own scheme. Both builders include a `PluginKitDialogTheme.dark()` or `.light()` extension on the returned `ThemeData`, so the dialog accents are wired up automatically.

## Custom renderers (`ConfigFieldRenderer`)

Custom field widgets are resolved from the dialog runtime's internal registry, not from the host runtime you pass into `showPluginKitDialog`. The public dialog widgets build that internal runtime with a fixed plugin list, so renderer plugins registered only on your host runtime are not visible there. The `ConfigFieldRenderer<F>` interface is the contract.

```dart
extractRegion(defaultFieldRenderersPluginSource, 'default-field-renderers-plugin-config-field-renderer')
```

A renderer takes a typed field, the field's current value handle, and a resolver for nested fields (used by group rendering). Build whatever widget you want; write through `handle.value = next` and the dialog tracks the edit on the working draft.

Renderers register under the namespace `'config_field_renderer'` keyed by a string `serviceId` of your choosing. The `ExtensionConfigField.rendererKey` you declare on the field side is the `serviceId` the dialog looks up.

```dart
extractRegion(dialogSnippets, 'dialog-color-picker-renderer')
```

The renderer resolves at render time: registering your plugin after the dialog opens does not affect an in-flight session. If the dialog cannot resolve a `rendererKey`, it surfaces an inline placeholder card naming the missing key rather than throwing during paint.

## Related reading

[Plugin Kit Dialog (guide)](https://plugin-kit.saad-ardati.dev/guides/plugin-kit-dialog/)
  [Capabilities](https://plugin-kit.saad-ardati.dev/concepts/capabilities/)
  [Configuration](https://plugin-kit.saad-ardati.dev/concepts/configuration/)
  [Service Registry & Capabilities (reference)](https://plugin-kit.saad-ardati.dev/reference/service-registry-and-capabilities/)