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:
parent
d4014a193d
commit
41b8d46a19
6 changed files with 210 additions and 247 deletions
|
|
@ -1,7 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { appRpc } from './rpc.ts';
|
||||
import { sanitize, sanitizePath, sanitizeUrl, isValidGitUrl, isValidGithubRepo } from './sanitize.ts';
|
||||
import { getProjects } from './workspace-store.svelte.ts';
|
||||
import WizardStep1 from './WizardStep1.svelte';
|
||||
import WizardStep2 from './WizardStep2.svelte';
|
||||
import WizardStep3 from './WizardStep3.svelte';
|
||||
|
|
@ -10,6 +9,7 @@
|
|||
id: string; name: string; cwd: string; provider?: string; model?: string;
|
||||
systemPrompt?: string; autoStart?: boolean; groupId?: string;
|
||||
useWorktrees?: boolean; shell?: string; icon?: string; color?: string;
|
||||
modelConfig?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
|
|
@ -17,68 +17,35 @@
|
|||
onCreated: (project: ProjectResult) => void;
|
||||
groupId: string;
|
||||
groups: Array<{ id: string; name: string }>;
|
||||
existingNames: string[];
|
||||
}
|
||||
|
||||
let { onClose, onCreated, groupId, groups }: Props = $props();
|
||||
let { onClose, onCreated, groupId, groups, existingNames }: Props = $props();
|
||||
|
||||
// ── Wizard step ─────────────────────────────────────────────
|
||||
type SourceType = 'local' | 'git-clone' | 'github' | 'template' | 'remote';
|
||||
type AuthMethod = 'password' | 'key' | 'agent' | 'config';
|
||||
|
||||
let step = $state(1);
|
||||
|
||||
// ── Step 1 state ────────────────────────────────────────────
|
||||
let sourceType = $state<SourceType>('local');
|
||||
let localPath = $state('');
|
||||
let repoUrl = $state('');
|
||||
let cloneTarget = $state('');
|
||||
let githubRepo = $state('');
|
||||
let selectedTemplate = $state('');
|
||||
let templateTargetDir = $state('~/projects');
|
||||
let remoteHost = $state('');
|
||||
let remoteUser = $state('');
|
||||
let remotePath = $state('');
|
||||
let remoteAuthMethod = $state<AuthMethod>('agent');
|
||||
let remotePassword = $state('');
|
||||
let remoteKeyPath = $state('~/.ssh/id_ed25519');
|
||||
let remoteSshfs = $state(false);
|
||||
let localPath = $state(''); let repoUrl = $state(''); let cloneTarget = $state('');
|
||||
let githubRepo = $state(''); let selectedTemplate = $state(''); let templateTargetDir = $state('~/projects');
|
||||
let remoteHost = $state(''); let remoteUser = $state(''); let remotePath = $state('');
|
||||
let remoteAuthMethod = $state<AuthMethod>('agent'); let remotePassword = $state(''); let remoteKeyPath = $state('~/.ssh/id_ed25519');
|
||||
let remoteSshfs = $state(false); let isGitRepo = $state(false); let gitBranch = $state('');
|
||||
let pathValid = $state<'idle' | 'checking' | 'valid' | 'invalid' | 'not-dir'>('idle');
|
||||
let isGitRepo = $state(false);
|
||||
let gitBranch = $state('');
|
||||
let gitProbeStatus = $state<'idle' | 'probing' | 'ok' | 'error'>('idle');
|
||||
let gitProbeBranches = $state<string[]>([]);
|
||||
let gitProbeBranches = $state<string[]>([]); let githubLoading = $state(false); let cloning = $state(false);
|
||||
let githubInfo = $state<{ stars: number; description: string; defaultBranch: string } | null>(null);
|
||||
let githubLoading = $state(false);
|
||||
let cloning = $state(false);
|
||||
let templates = $state<Array<{ id: string; name: string; description: string; icon: string }>>([]);
|
||||
|
||||
// ── Step 2 state ────────────────────────────────────────────
|
||||
let projectName = $state('');
|
||||
let nameError = $state('');
|
||||
let selectedBranch = $state('');
|
||||
let branches = $state<string[]>([]);
|
||||
let useWorktrees = $state(false);
|
||||
let selectedGroupId = $state(groupId);
|
||||
let projectIcon = $state('Terminal');
|
||||
let projectColor = $state('var(--ctp-blue)');
|
||||
let shellChoice = $state('bash');
|
||||
|
||||
// ── Step 3 state ────────────────────────────────────────────
|
||||
let provider = $state<string>('claude');
|
||||
let model = $state('');
|
||||
let permissionMode = $state('default');
|
||||
let systemPrompt = $state('');
|
||||
let autoStart = $state(false);
|
||||
let detectedProviders = $state<Array<{ id: string; available: boolean; hasApiKey: boolean; hasCli: boolean; cliPath: string | null; version: string | null }>>([]);
|
||||
let providerModels = $state<Array<{ id: string; name: string; provider: string }>>([]);
|
||||
let modelsLoading = $state(false);
|
||||
|
||||
// ── Debounce timers ─────────────────────────────────────────
|
||||
let projectName = $state(''); let nameError = $state(''); let selectedBranch = $state('');
|
||||
let branches = $state<string[]>([]); let useWorktrees = $state(false); let selectedGroupId = $state(groupId);
|
||||
let projectIcon = $state('Terminal'); let projectColor = $state('var(--ctp-blue)'); let shellChoice = $state('bash');
|
||||
let provider = $state<string>('claude'); let model = $state(''); let permissionMode = $state('default');
|
||||
let systemPrompt = $state(''); let autoStart = $state(false); let modelConfig = $state<Record<string, unknown>>({});
|
||||
type ProviderInfo = { id: string; available: boolean; hasApiKey: boolean; hasCli: boolean; cliPath: string | null; version: string | null };
|
||||
let detectedProviders = $state<ProviderInfo[]>([]); let providerModels = $state<Array<{ id: string; name: string; provider: string }>>([]); let modelsLoading = $state(false);
|
||||
let pathTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let probeTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let githubTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
// ── Templates loading ───────────────────────────────────────
|
||||
$effect(() => {
|
||||
if (sourceType === 'template' && templates.length === 0) {
|
||||
appRpc.request['project.templates']({}).then(r => {
|
||||
|
|
@ -86,8 +53,6 @@
|
|||
}).catch(console.error);
|
||||
}
|
||||
});
|
||||
|
||||
// ── Path validation (debounced) ─────────────────────────────
|
||||
$effect(() => {
|
||||
if (sourceType === 'local') {
|
||||
if (pathTimer) clearTimeout(pathTimer);
|
||||
|
|
@ -103,8 +68,6 @@
|
|||
}, 300);
|
||||
}
|
||||
});
|
||||
|
||||
// ── Git probe (debounced) ───────────────────────────────────
|
||||
$effect(() => {
|
||||
if (sourceType === 'git-clone' && repoUrl.trim()) {
|
||||
if (probeTimer) clearTimeout(probeTimer);
|
||||
|
|
@ -118,8 +81,6 @@
|
|||
}, 600);
|
||||
} else if (sourceType === 'git-clone') { gitProbeStatus = 'idle'; }
|
||||
});
|
||||
|
||||
// ── GitHub validation (debounced) ───────────────────────────
|
||||
$effect(() => {
|
||||
if (sourceType === 'github' && githubRepo.trim()) {
|
||||
if (githubTimer) clearTimeout(githubTimer);
|
||||
|
|
@ -138,16 +99,13 @@
|
|||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// ── Unique name validation ──────────────────────────────────
|
||||
$effect(() => {
|
||||
const trimmed = projectName.trim().toLowerCase();
|
||||
if (!trimmed) { nameError = ''; return; }
|
||||
const existing = getProjects().map(p => p.name.toLowerCase());
|
||||
nameError = existing.includes(trimmed) ? 'A project with this name already exists' : '';
|
||||
const lowerNames = existingNames.map(n => n.toLowerCase());
|
||||
nameError = lowerNames.includes(trimmed) ? 'A project with this name already exists' : '';
|
||||
});
|
||||
|
||||
// ── Provider scan on step 3 entry ───────────────────────────
|
||||
$effect(() => {
|
||||
if (step === 3 && detectedProviders.length === 0) {
|
||||
appRpc.request['provider.scan']({}).then(r => {
|
||||
|
|
@ -156,7 +114,6 @@
|
|||
}
|
||||
});
|
||||
|
||||
// ── Model list on provider change ───────────────────────────
|
||||
$effect(() => {
|
||||
if (step === 3 && provider) {
|
||||
providerModels = []; modelsLoading = true;
|
||||
|
|
@ -166,7 +123,6 @@
|
|||
}
|
||||
});
|
||||
|
||||
// ── Step 1 validation ──────────────────────────────────────
|
||||
let step1Valid = $derived(() => {
|
||||
switch (sourceType) {
|
||||
case 'local': return pathValid === 'valid';
|
||||
|
|
@ -178,7 +134,6 @@
|
|||
}
|
||||
});
|
||||
|
||||
// ── Step navigation ────────────────────────────────────────
|
||||
async function goToStep2() {
|
||||
if (sourceType === 'git-clone') {
|
||||
const url = sanitizeUrl(repoUrl); if (!url) return;
|
||||
|
|
@ -228,6 +183,7 @@
|
|||
provider: provider as string, model: model || undefined, systemPrompt: systemPrompt || undefined,
|
||||
autoStart, groupId: selectedGroupId, useWorktrees: useWorktrees || undefined,
|
||||
shell: shellChoice, icon: projectIcon, color: projectColor,
|
||||
modelConfig: Object.keys(modelConfig).length > 0 ? modelConfig : undefined,
|
||||
};
|
||||
onCreated(project);
|
||||
resetState();
|
||||
|
|
@ -246,41 +202,27 @@
|
|||
branches = []; useWorktrees = false; selectedGroupId = groupId;
|
||||
projectIcon = 'Terminal'; projectColor = 'var(--ctp-blue)'; shellChoice = 'bash';
|
||||
provider = 'claude'; model = ''; permissionMode = 'default';
|
||||
systemPrompt = ''; autoStart = false; providerModels = []; modelsLoading = false;
|
||||
systemPrompt = ''; autoStart = false; providerModels = []; modelsLoading = false; modelConfig = {};
|
||||
}
|
||||
|
||||
// ── Field updater (passed to sub-components) ────────────────
|
||||
function handleUpdate(field: string, value: unknown) {
|
||||
const v = value as string & boolean;
|
||||
switch (field) {
|
||||
case 'sourceType': sourceType = v as SourceType; break;
|
||||
case 'localPath': localPath = v; break;
|
||||
case 'repoUrl': repoUrl = v; break;
|
||||
case 'cloneTarget': cloneTarget = v; break;
|
||||
case 'githubRepo': githubRepo = v; break;
|
||||
case 'selectedTemplate': selectedTemplate = v; break;
|
||||
case 'templateTargetDir': templateTargetDir = v; break;
|
||||
case 'remoteHost': remoteHost = v; break;
|
||||
case 'remoteUser': remoteUser = v; break;
|
||||
case 'remotePath': remotePath = v; break;
|
||||
case 'remoteAuthMethod': remoteAuthMethod = v as AuthMethod; break;
|
||||
case 'remotePassword': remotePassword = v; break;
|
||||
case 'remoteKeyPath': remoteKeyPath = v; break;
|
||||
case 'remoteSshfs': remoteSshfs = v as unknown as boolean; break;
|
||||
case 'projectName': projectName = v; break;
|
||||
case 'selectedBranch': selectedBranch = v; break;
|
||||
case 'useWorktrees': useWorktrees = v as unknown as boolean; break;
|
||||
case 'selectedGroupId': selectedGroupId = v; break;
|
||||
case 'projectIcon': projectIcon = v; break;
|
||||
case 'projectColor': projectColor = v; break;
|
||||
case 'shellChoice': shellChoice = v; break;
|
||||
case 'provider': provider = v; break;
|
||||
case 'model': model = v; break;
|
||||
case 'permissionMode': permissionMode = v; break;
|
||||
case 'systemPrompt': systemPrompt = v; break;
|
||||
case 'autoStart': autoStart = v as unknown as boolean; break;
|
||||
}
|
||||
}
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
const SETTERS: Record<string, (v: any) => void> = {
|
||||
sourceType: v => sourceType = v, localPath: v => localPath = v,
|
||||
repoUrl: v => repoUrl = v, cloneTarget: v => cloneTarget = v,
|
||||
githubRepo: v => githubRepo = v, selectedTemplate: v => selectedTemplate = v,
|
||||
templateTargetDir: v => templateTargetDir = v, remoteHost: v => remoteHost = v,
|
||||
remoteUser: v => remoteUser = v, remotePath: v => remotePath = v,
|
||||
remoteAuthMethod: v => remoteAuthMethod = v, remotePassword: v => remotePassword = v,
|
||||
remoteKeyPath: v => remoteKeyPath = v, remoteSshfs: v => remoteSshfs = v,
|
||||
projectName: v => projectName = v, selectedBranch: v => selectedBranch = v,
|
||||
useWorktrees: v => useWorktrees = v, selectedGroupId: v => selectedGroupId = v,
|
||||
projectIcon: v => projectIcon = v, projectColor: v => projectColor = v,
|
||||
shellChoice: v => shellChoice = v, provider: v => provider = v,
|
||||
model: v => model = v, permissionMode: v => permissionMode = v,
|
||||
systemPrompt: v => systemPrompt = v, autoStart: v => autoStart = v,
|
||||
modelConfig: v => modelConfig = v,
|
||||
};
|
||||
function handleUpdate(field: string, value: unknown) { SETTERS[field]?.(value); }
|
||||
</script>
|
||||
|
||||
<div class="wizard-overlay" role="dialog" aria-label="New Project">
|
||||
|
|
@ -318,7 +260,7 @@
|
|||
|
||||
<div class="wz-body" style:display={step === 3 ? 'flex' : 'none'}>
|
||||
<h3 class="wz-step-title">Agent</h3>
|
||||
<WizardStep3 {provider} {model} {permissionMode} {systemPrompt} {autoStart} {detectedProviders} {providerModels} {modelsLoading} onUpdate={handleUpdate} />
|
||||
<WizardStep3 {provider} {model} {permissionMode} {systemPrompt} {autoStart} {detectedProviders} {providerModels} {modelsLoading} {modelConfig} onUpdate={handleUpdate} />
|
||||
<div class="wz-footer">
|
||||
<button class="wz-btn secondary" onclick={goBack}>Back</button>
|
||||
<div class="wz-footer-right">
|
||||
|
|
@ -354,4 +296,5 @@
|
|||
.wz-btn.secondary:hover { background: var(--ctp-surface0); color: var(--ctp-text); }
|
||||
.wz-btn.ghost { background: transparent; border: none; color: var(--ctp-subtext0); }
|
||||
.wz-btn.ghost:hover { color: var(--ctp-text); }
|
||||
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue