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
This commit is contained in:
parent
021feba3ed
commit
e61473b025
7 changed files with 112 additions and 21 deletions
|
|
@ -8,6 +8,30 @@ import { execSync, spawn } from "child_process";
|
||||||
|
|
||||||
export function createGitHandlers() {
|
export function createGitHandlers() {
|
||||||
return {
|
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<string>();
|
||||||
|
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 () => {
|
"ssh.checkSshfs": async () => {
|
||||||
try {
|
try {
|
||||||
const sshfsPath = execSync("which sshfs", { encoding: "utf8", timeout: 3000 }).trim();
|
const sshfsPath = execSync("which sshfs", { encoding: "utf8", timeout: 3000 }).trim();
|
||||||
|
|
|
||||||
|
|
@ -79,8 +79,10 @@
|
||||||
id: string; name: string; cwd: string; provider?: string; model?: string;
|
id: string; name: string; cwd: string; provider?: string; model?: string;
|
||||||
systemPrompt?: string; autoStart?: boolean; groupId?: string;
|
systemPrompt?: string; autoStart?: boolean; groupId?: string;
|
||||||
useWorktrees?: boolean; shell?: string; icon?: string; color?: string;
|
useWorktrees?: boolean; shell?: string; icon?: string; color?: string;
|
||||||
|
modelConfig?: Record<string, unknown>;
|
||||||
}) {
|
}) {
|
||||||
const accent = result.color || ACCENTS[PROJECTS.length % ACCENTS.length];
|
const accent = result.color || ACCENTS[PROJECTS.length % ACCENTS.length];
|
||||||
|
const gid = result.groupId ?? activeGroupId;
|
||||||
const project: Project = {
|
const project: Project = {
|
||||||
id: result.id,
|
id: result.id,
|
||||||
name: result.name,
|
name: result.name,
|
||||||
|
|
@ -92,14 +94,22 @@
|
||||||
messages: [],
|
messages: [],
|
||||||
provider: result.provider ?? 'claude',
|
provider: result.provider ?? 'claude',
|
||||||
model: result.model,
|
model: result.model,
|
||||||
groupId: result.groupId ?? activeGroupId,
|
groupId: gid,
|
||||||
};
|
};
|
||||||
PROJECTS = [...PROJECTS, project];
|
PROJECTS = [...PROJECTS, project];
|
||||||
trackProject(project.id);
|
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']({
|
appRpc.request['settings.setProject']({
|
||||||
id: project.id,
|
id: project.id,
|
||||||
config: JSON.stringify({ ...project, ...result }),
|
config: JSON.stringify(persistConfig),
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
|
|
||||||
showWizard = false;
|
showWizard = false;
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@
|
||||||
let provider = $state<string>('claude'); let model = $state(''); let permissionMode = $state('default');
|
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>>({});
|
let systemPrompt = $state(''); let autoStart = $state(false); let modelConfig = $state<Record<string, unknown>>({});
|
||||||
let detectedProviders = $state<ProviderInfo[]>([]); let providerModels = $state<Array<{ id: string; name: string; provider: string }>>([]); let modelsLoading = $state(false);
|
let detectedProviders = $state<ProviderInfo[]>([]); let providerModels = $state<Array<{ id: string; name: string; provider: string }>>([]); let modelsLoading = $state(false);
|
||||||
|
let githubPlatform = $state<'github' | 'gitlab' | null>(null);
|
||||||
let pathTimer: ReturnType<typeof setTimeout> | null = null;
|
let pathTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
let probeTimer: ReturnType<typeof setTimeout> | null = null;
|
let probeTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
let githubTimer: ReturnType<typeof setTimeout> | null = null;
|
let githubTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
@ -69,17 +70,41 @@
|
||||||
});
|
});
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (sourceType === 'github' && githubRepo.trim()) {
|
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();
|
const input = githubRepo.trim();
|
||||||
let probeUrl: string;
|
const isFullUrl = input.startsWith('http://') || input.startsWith('https://') || input.startsWith('git@');
|
||||||
if (input.startsWith('http://') || input.startsWith('https://') || input.startsWith('git@')) { probeUrl = input; }
|
const isShorthand = !isFullUrl && isValidGithubRepo(input);
|
||||||
else if (isValidGithubRepo(input)) { probeUrl = `https://github.com/${input}.git`; } else { return; }
|
if (!isFullUrl && !isShorthand) return;
|
||||||
githubLoading = true; githubProbeStatus = 'probing';
|
githubLoading = true; githubProbeStatus = 'probing';
|
||||||
githubTimer = setTimeout(async () => {
|
githubTimer = setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
const r = await appRpc.request['git.probe']({ url: probeUrl });
|
if (isFullUrl) {
|
||||||
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 */ } } }
|
// Full URL — probe directly, detect platform from hostname
|
||||||
else { githubProbeStatus = 'error'; }
|
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;
|
} catch { githubProbeStatus = 'error'; } githubLoading = false;
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
@ -101,7 +126,7 @@
|
||||||
|
|
||||||
async function goToStep2() {
|
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; }
|
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; }
|
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 (!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 */ } }
|
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';
|
step = 1; sourceType = 'local'; localPath = ''; repoUrl = ''; cloneTarget = ''; githubRepo = ''; selectedTemplate = ''; templateTargetDir = '~/projects';
|
||||||
remoteHost = ''; remoteUser = ''; remotePath = ''; remoteAuthMethod = 'agent'; remotePassword = ''; remoteKeyPath = '~/.ssh/id_ed25519'; remoteSshfs = false; remoteSshfsMountpoint = '';
|
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';
|
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';
|
projectIcon = 'Terminal'; projectColor = 'var(--ctp-blue)'; shellChoice = 'bash'; provider = 'claude'; model = ''; permissionMode = 'default';
|
||||||
systemPrompt = ''; autoStart = false; providerModels = []; modelsLoading = false; modelConfig = {};
|
systemPrompt = ''; autoStart = false; providerModels = []; modelsLoading = false; modelConfig = {};
|
||||||
}
|
}
|
||||||
|
|
@ -152,7 +177,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="wz-body" style:display={step === 1 ? 'flex' : 'none'}>
|
<div class="wz-body" style:display={step === 1 ? 'flex' : 'none'}>
|
||||||
<h3 class="wz-step-title">Source</h3>
|
<h3 class="wz-step-title">Source</h3>
|
||||||
<WizardStep1 bind:this={step1Ref} {sourceType} {localPath} {repoUrl} {cloneTarget} {githubRepo} {selectedTemplate} {remoteHost} {remoteUser} {remotePath} {remoteAuthMethod} {remotePassword} {remoteKeyPath} {remoteSshfs} {remoteSshfsMountpoint} {pathValid} {isGitRepo} {gitBranch} {gitProbeStatus} {gitProbeBranches} {githubInfo} {githubProbeStatus} {githubLoading} {cloning} {templates} {templateTargetDir} {templateOriginDir} onUpdate={handleUpdate} />
|
<WizardStep1 bind:this={step1Ref} {sourceType} {localPath} {repoUrl} {cloneTarget} {githubRepo} {selectedTemplate} {remoteHost} {remoteUser} {remotePath} {remoteAuthMethod} {remotePassword} {remoteKeyPath} {remoteSshfs} {remoteSshfsMountpoint} {pathValid} {isGitRepo} {gitBranch} {gitProbeStatus} {gitProbeBranches} {githubInfo} {githubProbeStatus} {githubLoading} {githubPlatform} {cloning} {templates} {templateTargetDir} {templateOriginDir} onUpdate={handleUpdate} />
|
||||||
<div class="wz-footer">
|
<div class="wz-footer">
|
||||||
<button class="wz-btn secondary" onclick={closeWizard}>Cancel</button>
|
<button class="wz-btn secondary" onclick={closeWizard}>Cancel</button>
|
||||||
<div class="wz-footer-right">{#if cloning}<span class="wz-cloning">Cloning…</span>{/if}<button class="wz-btn primary" disabled={!step1Valid() || cloning} onclick={goToStep2}>Next</button></div>
|
<div class="wz-footer-right">{#if cloning}<span class="wz-cloning">Cloning…</span>{/if}<button class="wz-btn primary" disabled={!step1Valid() || cloning} onclick={goToStep2}>Next</button></div>
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
gitProbeStatus: 'idle' | 'probing' | 'ok' | 'error'; gitProbeBranches: string[];
|
gitProbeStatus: 'idle' | 'probing' | 'ok' | 'error'; gitProbeBranches: string[];
|
||||||
githubInfo: { stars: number; description: string; defaultBranch: string } | null;
|
githubInfo: { stars: number; description: string; defaultBranch: string } | null;
|
||||||
githubProbeStatus: 'idle' | 'probing' | 'ok' | 'error'; githubLoading: boolean;
|
githubProbeStatus: 'idle' | 'probing' | 'ok' | 'error'; githubLoading: boolean;
|
||||||
|
githubPlatform: 'github' | 'gitlab' | null;
|
||||||
cloning: boolean; templates: Array<{ id: string; name: string; description: string; icon: string }>;
|
cloning: boolean; templates: Array<{ id: string; name: string; description: string; icon: string }>;
|
||||||
templateTargetDir: string; templateOriginDir: string;
|
templateTargetDir: string; templateOriginDir: string;
|
||||||
onUpdate: (field: string, value: unknown) => void;
|
onUpdate: (field: string, value: unknown) => void;
|
||||||
|
|
@ -28,7 +29,7 @@
|
||||||
remoteHost, remoteUser, remotePath, remoteAuthMethod, remotePassword,
|
remoteHost, remoteUser, remotePath, remoteAuthMethod, remotePassword,
|
||||||
remoteKeyPath, remoteSshfs, remoteSshfsMountpoint = '', pathValid, isGitRepo,
|
remoteKeyPath, remoteSshfs, remoteSshfsMountpoint = '', pathValid, isGitRepo,
|
||||||
gitBranch, gitProbeStatus, gitProbeBranches, githubInfo,
|
gitBranch, gitProbeStatus, gitProbeBranches, githubInfo,
|
||||||
githubProbeStatus = 'idle', githubLoading, cloning, templates,
|
githubProbeStatus = 'idle', githubLoading, githubPlatform = null, cloning, templates,
|
||||||
templateTargetDir, templateOriginDir = '', onUpdate,
|
templateTargetDir, templateOriginDir = '', onUpdate,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
|
|
@ -122,8 +123,10 @@
|
||||||
<input class="wz-input" type="text" placeholder="owner/repo or https://gitlab.com/owner/repo"
|
<input class="wz-input" type="text" placeholder="owner/repo or https://gitlab.com/owner/repo"
|
||||||
value={githubRepo} oninput={(e) => onUpdate('githubRepo', (e.target as HTMLInputElement).value)} />
|
value={githubRepo} oninput={(e) => onUpdate('githubRepo', (e.target as HTMLInputElement).value)} />
|
||||||
{#if githubLoading}<span class="wz-hint" style="color: var(--ctp-blue);">Checking…</span>{/if}
|
{#if githubLoading}<span class="wz-hint" style="color: var(--ctp-blue);">Checking…</span>{/if}
|
||||||
{#if githubProbeStatus === 'ok'}<span class="wz-hint valid">Repository verified</span>{/if}
|
{#if githubProbeStatus === 'ok' && githubPlatform === 'github'}<span class="wz-hint valid">Found on GitHub ✓</span>{/if}
|
||||||
{#if githubProbeStatus === 'error'}<span class="wz-hint error">Repository not found or inaccessible</span>{/if}
|
{#if githubProbeStatus === 'ok' && githubPlatform === 'gitlab'}<span class="wz-hint valid">Found on GitLab ✓</span>{/if}
|
||||||
|
{#if githubProbeStatus === 'ok' && !githubPlatform}<span class="wz-hint valid">Repository verified ✓</span>{/if}
|
||||||
|
{#if githubProbeStatus === 'error'}<span class="wz-hint error">Repository not found on GitHub or GitLab</span>{/if}
|
||||||
{#if githubInfo}
|
{#if githubInfo}
|
||||||
<div class="wz-github-info">
|
<div class="wz-github-info">
|
||||||
<span class="wz-hint" style="color: var(--ctp-yellow);">★ {githubInfo.stars}</span>
|
<span class="wz-hint" style="color: var(--ctp-yellow);">★ {githubInfo.stars}</span>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
import { PROJECT_ICONS, ACCENT_COLORS } from './wizard-icons.ts';
|
import { PROJECT_ICONS, ACCENT_COLORS } from './wizard-icons.ts';
|
||||||
import {
|
import {
|
||||||
Terminal, Server, Globe, Code, Database, Cpu, Zap, Shield,
|
Terminal, Server, Globe, Code, Database, Cpu, Zap, Shield,
|
||||||
|
|
@ -11,6 +12,7 @@
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
import CustomDropdown from './ui/CustomDropdown.svelte';
|
import CustomDropdown from './ui/CustomDropdown.svelte';
|
||||||
import CustomCheckbox from './ui/CustomCheckbox.svelte';
|
import CustomCheckbox from './ui/CustomCheckbox.svelte';
|
||||||
|
import { appRpc } from './rpc.ts';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
projectName: string;
|
projectName: string;
|
||||||
|
|
@ -33,7 +35,29 @@
|
||||||
isGitRepo, onUpdate,
|
isGitRepo, onUpdate,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const SHELLS = ['bash', 'zsh', 'fish', 'sh'];
|
let detectedShells = $state<Array<{ path: string; name: string }>>([]);
|
||||||
|
const FALLBACK_SHELLS = ['bash', 'zsh', 'fish', 'sh'];
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
const r = await appRpc.request['system.shells']({});
|
||||||
|
if (r?.shells?.length) {
|
||||||
|
detectedShells = r.shells;
|
||||||
|
// If current shellChoice is not in detected shells, select the first available
|
||||||
|
if (!r.shells.some(s => s.name === shellChoice)) {
|
||||||
|
onUpdate('shellChoice', r.shells[0].name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fallback — use hardcoded list
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let shellItems = $derived(
|
||||||
|
detectedShells.length > 0
|
||||||
|
? detectedShells.map(s => ({ value: s.name, label: `${s.name} (${s.path})` }))
|
||||||
|
: FALLBACK_SHELLS.map(sh => ({ value: sh, label: sh }))
|
||||||
|
);
|
||||||
|
|
||||||
const ICON_MAP: Record<string, typeof Terminal> = {
|
const ICON_MAP: Record<string, typeof Terminal> = {
|
||||||
Terminal, Server, Globe, Code, Database, Cpu, Zap, Shield,
|
Terminal, Server, Globe, Code, Database, Cpu, Zap, Shield,
|
||||||
|
|
@ -108,7 +132,7 @@
|
||||||
|
|
||||||
<label class="wz-label">Shell</label>
|
<label class="wz-label">Shell</label>
|
||||||
<CustomDropdown
|
<CustomDropdown
|
||||||
items={SHELLS.map(sh => ({ value: sh, label: sh }))}
|
items={shellItems}
|
||||||
selected={shellChoice}
|
selected={shellChoice}
|
||||||
onSelect={v => onUpdate('shellChoice', v)}
|
onSelect={v => onUpdate('shellChoice', v)}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,7 @@
|
||||||
if (!triggerRef) return;
|
if (!triggerRef) return;
|
||||||
const rect = triggerRef.getBoundingClientRect();
|
const rect = triggerRef.getBoundingClientRect();
|
||||||
const spaceBelow = window.innerHeight - rect.bottom;
|
const spaceBelow = window.innerHeight - rect.bottom;
|
||||||
const menuHeight = Math.min(items.length * 2.25 * 16, 14 * 16); // estimate
|
flipUp = spaceBelow < 200 && rect.top > spaceBelow;
|
||||||
flipUp = spaceBelow < menuHeight && rect.top > spaceBelow;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
|
|
@ -195,7 +194,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.dd-menu {
|
.dd-menu {
|
||||||
position: absolute; top: calc(100% + 0.125rem); left: 0; right: 0; z-index: 50;
|
position: absolute; top: calc(100% + 0.25rem); left: 0; right: 0; z-index: 50;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: var(--ctp-mantle); border: 1px solid var(--ctp-surface1); border-radius: 0.25rem;
|
background: var(--ctp-mantle); border: 1px solid var(--ctp-surface1); border-radius: 0.25rem;
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
|
|
@ -204,7 +203,7 @@
|
||||||
}
|
}
|
||||||
.dd-menu.flip-up {
|
.dd-menu.flip-up {
|
||||||
top: auto;
|
top: auto;
|
||||||
bottom: calc(100% + 0.125rem);
|
bottom: calc(100% + 0.25rem);
|
||||||
box-shadow: 0 -0.5rem 1rem color-mix(in srgb, var(--ctp-crust) 60%, transparent);
|
box-shadow: 0 -0.5rem 1rem color-mix(in srgb, var(--ctp-crust) 60%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -248,6 +248,12 @@ export type PtyRPCRequests = {
|
||||||
response: { installed: boolean; path: string | null };
|
response: { installed: boolean; path: string | null };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Detect available shells on this system. */
|
||||||
|
"system.shells": {
|
||||||
|
params: Record<string, never>;
|
||||||
|
response: { shells: Array<{ path: string; name: string }> };
|
||||||
|
};
|
||||||
|
|
||||||
// ── Project templates RPC ───────────────────────────────────────────────────
|
// ── Project templates RPC ───────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Return available project templates. Optionally pass custom template dir. */
|
/** Return available project templates. Optionally pass custom template dir. */
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue