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
|
|
@ -13,33 +13,53 @@
|
|||
let { provider, model, config, onChange }: Props = $props();
|
||||
|
||||
// ── Claude config ──────────────────────────────────────
|
||||
let claudeThinking = $state<'disabled' | 'enabled' | 'adaptive'>((config.thinking as string) ?? 'disabled');
|
||||
let claudeEffort = $state((config.effort as string) ?? 'medium');
|
||||
let claudeTemp = $state<number>((config.temperature as number) ?? 1.0);
|
||||
let claudeMaxTokens = $state<number>((config.max_tokens as number) ?? 8192);
|
||||
|
||||
// Temperature is locked to 1.0 when thinking=enabled
|
||||
let claudeThinking = $state<'disabled' | 'enabled' | 'adaptive'>('disabled');
|
||||
let claudeEffort = $state('medium');
|
||||
let claudeTemp = $state<number>(1.0);
|
||||
let claudeMaxTokens = $state<number>(8192);
|
||||
let claudeTempLocked = $derived(claudeThinking === 'enabled');
|
||||
|
||||
// ── Codex config ───────────────────────────────────────
|
||||
let codexSandbox = $state((config.sandbox as string) ?? 'workspace-write');
|
||||
let codexApproval = $state((config.approval as string) ?? 'on-request');
|
||||
let codexReasoning = $state((config.reasoning as string) ?? 'medium');
|
||||
let codexSandbox = $state('workspace-write');
|
||||
let codexApproval = $state('on-request');
|
||||
let codexReasoning = $state('medium');
|
||||
|
||||
// ── Ollama config ──────────────────────────────────────
|
||||
let ollamaTemp = $state<number>((config.temperature as number) ?? 0.8);
|
||||
let ollamaCtx = $state<number>((config.num_ctx as number) ?? 32768);
|
||||
let ollamaPredict = $state<number>((config.num_predict as number) ?? 0);
|
||||
let ollamaTopK = $state<number>((config.top_k as number) ?? 40);
|
||||
let ollamaTopP = $state<number>((config.top_p as number) ?? 0.9);
|
||||
let ollamaTemp = $state<number>(0.8);
|
||||
let ollamaCtx = $state<number>(32768);
|
||||
let ollamaPredict = $state<number>(0);
|
||||
let ollamaTopK = $state<number>(40);
|
||||
let ollamaTopP = $state<number>(0.9);
|
||||
let ollamaCtxWarn = $derived(ollamaCtx < 8192);
|
||||
|
||||
// ── Gemini config ──────────────────────────────────────
|
||||
let geminiTemp = $state<number>((config.temperature as number) ?? 1.0);
|
||||
let geminiThinkingMode = $state<'level' | 'budget'>((config.thinkingMode as string as 'level' | 'budget') ?? 'level');
|
||||
let geminiThinkingLevel = $state((config.thinkingLevel as string) ?? 'medium');
|
||||
let geminiThinkingBudget = $state<number>((config.thinkingBudget as number) ?? 8192);
|
||||
let geminiMaxOutput = $state<number>((config.maxOutputTokens as number) ?? 8192);
|
||||
let geminiTemp = $state<number>(1.0);
|
||||
let geminiThinkingMode = $state<'level' | 'budget'>('level');
|
||||
let geminiThinkingLevel = $state('medium');
|
||||
let geminiThinkingBudget = $state<number>(8192);
|
||||
let geminiMaxOutput = $state<number>(8192);
|
||||
|
||||
// Sync local state from config prop (re-runs when config changes)
|
||||
$effect(() => {
|
||||
const c = config;
|
||||
claudeThinking = (c.thinking as string as typeof claudeThinking) ?? 'disabled';
|
||||
claudeEffort = (c.effort as string) ?? 'medium';
|
||||
claudeTemp = (c.temperature as number) ?? 1.0;
|
||||
claudeMaxTokens = (c.max_tokens as number) ?? 8192;
|
||||
codexSandbox = (c.sandbox as string) ?? 'workspace-write';
|
||||
codexApproval = (c.approval as string) ?? 'on-request';
|
||||
codexReasoning = (c.reasoning as string) ?? 'medium';
|
||||
ollamaTemp = (c.temperature as number) ?? 0.8;
|
||||
ollamaCtx = (c.num_ctx as number) ?? 32768;
|
||||
ollamaPredict = (c.num_predict as number) ?? 0;
|
||||
ollamaTopK = (c.top_k as number) ?? 40;
|
||||
ollamaTopP = (c.top_p as number) ?? 0.9;
|
||||
geminiTemp = (c.temperature as number) ?? 1.0;
|
||||
geminiThinkingMode = (c.thinkingMode as string as 'level' | 'budget') ?? 'level';
|
||||
geminiThinkingLevel = (c.thinkingLevel as string) ?? 'medium';
|
||||
geminiThinkingBudget = (c.thinkingBudget as number) ?? 8192;
|
||||
geminiMaxOutput = (c.maxOutputTokens as number) ?? 8192;
|
||||
});
|
||||
|
||||
// ── Emit changes ───────────────────────────────────────
|
||||
function emitClaude() {
|
||||
|
|
@ -285,48 +305,20 @@
|
|||
|
||||
<style>
|
||||
.mcp-root { display: flex; flex-direction: column; gap: 0.5rem; margin-top: 0.375rem; }
|
||||
|
||||
.mcp-field { display: flex; flex-direction: column; gap: 0.25rem; }
|
||||
|
||||
.mcp-label {
|
||||
font-size: 0.75rem; font-weight: 500; color: var(--ctp-subtext0);
|
||||
display: flex; align-items: center; gap: 0.375rem;
|
||||
}
|
||||
|
||||
.mcp-hint {
|
||||
font-size: 0.6875rem; color: var(--ctp-overlay0); font-weight: 400; font-style: italic;
|
||||
}
|
||||
|
||||
.mcp-warn {
|
||||
font-size: 0.6875rem; color: var(--ctp-peach); font-weight: 400;
|
||||
}
|
||||
|
||||
.mcp-input {
|
||||
padding: 0.375rem 0.5rem; width: 100%;
|
||||
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);
|
||||
appearance: textfield; -moz-appearance: textfield;
|
||||
}
|
||||
.mcp-input::-webkit-inner-spin-button,
|
||||
.mcp-input::-webkit-outer-spin-button { -webkit-appearance: none; appearance: none; }
|
||||
.mcp-label { font-size: 0.75rem; font-weight: 500; color: var(--ctp-subtext0); display: flex; align-items: center; gap: 0.375rem; }
|
||||
.mcp-hint { font-size: 0.6875rem; color: var(--ctp-overlay0); font-weight: 400; font-style: italic; }
|
||||
.mcp-warn { font-size: 0.6875rem; color: var(--ctp-peach); font-weight: 400; }
|
||||
.mcp-input { padding: 0.375rem 0.5rem; width: 100%; 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); appearance: textfield; -moz-appearance: textfield; }
|
||||
.mcp-input::-webkit-inner-spin-button, .mcp-input::-webkit-outer-spin-button { -webkit-appearance: none; appearance: none; }
|
||||
.mcp-input:focus { outline: none; border-color: var(--ctp-blue); }
|
||||
|
||||
.mcp-slider-row { display: flex; align-items: center; gap: 0.5rem; }
|
||||
.mcp-slider { flex: 1; accent-color: var(--ctp-blue); }
|
||||
.mcp-slider:disabled { opacity: 0.4; }
|
||||
.mcp-slider-val { font-size: 0.8125rem; color: var(--ctp-text); min-width: 2.5rem; text-align: right; }
|
||||
|
||||
.mcp-seg { display: flex; border-radius: 0.25rem; overflow: hidden; border: 1px solid var(--ctp-surface1); }
|
||||
.mcp-seg-btn {
|
||||
flex: 1; padding: 0.25rem 0.375rem; background: var(--ctp-surface0); border: none;
|
||||
color: var(--ctp-overlay1); font-size: 0.6875rem; cursor: pointer;
|
||||
font-family: var(--ui-font-family);
|
||||
border-right: 1px solid var(--ctp-surface1);
|
||||
}
|
||||
.mcp-seg-btn { flex: 1; padding: 0.25rem 0.375rem; background: var(--ctp-surface0); border: none; color: var(--ctp-overlay1); font-size: 0.6875rem; cursor: pointer; font-family: var(--ui-font-family); border-right: 1px solid var(--ctp-surface1); }
|
||||
.mcp-seg-btn:last-child { border-right: none; }
|
||||
.mcp-seg-btn:hover { background: var(--ctp-surface1); color: var(--ctp-subtext1); }
|
||||
.mcp-seg-btn.active {
|
||||
background: color-mix(in srgb, var(--ctp-blue) 20%, var(--ctp-surface0));
|
||||
color: var(--ctp-blue); font-weight: 600;
|
||||
}
|
||||
.mcp-seg-btn.active { background: color-mix(in srgb, var(--ctp-blue) 20%, var(--ctp-surface0)); color: var(--ctp-blue); font-weight: 600; }
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
Rocket, Bug, Puzzle, Box, Layers, GitBranch, Wifi, Lock,
|
||||
FlaskConical, Sparkles, FileCode, Wrench, Folder, Bot, Cloud, HardDrive,
|
||||
} from 'lucide-svelte';
|
||||
import CustomDropdown from './ui/CustomDropdown.svelte';
|
||||
import CustomCheckbox from './ui/CustomCheckbox.svelte';
|
||||
|
||||
interface Props {
|
||||
projectName: string;
|
||||
|
|
@ -28,14 +30,6 @@
|
|||
}: Props = $props();
|
||||
|
||||
const SHELLS = ['bash', 'zsh', 'fish', 'sh'];
|
||||
let branchDDOpen = $state(false);
|
||||
let groupDDOpen = $state(false);
|
||||
let shellDDOpen = $state(false);
|
||||
|
||||
function closeAllDD() { branchDDOpen = false; groupDDOpen = false; shellDDOpen = false; }
|
||||
|
||||
let branchLabel = $derived(selectedBranch || 'main');
|
||||
let groupLabel = $derived(groups.find(g => g.id === selectedGroupId)?.name ?? 'Select');
|
||||
|
||||
const ICON_MAP: Record<string, typeof Terminal> = {
|
||||
Terminal, Server, Globe, Code, Database, Cpu, Zap, Shield,
|
||||
|
|
@ -52,36 +46,26 @@
|
|||
|
||||
{#if isGitRepo && branches.length > 0}
|
||||
<label class="wz-label">Branch</label>
|
||||
<div class="wz-dd" style="position:relative;">
|
||||
<button class="wz-dd-btn" onclick={() => { closeAllDD(); branchDDOpen = !branchDDOpen; }}>
|
||||
<span>{branchLabel}</span><span class="wz-dd-chev">▾</span>
|
||||
</button>
|
||||
<div class="wz-dd-menu" style:display={branchDDOpen ? 'block' : 'none'}>
|
||||
{#each branches as br}
|
||||
<button class="wz-dd-item" class:active={selectedBranch === br}
|
||||
onclick={() => { onUpdate('selectedBranch', br); branchDDOpen = false; }}>{br}</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<label class="wz-toggle-row">
|
||||
<input type="checkbox" checked={useWorktrees}
|
||||
onchange={(e) => onUpdate('useWorktrees', (e.target as HTMLInputElement).checked)} />
|
||||
<span>Use worktrees</span>
|
||||
</label>
|
||||
<CustomDropdown
|
||||
items={branches.map(br => ({ value: br, label: br }))}
|
||||
selected={selectedBranch}
|
||||
placeholder="main"
|
||||
onSelect={v => onUpdate('selectedBranch', v)}
|
||||
/>
|
||||
<CustomCheckbox
|
||||
checked={useWorktrees}
|
||||
label="Use worktrees"
|
||||
onChange={v => onUpdate('useWorktrees', v)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<label class="wz-label">Group</label>
|
||||
<div class="wz-dd" style="position:relative;">
|
||||
<button class="wz-dd-btn" onclick={() => { closeAllDD(); groupDDOpen = !groupDDOpen; }}>
|
||||
<span>{groupLabel}</span><span class="wz-dd-chev">▾</span>
|
||||
</button>
|
||||
<div class="wz-dd-menu" style:display={groupDDOpen ? 'block' : 'none'}>
|
||||
{#each groups as g}
|
||||
<button class="wz-dd-item" class:active={selectedGroupId === g.id}
|
||||
onclick={() => { onUpdate('selectedGroupId', g.id); groupDDOpen = false; }}>{g.name}</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<CustomDropdown
|
||||
items={groups.map(g => ({ value: g.id, label: g.name }))}
|
||||
selected={selectedGroupId}
|
||||
placeholder="Select group"
|
||||
onSelect={v => onUpdate('selectedGroupId', v)}
|
||||
/>
|
||||
|
||||
<label class="wz-label">Icon</label>
|
||||
<div class="wz-icon-grid">
|
||||
|
|
@ -106,17 +90,11 @@
|
|||
</div>
|
||||
|
||||
<label class="wz-label">Shell</label>
|
||||
<div class="wz-dd" style="position:relative;">
|
||||
<button class="wz-dd-btn" onclick={() => { closeAllDD(); shellDDOpen = !shellDDOpen; }}>
|
||||
<span>{shellChoice}</span><span class="wz-dd-chev">▾</span>
|
||||
</button>
|
||||
<div class="wz-dd-menu" style:display={shellDDOpen ? 'block' : 'none'}>
|
||||
{#each SHELLS as sh}
|
||||
<button class="wz-dd-item" class:active={shellChoice === sh}
|
||||
onclick={() => { onUpdate('shellChoice', sh); shellDDOpen = false; }}>{sh}</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<CustomDropdown
|
||||
items={SHELLS.map(sh => ({ value: sh, label: sh }))}
|
||||
selected={shellChoice}
|
||||
onSelect={v => onUpdate('shellChoice', v)}
|
||||
/>
|
||||
|
||||
<style>
|
||||
.wz-label { font-size: 0.75rem; font-weight: 500; color: var(--ctp-subtext0); margin-top: 0.25rem; }
|
||||
|
|
@ -126,15 +104,6 @@
|
|||
.wz-input::placeholder { color: var(--ctp-overlay0); }
|
||||
.wz-hint { font-size: 0.6875rem; }
|
||||
.wz-hint.error { color: var(--ctp-red); }
|
||||
.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: 10rem; 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-toggle-row { display: flex; align-items: center; gap: 0.5rem; font-size: 0.8125rem; color: var(--ctp-text); cursor: pointer; margin-top: 0.25rem; }
|
||||
.wz-icon-grid { display: flex; flex-wrap: wrap; gap: 0.25rem; }
|
||||
.wz-icon-btn { width: 2rem; height: 2rem; border-radius: 0.25rem; border: 1px solid var(--ctp-surface1); background: var(--ctp-surface0); cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--ctp-subtext0); }
|
||||
.wz-icon-btn:hover { border-color: var(--ctp-overlay1); color: var(--ctp-text); }
|
||||
|
|
@ -143,7 +112,4 @@
|
|||
.wz-color-dot { width: 1.5rem; height: 1.5rem; border-radius: 50%; border: 2px solid transparent; cursor: pointer; transition: border-color 0.12s, transform 0.12s; }
|
||||
.wz-color-dot:hover { transform: scale(1.15); }
|
||||
.wz-color-dot.selected { border-color: var(--ctp-text); box-shadow: 0 0 0 1px var(--ctp-surface0); }
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -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…</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>
|
||||
|
|
|
|||
|
|
@ -110,7 +110,8 @@
|
|||
<ChevronDown size={14} class="dd-chev {open ? 'dd-chev-open' : ''}" />
|
||||
</button>
|
||||
|
||||
<div class="dd-backdrop" style:display={open ? 'block' : 'none'} onclick={handleBackdropClick}></div>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions a11y-no-static-element-interactions -->
|
||||
<div class="dd-backdrop" style:display={open ? 'block' : 'none'} onclick={handleBackdropClick} onkeydown={e => e.key === 'Escape' && close()}></div>
|
||||
|
||||
<div class="dd-menu" style:display={open ? 'flex' : 'none'} bind:this={menuRef} role="listbox">
|
||||
{#if groupBy && groups().length > 0}
|
||||
|
|
|
|||
68
ui-electrobun/src/mainview/wizard-state.ts
Normal file
68
ui-electrobun/src/mainview/wizard-state.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
/**
|
||||
* Wizard state management — field update dispatch and default values.
|
||||
*
|
||||
* Extracted from ProjectWizard.svelte to keep it under 300 lines.
|
||||
*/
|
||||
|
||||
export type SourceType = 'local' | 'git-clone' | 'github' | 'template' | 'remote';
|
||||
export type AuthMethod = 'password' | 'key' | 'agent' | 'config';
|
||||
export type PathState = 'idle' | 'checking' | 'valid' | 'invalid' | 'not-dir';
|
||||
export type ProbeState = 'idle' | 'probing' | 'ok' | 'error';
|
||||
|
||||
export interface WizardState {
|
||||
step: number;
|
||||
sourceType: SourceType;
|
||||
localPath: string;
|
||||
repoUrl: string;
|
||||
cloneTarget: string;
|
||||
githubRepo: string;
|
||||
selectedTemplate: string;
|
||||
templateTargetDir: string;
|
||||
remoteHost: string;
|
||||
remoteUser: string;
|
||||
remotePath: string;
|
||||
remoteAuthMethod: AuthMethod;
|
||||
remotePassword: string;
|
||||
remoteKeyPath: string;
|
||||
remoteSshfs: boolean;
|
||||
pathValid: PathState;
|
||||
isGitRepo: boolean;
|
||||
gitBranch: string;
|
||||
gitProbeStatus: ProbeState;
|
||||
gitProbeBranches: string[];
|
||||
githubInfo: { stars: number; description: string; defaultBranch: string } | null;
|
||||
githubLoading: boolean;
|
||||
cloning: boolean;
|
||||
projectName: string;
|
||||
nameError: string;
|
||||
selectedBranch: string;
|
||||
branches: string[];
|
||||
useWorktrees: boolean;
|
||||
selectedGroupId: string;
|
||||
projectIcon: string;
|
||||
projectColor: string;
|
||||
shellChoice: string;
|
||||
provider: string;
|
||||
model: string;
|
||||
permissionMode: string;
|
||||
systemPrompt: string;
|
||||
autoStart: boolean;
|
||||
providerModels: Array<{ id: string; name: string; provider: string }>;
|
||||
modelsLoading: boolean;
|
||||
}
|
||||
|
||||
export function getDefaults(groupId: string): WizardState {
|
||||
return {
|
||||
step: 1, sourceType: 'local', localPath: '', repoUrl: '', cloneTarget: '',
|
||||
githubRepo: '', selectedTemplate: '', templateTargetDir: '~/projects',
|
||||
remoteHost: '', remoteUser: '', remotePath: '',
|
||||
remoteAuthMethod: 'agent', remotePassword: '', remoteKeyPath: '~/.ssh/id_ed25519',
|
||||
remoteSshfs: false, pathValid: 'idle', isGitRepo: false, gitBranch: '',
|
||||
gitProbeStatus: 'idle', gitProbeBranches: [], githubInfo: null, githubLoading: false,
|
||||
cloning: false, projectName: '', nameError: '', selectedBranch: '',
|
||||
branches: [], useWorktrees: false, selectedGroupId: groupId,
|
||||
projectIcon: 'Terminal', projectColor: 'var(--ctp-blue)', shellChoice: 'bash',
|
||||
provider: 'claude', model: '', permissionMode: 'default',
|
||||
systemPrompt: '', autoStart: false, providerModels: [], modelsLoading: false,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue