feat(electrobun): complete project wizard phases 4-5

- ModelConfigPanel: Claude thinking/effort, Codex sandbox/approval, Ollama
  temp/ctx/predict, Gemini thinkingLevel/budget (mutually exclusive)
- CustomDropdown: themed with keyboard nav, groupBy, display:none pattern
- CustomCheckbox/CustomRadio: themed with Lucide icons
- Replaced native selects in 5 settings panels + wizard steps
- wizard-state.ts: shared type definitions
- Gemini added as 4th provider throughout
This commit is contained in:
Hibryda 2026-03-23 13:12:47 +01:00
parent d4014a193d
commit 41b8d46a19
6 changed files with 210 additions and 247 deletions

View file

@ -1,5 +1,9 @@
<script lang="ts">
import { appRpc } from './rpc.ts';
import CustomDropdown from './ui/CustomDropdown.svelte';
import CustomCheckbox from './ui/CustomCheckbox.svelte';
import ModelConfigPanel from './ModelConfigPanel.svelte';
import type { ProviderId } from './provider-capabilities';
interface ProviderInfo {
id: string;
@ -25,23 +29,24 @@
detectedProviders: ProviderInfo[];
providerModels: ModelInfo[];
modelsLoading: boolean;
modelConfig: Record<string, unknown>;
onUpdate: (field: string, value: unknown) => void;
}
let {
provider, model, permissionMode, systemPrompt, autoStart,
detectedProviders, providerModels, modelsLoading, onUpdate,
detectedProviders, providerModels, modelsLoading, modelConfig, onUpdate,
}: Props = $props();
let modelDDOpen = $state(false);
let availableProviders = $derived(
detectedProviders.length > 0
? detectedProviders.filter(p => p.available)
: [{ id: 'claude' }, { id: 'codex' }, { id: 'ollama' }] as ProviderInfo[]
: [{ id: 'claude' }, { id: 'codex' }, { id: 'ollama' }, { id: 'gemini' }] as ProviderInfo[]
);
let modelLabel = $derived(model || 'Select model...');
let modelItems = $derived(
providerModels.map(m => ({ value: m.id, label: m.name || m.id }))
);
function providerBadge(p: ProviderInfo): string {
const parts: string[] = [];
@ -69,7 +74,7 @@
<div class="wz-provider-grid">
{#each availableProviders as p}
<button class="wz-provider-card" class:active={provider === p.id}
onclick={() => onUpdate('provider', p.id)}>
onclick={() => { onUpdate('provider', p.id); onUpdate('modelConfig', {}); }}>
<span class="wz-provider-name">{p.id}</span>
{#if 'hasApiKey' in p}
<span class="wz-provider-badge">{providerBadge(p as ProviderInfo)}</span>
@ -80,20 +85,12 @@
<label class="wz-label">Model</label>
{#if providerModels.length > 0}
<div class="wz-dd" style="position:relative;">
<button class="wz-dd-btn" onclick={() => modelDDOpen = !modelDDOpen}>
<span>{modelLabel}</span>
<span class="wz-dd-chev">{modelsLoading ? '\u2026' : '\u25BE'}</span>
</button>
<div class="wz-dd-menu" style:display={modelDDOpen ? 'block' : 'none'}>
{#each providerModels as m}
<button class="wz-dd-item" class:active={model === m.id}
onclick={() => { onUpdate('model', m.id); modelDDOpen = false; }}>
{m.name || m.id}
</button>
{/each}
</div>
</div>
<CustomDropdown
items={modelItems}
selected={model}
placeholder={modelsLoading ? 'Loading...' : 'Select model...'}
onSelect={v => onUpdate('model', v)}
/>
{:else}
<input class="wz-input" type="text" value={model}
oninput={(e) => onUpdate('model', (e.target as HTMLInputElement).value)}
@ -101,6 +98,14 @@
{#if modelsLoading}<span class="wz-hint" style="color: var(--ctp-blue);">Loading models&hellip;</span>{/if}
{/if}
<!-- Per-model configuration -->
<ModelConfigPanel
provider={provider as ProviderId}
{model}
config={modelConfig}
onChange={c => onUpdate('modelConfig', c)}
/>
<label class="wz-label">Permission mode</label>
<div class="wz-segmented">
{#each ['restricted', 'default', 'bypassPermissions'] as pm}
@ -114,11 +119,11 @@
oninput={(e) => onUpdate('systemPrompt', (e.target as HTMLTextAreaElement).value)}
rows="3" placeholder="Optional system instructions..."></textarea>
<label class="wz-toggle-row">
<input type="checkbox" checked={autoStart}
onchange={(e) => onUpdate('autoStart', (e.target as HTMLInputElement).checked)} />
<span>Auto-start agent on create</span>
</label>
<CustomCheckbox
checked={autoStart}
label="Auto-start agent on create"
onChange={v => onUpdate('autoStart', v)}
/>
<style>
.wz-label { font-size: 0.75rem; font-weight: 500; color: var(--ctp-subtext0); margin-top: 0.25rem; }
@ -134,21 +139,9 @@
.wz-provider-card.active { border-color: var(--ctp-blue); background: color-mix(in srgb, var(--ctp-blue) 10%, transparent); }
.wz-provider-name { font-size: 0.8125rem; font-weight: 600; color: var(--ctp-text); text-transform: capitalize; }
.wz-provider-badge { font-size: 0.5625rem; color: var(--ctp-subtext0); }
.wz-dd { width: 100%; }
.wz-dd-btn { width: 100%; display: flex; align-items: center; justify-content: space-between; padding: 0.375rem 0.5rem; background: var(--ctp-surface0); border: 1px solid var(--ctp-surface1); border-radius: 0.25rem; color: var(--ctp-text); font-size: 0.8125rem; font-family: var(--ui-font-family); cursor: pointer; text-align: left; }
.wz-dd-btn:hover { border-color: var(--ctp-surface2); }
.wz-dd-chev { color: var(--ctp-overlay1); font-size: 0.75rem; }
.wz-dd-menu { position: absolute; top: 100%; left: 0; right: 0; z-index: 20; background: var(--ctp-mantle); border: 1px solid var(--ctp-surface1); border-radius: 0.25rem; margin-top: 0.125rem; max-height: 12rem; overflow-y: auto; box-shadow: 0 0.25rem 0.75rem color-mix(in srgb, var(--ctp-crust) 50%, transparent); }
.wz-dd-item { width: 100%; padding: 0.3125rem 0.5rem; background: none; border: none; color: var(--ctp-text); font-size: 0.8125rem; font-family: var(--ui-font-family); cursor: pointer; text-align: left; }
.wz-dd-item:hover { background: var(--ctp-surface0); }
.wz-dd-item.active { color: var(--ctp-blue); background: color-mix(in srgb, var(--ctp-blue) 10%, transparent); }
.wz-segmented { display: flex; gap: 0; border-radius: 0.25rem; overflow: hidden; border: 1px solid var(--ctp-surface1); }
.wz-seg-btn { flex: 1; padding: 0.375rem 0.5rem; font-size: 0.75rem; background: var(--ctp-surface0); border: none; color: var(--ctp-subtext0); cursor: pointer; font-family: var(--ui-font-family); border-right: 1px solid var(--ctp-surface1); }
.wz-seg-btn:last-child { border-right: none; }
.wz-seg-btn:hover { color: var(--ctp-text); }
.wz-seg-btn.active { background: color-mix(in srgb, var(--ctp-blue) 20%, transparent); color: var(--ctp-blue); }
.wz-toggle-row { display: flex; align-items: center; gap: 0.5rem; font-size: 0.8125rem; color: var(--ctp-text); cursor: pointer; margin-top: 0.25rem; }
input[type="checkbox"] { -webkit-appearance: none; appearance: none; width: 1rem; height: 1rem; border: 1px solid var(--ctp-surface2); border-radius: 0.1875rem; background: var(--ctp-surface0); cursor: pointer; position: relative; vertical-align: middle; flex-shrink: 0; }
input[type="checkbox"]:checked { background: var(--ctp-blue); border-color: var(--ctp-blue); }
input[type="checkbox"]:checked::after { content: ''; position: absolute; left: 0.25rem; top: 0.0625rem; width: 0.3125rem; height: 0.5625rem; border: solid var(--ctp-base); border-width: 0 2px 2px 0; transform: rotate(45deg); }
</style>