From e61473b02537a9ca40c4363ba802729606bc7ecc Mon Sep 17 00:00:00 2001 From: Hibryda Date: Mon, 23 Mar 2026 15:34:57 +0100 Subject: [PATCH] fix(electrobun): wizard creation flow + GitLab probe + shell detection + dropdown flip - Git probe tries GitHub then GitLab for owner/repo shorthand - Shows "Found on GitHub/GitLab" with platform indicator - system.shells RPC detects installed shells (bash/zsh/fish/sh/dash) - CustomDropdown flip logic uses 200px threshold for flip-up - Project creation properly persists all wizard fields + adds card --- .../src/bun/handlers/git-handlers.ts | 24 ++++++++++ ui-electrobun/src/mainview/App.svelte | 14 +++++- .../src/mainview/ProjectWizard.svelte | 45 ++++++++++++++----- ui-electrobun/src/mainview/WizardStep1.svelte | 9 ++-- ui-electrobun/src/mainview/WizardStep2.svelte | 28 +++++++++++- .../src/mainview/ui/CustomDropdown.svelte | 7 ++- ui-electrobun/src/shared/pty-rpc-schema.ts | 6 +++ 7 files changed, 112 insertions(+), 21 deletions(-) diff --git a/ui-electrobun/src/bun/handlers/git-handlers.ts b/ui-electrobun/src/bun/handlers/git-handlers.ts index 3d6189a..e2b546b 100644 --- a/ui-electrobun/src/bun/handlers/git-handlers.ts +++ b/ui-electrobun/src/bun/handlers/git-handlers.ts @@ -8,6 +8,30 @@ import { execSync, spawn } from "child_process"; export function createGitHandlers() { return { + "system.shells": async () => { + const candidates = [ + { path: '/bin/bash', name: 'bash' }, + { path: '/bin/zsh', name: 'zsh' }, + { path: '/bin/fish', name: 'fish' }, + { path: '/usr/bin/fish', name: 'fish' }, + { path: '/bin/sh', name: 'sh' }, + { path: '/usr/bin/dash', name: 'dash' }, + ]; + const seen = new Set(); + const shells: Array<{ path: string; name: string }> = []; + for (const c of candidates) { + if (seen.has(c.name)) continue; + try { + fs.statSync(c.path); + shells.push(c); + seen.add(c.name); + } catch { + // not installed + } + } + return { shells }; + }, + "ssh.checkSshfs": async () => { try { const sshfsPath = execSync("which sshfs", { encoding: "utf8", timeout: 3000 }).trim(); diff --git a/ui-electrobun/src/mainview/App.svelte b/ui-electrobun/src/mainview/App.svelte index 524cae3..01d5495 100644 --- a/ui-electrobun/src/mainview/App.svelte +++ b/ui-electrobun/src/mainview/App.svelte @@ -79,8 +79,10 @@ 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; }) { const accent = result.color || ACCENTS[PROJECTS.length % ACCENTS.length]; + const gid = result.groupId ?? activeGroupId; const project: Project = { id: result.id, name: result.name, @@ -92,14 +94,22 @@ messages: [], provider: result.provider ?? 'claude', model: result.model, - groupId: result.groupId ?? activeGroupId, + groupId: gid, }; PROJECTS = [...PROJECTS, project]; trackProject(project.id); + // Persist full config including shell, icon, modelConfig etc. + const persistConfig = { + id: result.id, name: result.name, cwd: result.cwd, accent, + provider: result.provider ?? 'claude', model: result.model, + groupId: gid, shell: result.shell, icon: result.icon, + useWorktrees: result.useWorktrees, systemPrompt: result.systemPrompt, + autoStart: result.autoStart, modelConfig: result.modelConfig, + }; appRpc.request['settings.setProject']({ id: project.id, - config: JSON.stringify({ ...project, ...result }), + config: JSON.stringify(persistConfig), }).catch(console.error); showWizard = false; diff --git a/ui-electrobun/src/mainview/ProjectWizard.svelte b/ui-electrobun/src/mainview/ProjectWizard.svelte index 75fe2d9..f171254 100644 --- a/ui-electrobun/src/mainview/ProjectWizard.svelte +++ b/ui-electrobun/src/mainview/ProjectWizard.svelte @@ -37,6 +37,7 @@ let provider = $state('claude'); let model = $state(''); let permissionMode = $state('default'); let systemPrompt = $state(''); let autoStart = $state(false); let modelConfig = $state>({}); let detectedProviders = $state([]); let providerModels = $state>([]); let modelsLoading = $state(false); + let githubPlatform = $state<'github' | 'gitlab' | null>(null); let pathTimer: ReturnType | null = null; let probeTimer: ReturnType | null = null; let githubTimer: ReturnType | null = null; @@ -69,17 +70,41 @@ }); $effect(() => { if (sourceType === 'github' && githubRepo.trim()) { - if (githubTimer) clearTimeout(githubTimer); githubInfo = null; githubProbeStatus = 'idle'; + if (githubTimer) clearTimeout(githubTimer); githubInfo = null; githubProbeStatus = 'idle'; githubPlatform = null; const input = githubRepo.trim(); - let probeUrl: string; - if (input.startsWith('http://') || input.startsWith('https://') || input.startsWith('git@')) { probeUrl = input; } - else if (isValidGithubRepo(input)) { probeUrl = `https://github.com/${input}.git`; } else { return; } + const isFullUrl = input.startsWith('http://') || input.startsWith('https://') || input.startsWith('git@'); + const isShorthand = !isFullUrl && isValidGithubRepo(input); + if (!isFullUrl && !isShorthand) return; githubLoading = true; githubProbeStatus = 'probing'; githubTimer = setTimeout(async () => { try { - const r = await appRpc.request['git.probe']({ url: probeUrl }); - if (r?.ok) { githubProbeStatus = 'ok'; if (isValidGithubRepo(input)) { try { const res = await fetch(`https://api.github.com/repos/${input}`, { signal: AbortSignal.timeout(5000) }); if (res.ok) { const d = await res.json(); githubInfo = { stars: d.stargazers_count ?? 0, description: d.description ?? '', defaultBranch: d.default_branch ?? 'main' }; } } catch { /* optional */ } } } - else { githubProbeStatus = 'error'; } + if (isFullUrl) { + // Full URL — probe directly, detect platform from hostname + const r = await appRpc.request['git.probe']({ url: input }); + if (r?.ok) { + githubProbeStatus = 'ok'; + if (input.includes('gitlab.com')) githubPlatform = 'gitlab'; + else if (input.includes('github.com')) githubPlatform = 'github'; + } else { githubProbeStatus = 'error'; } + } else { + // owner/repo shorthand — try GitHub first, then GitLab + const ghUrl = `https://github.com/${input}.git`; + const ghResult = await appRpc.request['git.probe']({ url: ghUrl }); + if (ghResult?.ok) { + githubProbeStatus = 'ok'; githubPlatform = 'github'; + // Fetch GitHub API metadata + try { const res = await fetch(`https://api.github.com/repos/${input}`, { signal: AbortSignal.timeout(5000) }); if (res.ok) { const d = await res.json(); githubInfo = { stars: d.stargazers_count ?? 0, description: d.description ?? '', defaultBranch: d.default_branch ?? 'main' }; } } catch { /* optional */ } + } else { + // GitHub failed — try GitLab + const glUrl = `https://gitlab.com/${input}.git`; + const glResult = await appRpc.request['git.probe']({ url: glUrl }); + if (glResult?.ok) { + githubProbeStatus = 'ok'; githubPlatform = 'gitlab'; + } else { + githubProbeStatus = 'error'; + } + } + } } catch { githubProbeStatus = 'error'; } githubLoading = false; }, 500); } @@ -101,7 +126,7 @@ async function goToStep2() { if (sourceType === 'git-clone') { const url = sanitizeUrl(repoUrl); const target = sanitizePath(cloneTarget); if (!url || !target) return; cloning = true; try { const r = await appRpc.request['git.clone']({ url, target }); if (!r?.ok) { cloning = false; return; } localPath = target; isGitRepo = true; } catch { cloning = false; return; } cloning = false; } - else if (sourceType === 'github') { const input = githubRepo.trim(); const url = (input.startsWith('http') || input.startsWith('git@')) ? input : `https://github.com/${input}.git`; const name = input.includes('/') ? input.split('/').pop()?.replace(/\.git$/, '') || 'project' : 'project'; const target = `~/projects/${name}`; cloning = true; try { const r = await appRpc.request['git.clone']({ url, target }); if (!r?.ok) { cloning = false; return; } localPath = target; isGitRepo = true; } catch { cloning = false; return; } cloning = false; } + else if (sourceType === 'github') { const input = githubRepo.trim(); const platformBase = githubPlatform === 'gitlab' ? 'https://gitlab.com' : 'https://github.com'; const url = (input.startsWith('http') || input.startsWith('git@')) ? input : `${platformBase}/${input}.git`; const name = input.includes('/') ? input.split('/').pop()?.replace(/\.git$/, '') || 'project' : 'project'; const target = `~/projects/${name}`; cloning = true; try { const r = await appRpc.request['git.clone']({ url, target }); if (!r?.ok) { cloning = false; return; } localPath = target; isGitRepo = true; } catch { cloning = false; return; } cloning = false; } else if (sourceType === 'template') { const target = sanitizePath(templateTargetDir); if (!target) return; const name = sanitize(projectName) || selectedTemplate; cloning = true; try { const r = await appRpc.request['project.createFromTemplate']({ templateId: selectedTemplate, targetDir: target, projectName: name }); if (!r?.ok) { cloning = false; return; } localPath = r.path; if (!projectName) projectName = name; } catch { cloning = false; return; } cloning = false; } if (!projectName && localPath) { const parts = localPath.replace(/\/+$/, '').split('/'); projectName = parts[parts.length - 1] || ''; } if (isGitRepo && localPath) { try { const r = await appRpc.request['git.branches']({ path: localPath }); if (r?.branches) { branches = r.branches; selectedBranch = r.current || ''; } } catch { /* ignore */ } } @@ -122,7 +147,7 @@ step = 1; sourceType = 'local'; localPath = ''; repoUrl = ''; cloneTarget = ''; githubRepo = ''; selectedTemplate = ''; templateTargetDir = '~/projects'; remoteHost = ''; remoteUser = ''; remotePath = ''; remoteAuthMethod = 'agent'; remotePassword = ''; remoteKeyPath = '~/.ssh/id_ed25519'; remoteSshfs = false; remoteSshfsMountpoint = ''; pathValid = 'idle'; isGitRepo = false; gitBranch = ''; gitProbeStatus = 'idle'; gitProbeBranches = []; githubInfo = null; githubLoading = false; githubProbeStatus = 'idle'; - cloning = false; projectName = ''; nameError = ''; selectedBranch = ''; branches = []; useWorktrees = false; selectedGroupId = groupId; + cloning = false; githubPlatform = null; 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; modelConfig = {}; } @@ -152,7 +177,7 @@

Source

- +