aiSettingsSection function

List<HTMLElement> aiSettingsSection(
  1. AiSettingsController ai
)

The AI-agent settings controls, bound to an AiSettingsController.

This is the portable half of the settings dialog: it needs no AppContext, only the controller (which itself wraps the settings store and the shell service), so any app that offers the :ai command can drop these rows into its own settings modal. Returns the section's elements in order, ready to place under the rest of a settings body.

Bindings persist on change through the controller; the "use Hub default" checkbox enables or disables the custom provider/model/key fields, and the Hub's advertised default (if any) is fetched and shown as a hint.

Implementation

List<web.HTMLElement> aiSettingsSection(AiSettingsController ai) {
  final providerSelect =
      el('select', id: 'ai-provider') as web.HTMLSelectElement;
  for (final p in const ['anthropic', 'openai', 'gemini']) {
    final opt = el('option', text: p) as web.HTMLOptionElement;
    opt.value = p;
    opt.selected = p == ai.provider;
    providerSelect.appendChild(opt);
  }
  on(providerSelect, 'change', (_) => ai.provider = providerSelect.value);

  final aiModel = input(
    id: 'ai-model',
    value: ai.model,
    placeholder: 'provider default',
  );
  on(aiModel, 'change', (_) => ai.model = aiModel.value);

  final aiKey = input(
    id: 'ai-key',
    type: 'password',
    value: ai.apiKey,
    placeholder: 'sk-…',
    autocomplete: 'off',
  );
  on(aiKey, 'change', (_) => ai.apiKey = aiKey.value);

  final aiCustom = el(
    'div',
    classes: 'stack ai-custom',
    children: [
      field('Provider', providerSelect),
      field('Model', aiModel, hint: 'Leave blank for the provider default.'),
      field(
        'Your API key',
        aiKey,
        hint: 'Stored in this browser only; sent via the Hub to the provider.',
      ),
    ],
  );

  void syncAiEnabled() {
    final custom = !ai.useHubDefault;
    aiCustom.classList.toggle('disabled', !custom);
    providerSelect.disabled = !custom;
    aiModel.disabled = !custom;
    aiKey.disabled = !custom;
  }

  final hubHint = el('div', classes: 'hint', text: 'Checking the Hub default…');
  unawaited(
    ai.hubDefault().then((cfg) {
      hubHint.textContent = (cfg == null || !cfg.available)
          ? 'The Hub has no default AI provider — add your own key below.'
          : 'Hub default: ${cfg.provider} / ${cfg.model ?? 'model default'} '
                '(the key stays on the Hub).';
    }),
  );

  final useHub = checkbox(
    "Use the Hub's default AI provider",
    id: 'ai-use-hub',
    checked: ai.useHubDefault,
  );
  on(useHub.box, 'change', (_) {
    ai.useHubDefault = useHub.box.checked;
    syncAiEnabled();
  });

  final aiMode = radioGroup(
    name: 'ai-mode',
    ariaLabel: 'Agent mode',
    inline: true,
    selected: ai.mode,
    options: const [
      (value: 'standard', label: 'Standard'),
      (value: 'plan', label: 'Plan'),
      (value: 'auto', label: 'Auto'),
    ],
    onChange: (value) => ai.mode = value,
  );

  final aiLang = input(
    id: 'ai-lang',
    value: ai.language,
    placeholder: 'model default',
  );
  on(aiLang, 'change', (_) => ai.language = aiLang.value);

  syncAiEnabled();

  return [
    el('h3', text: 'AI agent'),
    useHub.root,
    hubHint,
    aiCustom,
    el('div', classes: 'hint', text: 'Default mode'),
    aiMode,
    field(
      'Reply language',
      aiLang,
      hint: 'e.g. english, portuguese — blank for the model default.',
    ),
    el('div', classes: 'hint', text: 'Applies to the next session you open.'),
  ];
}