Skip to content

Plugin Kit Dialog

Plugin Kit Dialog is a Flutter dialog that inspects and edits any PluginRuntime at runtime. Drop it in once and your users get a three-tab UI for toggling plugins, editing service config, and inspecting the registry. You write zero settings-screens-per-plugin code.

The dialog is built from internal plugin_kit plugins. Tabs and field renderers come from the dialog’s internal runtime, so host app plugins cannot shadow or replace them. Header actions (Reset all, Cancel, Save) are built-in widgets.

dependencies:
plugin_kit: ^PUBVER_plugin_kit
plugin_kit_dialog: ^PUBVER_plugin_kit_dialog

plugin_kit carries the dart-only declaration types (UiConfigurableCapability, ConfigField, etc.). plugin_kit_dialog adds the Flutter UI on top.

Future<void> openConfigDialog(
BuildContext context,
PluginRuntime myRuntime,
RuntimeSettings currentSettings,
) async {
final next = await showPluginKitDialog(
context: context,
runtime: myRuntime,
initialSettings: currentSettings,
onSave: (settings) async {
await persistSettings(settings); // write to disk, push to runtime, etc.
},
);
if (next != null) {
// User saved. `next` is the merged RuntimeSettings.
}
}

That is it. If your plugins already attach UiConfigurableCapability, the Services tab populates itself.

onSave is awaited before the dialog closes, so you can persist (write to disk, push to a server, call runtime.updateSettings(...)) without racing the dismissal.

Enable and disable plugins live. Stable plugins toggle freely; experimental plugins are flagged; plugins declaring FeatureFlag.locked cannot be turned off and the runtime logs a configuration error if their dependencies disappear.

Plugin Kit Dialog Plugins tab showing a grid of registered plugins with enable/disable toggles, stable/experimental tiers, and per-plugin icons and colors

Each tile is one plugin. Decorated tiles get their icons, labels, and accent colors from PluginKitVisualsPlugin, a single host-app plugin that maps visuals across the plugin, namespace, and service axes.

Each resolved winner in runtime.sessions.lastOrNull?.registry ?? runtime.globalRegistry that ships a UiConfigurableCapability becomes an editable card. Text, numbers, dropdowns, switches, multiline, password, grouped, or custom fields are all supported.

A registry inspector with priority chains, winners, shadowed contenders, and a JSON view of the working draft. It inspects runtime.globalRegistry, even when sessions are active.

Plugin Kit Dialog Advanced tab showing the service registry inspector with namespaces, competing registrations, priority badges, and the current winner picked out

Each row is a slot. Chips on the right show registrations in priority order, with the current winner picked out and any meta entries (visuals contributions, wildcard fallbacks) marked. The JSON preview shows the current working RuntimeSettings snapshot, not a baseline diff.

Configurability is opt-in per registration. Attach a UiConfigurableCapability next to any service:

void registerConfigurableService(ScopedServiceRegistry registry) {
const agent = Namespace('agent');
registry.registerSingleton<MyService>(
agent('temperature'),
() => const MyService(),
capabilities: const {
UiConfigurableCapability(
label: 'Temperature',
description: 'Controls randomness in responses.',
fields: [
NumberConfigField(
key: 'temperature',
label: 'Temperature',
min: 0,
max: 2,
step: 0.1,
defaultValue: 1.0,
),
],
),
},
);
}

Saved values flow through RuntimeSettings.services[Pin('main_agent', ['agent', 'temperature'])].config (or, if you prefer the dotted shorthand, pluginId.namespace('agent').service('temperature')). From there, Configuration covers how the service reads them via config.getDouble('temperature') and similar typed accessors.

All field types live in plugin_kit (Dart-only):

FieldRenders as
TextConfigFieldSingle-line TextField.
MultilineConfigFieldMultiline editor with optional moustache-tag chips.
PasswordConfigFieldObscured input with show/hide toggle.
NumberConfigFieldSlider when both min and max are set; numeric TextField otherwise. style: NumberFieldStyle.textInput forces text mode. isInteger: true stores int and snaps to whole numbers.
DropdownConfigField<T>Typed dropdown over List<DropdownOption<T>>.
BoolConfigFieldSwitch with label and helper text.
GroupConfigFieldIndented sub-section grouping nested fields under a heading.
ExtensionConfigFieldEscape hatch for custom Flutter renderers. See Custom field renderers below.

Each field carries key, label, helperText, and defaultValue. Dotted keys (provider.api_key) write to nested maps automatically.

Visuals are a Flutter-only concern, so the canonical attachment path is a single locked GlobalPlugin that carries host-app overrides. Three independent maps cover the three things the dialog renders: plugin tiles, namespace section headers, and individual service cards.

void addVisualsPlugin(PluginRuntime runtime, List<Plugin> myPlugins) {
runtime
..addPlugins(myPlugins)
..addPlugin(
PluginKitVisualsPlugin(
pluginVisuals: {
const PluginId('main_agent'): const PluginKitVisual(
label: 'Main Agent',
description: 'The brain. Drives chat, tools, and routing.',
icon: Icon(Icons.psychology),
color: Color(0xFF7C5CFF),
),
},
namespaceVisuals: {
const Namespace('agent'): const PluginKitVisual(
label: 'Agent',
icon: Icon(Icons.smart_toy),
color: Color(0xFF7C5CFF),
),
},
serviceVisuals: {
const Namespace('agent')('temperature'): const PluginKitVisual(
label: 'Temperature',
icon: Icon(Icons.thermostat),
color: Color(0xFFFF9500),
),
},
),
);
}

