feat(electrobun): project wizard phases 1-5 (WIP)
- sanitize.ts: input sanitization (trim, control chars, path traversal) - provider-scanner.ts: detect Claude/Codex/Ollama/Gemini availability - model-fetcher.ts: live model lists from 4 provider APIs - ModelConfigPanel.svelte: per-provider config (thinking, effort, sandbox, temperature) - WizardStep1-3.svelte: split wizard into composable steps - CustomDropdown/Checkbox/Radio: themed UI components - provider-handlers.ts: provider.scan + provider.models RPC - Wire providers into wizard step 3 (live detection + model lists) - Replace native selects in 5 settings panels with CustomDropdown
This commit is contained in:
parent
b7fc3a0f9b
commit
d4014a193d
25 changed files with 2112 additions and 759 deletions
154
ui-electrobun/src/mainview/WizardStep3.svelte
Normal file
154
ui-electrobun/src/mainview/WizardStep3.svelte
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
<script lang="ts">
|
||||
import { appRpc } from './rpc.ts';
|
||||
|
||||
interface ProviderInfo {
|
||||
id: string;
|
||||
available: boolean;
|
||||
hasApiKey: boolean;
|
||||
hasCli: boolean;
|
||||
cliPath: string | null;
|
||||
version: string | null;
|
||||
}
|
||||
|
||||
interface ModelInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
provider: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
provider: string;
|
||||
model: string;
|
||||
permissionMode: string;
|
||||
systemPrompt: string;
|
||||
autoStart: boolean;
|
||||
detectedProviders: ProviderInfo[];
|
||||
providerModels: ModelInfo[];
|
||||
modelsLoading: boolean;
|
||||
onUpdate: (field: string, value: unknown) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
provider, model, permissionMode, systemPrompt, autoStart,
|
||||
detectedProviders, providerModels, modelsLoading, 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[]
|
||||
);
|
||||
|
||||
let modelLabel = $derived(model || 'Select model...');
|
||||
|
||||
function providerBadge(p: ProviderInfo): string {
|
||||
const parts: string[] = [];
|
||||
if (p.hasApiKey) parts.push('API Key');
|
||||
if (p.hasCli) parts.push('CLI');
|
||||
if (p.version) parts.push(`v${p.version}`);
|
||||
return parts.join(' \u00b7 ') || '';
|
||||
}
|
||||
|
||||
function defaultPlaceholder(): string {
|
||||
switch (provider) {
|
||||
case 'claude': return 'claude-sonnet-4-20250514';
|
||||
case 'codex': return 'gpt-5.4';
|
||||
case 'ollama': return 'qwen3:8b';
|
||||
case 'gemini': return 'gemini-2.5-flash';
|
||||
default: return '';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<label class="wz-label">AI Provider</label>
|
||||
{#if detectedProviders.length > 0 && availableProviders.length === 0}
|
||||
<div class="wz-hint warn">No providers detected. Install Claude CLI, set OPENAI_API_KEY, or start Ollama.</div>
|
||||
{/if}
|
||||
<div class="wz-provider-grid">
|
||||
{#each availableProviders as p}
|
||||
<button class="wz-provider-card" class:active={provider === p.id}
|
||||
onclick={() => onUpdate('provider', p.id)}>
|
||||
<span class="wz-provider-name">{p.id}</span>
|
||||
{#if 'hasApiKey' in p}
|
||||
<span class="wz-provider-badge">{providerBadge(p as ProviderInfo)}</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<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>
|
||||
{:else}
|
||||
<input class="wz-input" type="text" value={model}
|
||||
oninput={(e) => onUpdate('model', (e.target as HTMLInputElement).value)}
|
||||
placeholder={defaultPlaceholder()} />
|
||||
{#if modelsLoading}<span class="wz-hint" style="color: var(--ctp-blue);">Loading models…</span>{/if}
|
||||
{/if}
|
||||
|
||||
<label class="wz-label">Permission mode</label>
|
||||
<div class="wz-segmented">
|
||||
{#each ['restricted', 'default', 'bypassPermissions'] as pm}
|
||||
<button class="wz-seg-btn" class:active={permissionMode === pm}
|
||||
onclick={() => onUpdate('permissionMode', pm)}>{pm}</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<label class="wz-label">System prompt</label>
|
||||
<textarea class="wz-textarea" value={systemPrompt}
|
||||
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>
|
||||
|
||||
<style>
|
||||
.wz-label { font-size: 0.75rem; font-weight: 500; color: var(--ctp-subtext0); margin-top: 0.25rem; }
|
||||
.wz-input, .wz-textarea { 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); width: 100%; }
|
||||
.wz-input:focus, .wz-textarea:focus { outline: none; border-color: var(--ctp-blue); }
|
||||
.wz-input::placeholder, .wz-textarea::placeholder { color: var(--ctp-overlay0); }
|
||||
.wz-textarea { resize: vertical; min-height: 3rem; }
|
||||
.wz-hint { font-size: 0.6875rem; }
|
||||
.wz-hint.warn { color: var(--ctp-peach); }
|
||||
.wz-provider-grid { display: flex; flex-wrap: wrap; gap: 0.375rem; }
|
||||
.wz-provider-card { display: flex; flex-direction: column; align-items: center; gap: 0.125rem; padding: 0.5rem 0.75rem; border-radius: 0.375rem; border: 1px solid var(--ctp-surface1); background: var(--ctp-surface0); cursor: pointer; min-width: 5rem; }
|
||||
.wz-provider-card:hover { border-color: var(--ctp-overlay1); }
|
||||
.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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue