fix(electrobun): wizard fixes — native dialog, models, PathBrowser, ensureDir
- Native dialog: resolve to nearest existing parent dir, detect user cancel (exit code 1) vs actual error, add createIfMissing option - Claude models: fallback to KNOWN_CLAUDE_MODELS (6 models) when API key unavailable. Adds Opus 4.6, Sonnet 4.6, Opus 4.5, Sonnet 4, Haiku 4.5, Sonnet 3.7. Live API paginated to limit=100. - PathBrowser: Select button moved to sticky header (always visible). Current path shown compact in header with RTL ellipsis. - files.ensureDir RPC: creates directory recursively before project creation - files.ensureDir added to RPC schema
This commit is contained in:
parent
162b5417e4
commit
a4d180d382
6 changed files with 77 additions and 12 deletions
|
|
@ -163,6 +163,16 @@ export function createFilesHandlers() {
|
|||
}
|
||||
},
|
||||
|
||||
"files.ensureDir": async ({ path: dirPath }: { path: string }) => {
|
||||
try {
|
||||
const resolved = path.resolve(dirPath.replace(/^~/, process.env.HOME ?? ""));
|
||||
fs.mkdirSync(resolved, { recursive: true });
|
||||
return { ok: true, path: resolved };
|
||||
} catch (err) {
|
||||
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
||||
}
|
||||
},
|
||||
|
||||
"files.write": async ({ path: filePath, content }: { path: string; content: string }) => {
|
||||
const guard = guardPath(filePath);
|
||||
if (!guard.valid) {
|
||||
|
|
|
|||
|
|
@ -56,16 +56,30 @@ export function createMiscHandlers(deps: MiscDeps) {
|
|||
|
||||
return {
|
||||
// ── Files: picker + homeDir ─────────────────────────────────────────
|
||||
"files.pickDirectory": async ({ startingFolder }: { startingFolder?: string }) => {
|
||||
"files.pickDirectory": async ({ startingFolder, createIfMissing }: { startingFolder?: string; createIfMissing?: boolean }) => {
|
||||
try {
|
||||
const { execSync } = await import("child_process");
|
||||
const start = startingFolder?.replace(/^~/, process.env.HOME || "/home") || process.env.HOME || "/home";
|
||||
const fs = await import("fs");
|
||||
const home = process.env.HOME || "/home";
|
||||
let start = (startingFolder || "~/").replace(/^~/, home);
|
||||
// Resolve to nearest existing parent
|
||||
while (start && start !== "/" && !fs.existsSync(start)) {
|
||||
start = start.replace(/\/[^/]+\/?$/, "") || home;
|
||||
}
|
||||
if (!start || !fs.existsSync(start)) start = home;
|
||||
const result = execSync(
|
||||
`zenity --file-selection --directory --title="Select Project Folder" --filename="${start}/"`,
|
||||
{ encoding: "utf-8", timeout: 120_000 },
|
||||
).trim();
|
||||
if (result && createIfMissing) {
|
||||
fs.mkdirSync(result, { recursive: true });
|
||||
}
|
||||
return { path: result || null };
|
||||
} catch {
|
||||
} catch (err: unknown) {
|
||||
// Exit code 1 = user cancelled — not an error
|
||||
if (err && typeof err === "object" && "status" in err && (err as {status:number}).status === 1) {
|
||||
return { path: null };
|
||||
}
|
||||
return { path: null };
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,24 +13,35 @@ export interface ModelInfo {
|
|||
|
||||
const TIMEOUT = 8000;
|
||||
|
||||
// Known Claude models as fallback when API key is not available
|
||||
const KNOWN_CLAUDE_MODELS: ModelInfo[] = [
|
||||
{ id: 'claude-opus-4-6', name: 'Claude Opus 4.6', provider: 'claude' },
|
||||
{ id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6', provider: 'claude' },
|
||||
{ id: 'claude-opus-4-5-20250514', name: 'Claude Opus 4.5', provider: 'claude' },
|
||||
{ id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4', provider: 'claude' },
|
||||
{ id: 'claude-haiku-4-5-20251001', name: 'Claude Haiku 4.5', provider: 'claude' },
|
||||
{ id: 'claude-sonnet-3-7-20250219', name: 'Claude Sonnet 3.7', provider: 'claude' },
|
||||
];
|
||||
|
||||
export async function fetchClaudeModels(): Promise<ModelInfo[]> {
|
||||
const apiKey = process.env.ANTHROPIC_API_KEY;
|
||||
if (!apiKey) return [];
|
||||
if (!apiKey) return KNOWN_CLAUDE_MODELS;
|
||||
try {
|
||||
const res = await fetch('https://api.anthropic.com/v1/models', {
|
||||
const res = await fetch('https://api.anthropic.com/v1/models?limit=100', {
|
||||
headers: {
|
||||
'x-api-key': apiKey,
|
||||
'anthropic-version': '2023-06-01',
|
||||
},
|
||||
signal: AbortSignal.timeout(TIMEOUT),
|
||||
});
|
||||
if (!res.ok) return [];
|
||||
if (!res.ok) return KNOWN_CLAUDE_MODELS;
|
||||
const data = await res.json() as { data?: Array<{ id: string; display_name?: string }> };
|
||||
return (data.data ?? [])
|
||||
const live = (data.data ?? [])
|
||||
.map(m => ({ id: m.id, name: m.display_name ?? m.id, provider: 'claude' }))
|
||||
.sort((a, b) => a.id.localeCompare(b.id));
|
||||
return live.length > 0 ? live : KNOWN_CLAUDE_MODELS;
|
||||
} catch {
|
||||
return [];
|
||||
return KNOWN_CLAUDE_MODELS;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -125,10 +125,14 @@
|
|||
</script>
|
||||
|
||||
<div class="path-browser">
|
||||
<!-- Header -->
|
||||
<!-- Header with Select always visible -->
|
||||
<div class="pb-header">
|
||||
<span class="pb-title">{t('wizard.step1.browse' as any)}</span>
|
||||
<button class="pb-close" onclick={onClose} aria-label={t('common.close' as any)}>✕</button>
|
||||
<div class="pb-header-actions">
|
||||
<span class="pb-current-compact" title={currentPath}>{currentPath}</span>
|
||||
<button class="pb-select-btn" onclick={confirmSelect}>Select</button>
|
||||
<button class="pb-close" onclick={onClose} aria-label={t('common.close' as any)}>✕</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shortcuts -->
|
||||
|
|
@ -172,10 +176,9 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Footer: current path + select -->
|
||||
<!-- Footer: current path (Select moved to header) -->
|
||||
<div class="pb-footer">
|
||||
<span class="pb-current" title={currentPath}>{currentPath}</span>
|
||||
<button class="pb-select-btn" onclick={confirmSelect}>Select</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -199,6 +202,25 @@
|
|||
justify-content: space-between;
|
||||
padding: 0.375rem 0.5rem;
|
||||
border-bottom: 1px solid var(--ctp-surface0);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
background: var(--ctp-mantle);
|
||||
}
|
||||
.pb-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
.pb-current-compact {
|
||||
font-size: 0.6875rem;
|
||||
color: var(--ctp-subtext0);
|
||||
max-width: 12rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
direction: rtl;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.pb-title { font-size: 0.75rem; font-weight: 600; color: var(--ctp-text); }
|
||||
|
|
|
|||
|
|
@ -137,6 +137,10 @@
|
|||
|
||||
async function createProject() {
|
||||
const cwd = sourceType === 'remote' ? `ssh://${sanitize(remoteUser)}@${sanitize(remoteHost)}:${sanitize(remotePath)}` : (sanitizePath(localPath) || localPath.trim());
|
||||
// Ensure directory exists for local paths
|
||||
if (sourceType !== 'remote' && cwd) {
|
||||
try { await appRpc.request['files.ensureDir']({ path: cwd }); } catch { /* best effort */ }
|
||||
}
|
||||
onCreated({ id: `p-${Date.now()}`, name: sanitize(projectName) || 'Untitled', cwd, provider, 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 });
|
||||
resetState();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -146,6 +146,10 @@ export type PtyRPCRequests = {
|
|||
error?: string;
|
||||
};
|
||||
};
|
||||
"files.ensureDir": {
|
||||
params: { path: string };
|
||||
response: { ok: boolean; path?: string; error?: string };
|
||||
};
|
||||
/** Unguarded directory listing for PathBrowser (dirs only, no file content) */
|
||||
"files.browse": {
|
||||
params: { path: string };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue