showSettingsPanel function

void showSettingsPanel(
  1. AppContext ctx
)

Opens the global settings panel — a modal where the user picks the terminal dimensions for new sessions and the terminal text size. Both choices persist immediately (via TerminalDisplayController); dimension changes take effect the next time a session is opened, while text size applies live.

Implementation

void showSettingsPanel(AppContext ctx) {
  final display = ctx.display;
  final device = deviceMetrics();
  final landscape = computeFitDims(device.landscape);
  final portrait = computeFitDims(device.portrait);

  // Custom cols/rows inputs (revealed when the "Custom" preset is selected).
  final cols = input(
    id: 'term-cols',
    type: 'number',
    value: '${display.customCols.value}',
  );
  final rows = input(
    id: 'term-rows',
    type: 'number',
    value: '${display.customRows.value}',
  );
  void commitCustom() {
    final c = int.tryParse(cols.value) ?? display.customCols.value;
    final r = int.tryParse(rows.value) ?? display.customRows.value;
    display.setCustom(c, r);
    // Reflect any clamping back into the inputs.
    cols.value = '${display.customCols.value}';
    rows.value = '${display.customRows.value}';
  }

  on(cols, 'change', (_) => commitCustom());
  on(rows, 'change', (_) => commitCustom());

  final customFields = el(
    'div',
    classes: 'row custom-dims',
    children: [
      field('Columns', cols, hint: '$kMinCols–$kMaxCols'),
      field('Rows', rows, hint: '$kMinRows–$kMaxRows'),
    ],
  );
  void syncCustomEnabled() {
    final enabled = display.preset.value == DimensionPreset.custom;
    customFields.classList.toggle('disabled', !enabled);
    cols.disabled = !enabled;
    rows.disabled = !enabled;
  }

  final dimensions = radioGroup(
    name: 'term-dim',
    ariaLabel: 'Terminal size',
    selected: display.preset.value.name,
    options: [
      (value: DimensionPreset.autoFit.name, label: 'Auto-fit (current screen)'),
      (
        value: DimensionPreset.landscape.name,
        label: 'Fit landscape · ${landscape.cols}×${landscape.rows}',
      ),
      (
        value: DimensionPreset.portrait.name,
        label: 'Fit portrait · ${portrait.cols}×${portrait.rows}',
      ),
      (value: DimensionPreset.standard.name, label: 'Standard · 80×24'),
      (value: DimensionPreset.custom.name, label: 'Custom'),
    ],
    onChange: (value) {
      display.setPreset(DimensionPreset.parse(value));
      syncCustomEnabled();
    },
  );
  syncCustomEnabled();

  final textSize = radioGroup(
    name: 'term-text',
    ariaLabel: 'Text size',
    selected: display.textSize.value.name,
    inline: true,
    options: [
      (value: TerminalTextSize.auto.name, label: 'Auto'),
      (value: TerminalTextSize.smaller.name, label: 'Smaller'),
      (value: TerminalTextSize.normal.name, label: 'Normal'),
      (value: TerminalTextSize.larger.name, label: 'Larger'),
    ],
    onChange: (value) => display.setTextSize(TerminalTextSize.parse(value)),
  );

  // --- AI agent -------------------------------------------------------------
  final ai = ctx.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();

  late final Modal modal;
  final body = el(
    'div',
    classes: 'stack settings-panel',
    children: [
      el('h3', text: 'Terminal size'),
      dimensions,
      customFields,
      el(
        'div',
        classes: 'hint',
        text:
            'Applies to the next session you open — '
            'running sessions keep their size.',
      ),
      el('h3', text: 'Text size'),
      textSize,
      el('div', classes: 'hint', text: 'Applies immediately.'),
      el('hr'),
      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.'),
    ],
  );

  modal = Modal(
    title: 'Settings',
    body: body,
    actions: [button('Close', primary: true, onClick: () => modal.close())],
  );
  modal.show();
}