Because the visuals plugin lives in your host app (which has Flutter), Dart-only plugins still get icons, labels, and colors without importing Flutter. The decoration is keyed by PluginId, Namespace, or ServiceId, so the host owns the map and the plugin source code stays portable.

The plugin registers at priority 1000, above the default registry priority of 500, so host overrides beat anything a Flutter plugin self-attaches from its own register(). Unknown keys (a plugin or service that does not currently exist) are accepted silently; this lets you keep visuals for plugins that may be enabled later. When no visual is found, cards fall back to a generic gear icon and the theme’s primary color.

Need a color picker, file selector, or any other widget? Declare an ExtensionConfigField from anywhere (no Flutter dependency at the field site):

const extensionField = ExtensionConfigField(
key: 'theme.accent',
label: 'Accent color',
rendererKey: 'color_picker',
args: {'allow_alpha': false},
);

The default showPluginKitDialog and PluginKitDialogBody entry points use a private dialog runtime with fixed built-in plugins, so host-runtime renderer registrations are not used.

/// A custom field renderer for color values (Flutter-side).
class ColorPickerRenderer implements ConfigFieldRenderer<ExtensionConfigField> {
/// Creates a [ColorPickerRenderer].
const ColorPickerRenderer();
@override
Widget build(
BuildContext context,
ExtensionConfigField field,
ConfigFieldHandle handle,
FieldRenderResolver resolveRenderer,
) {
final allowAlpha = field.args['allow_alpha'] as bool? ?? false;
return Slider(
value: ((handle.value as int?) ?? 0xFF000000).toDouble(),
min: 0,
max: 0xFFFFFFFF.toDouble(),
onChanged: (next) => handle.value = next.toInt(),
label: allowAlpha ? 'ARGB' : 'RGB',
);
}
}
class ColorPickerRendererPlugin extends GlobalPlugin {
@override
PluginId get pluginId => const PluginId('color_picker_renderer');
@override
void register(ScopedServiceRegistry registry) {
registry.registerFactory<ConfigFieldRenderer>(
FieldRenderersPlugin.namespace('color_picker'),
ColorPickerRenderer.new,
);
}
}

If a renderer key is unknown when the dialog tries to resolve it, an inline placeholder card surfaces the missing key. The dialog never throws at paint time.

Pass a PluginKitDialogTheme to override accents, surfaces, and badges:

/// Demonstrates showPluginKitDialog with a custom [PluginKitDialogTheme].
Future<void> openConfigDialogThemed(
BuildContext context,
PluginRuntime myRuntime,
RuntimeSettings settings,
Future<void> Function(RuntimeSettings) persist,
) async {
await showPluginKitDialog(
context: context,
runtime: myRuntime,
initialSettings: settings,
onSave: persist,
theme: PluginKitDialogTheme.dark().copyWith(
stableAccent: Colors.greenAccent,
experimentalAccent: Colors.deepOrange,
),
);
}

Or wrap your app with buildPluginKitDialogDarkTheme() / buildPluginKitDialogLightTheme() to adopt the full Material 3 ThemeData.

The capability and field types live in plugin_kit, not in this package. That means a non-Flutter package (server-side service, CLI, shared common/ library) can declare configurable services without taking a Flutter dependency. The Flutter UI layers on top through:

  • PluginKitVisualsPlugin (Flutter, host-app side) for icons, labels, and colors across the plugin, namespace, and service axes
  • ExtensionConfigField plus a registered Flutter renderer for custom widgets

That keeps your shared plugin packages portable. The host app owns the Flutter-only glue.

The dialog is non-destructive. Edits accumulate in a working draft; nothing reaches the runtime until the user hits Save. onSave receives the merged RuntimeSettings; persist it however you like. Cancel discards the draft, with a confirm prompt if it is dirty.

Overrides matching the active baseline are pruned automatically, so the resulting RuntimeSettings stays minimal. Two users with different starting points can save the same diff.

A runnable demo with 20 competing plugins (priority towers on agent.model, agent.system_message, retry.policy, search.provider, plus locked and experimental tiers) plus one PluginKitVisualsPlugin decorating every plugin, namespace, and service (21 total runtime plugins) lives at example/plugin_kit_dialog_demo. Open the live web build, or run it locally with flutter run from that directory.

The screenshots above are golden-tested against that demo, so what you see here is exactly what the demo renders.

Future<void> openConfigDialog(
BuildContext context,
PluginRuntime myRuntime,
RuntimeSettings currentSettings,
) async {
final next = await showPluginKitDialog(
context: context,
runtime: myRuntime,
initialSettings: currentSettings,
onSave: (settings) async {
await persistSettings(settings); // write to disk, push to runtime, etc.
},
);
if (next != null) {
// User saved. `next` is the merged RuntimeSettings.
}
}

Declarative types come from plugin_kit:

import 'package:plugin_kit/plugin_kit.dart';
UiConfigurableCapability({label, fields, description});
TextConfigField, MultilineConfigField, PasswordConfigField,
NumberConfigField (NumberFieldStyle, isInteger),
DropdownConfigField<T>, DropdownOption<T>,
BoolConfigField, GroupConfigField,
ExtensionConfigField (rendererKey, args),
ConfigField // sealed base
ConfigFieldHandle // value/reset handle for renderers