feat(electrobun): ProjectWizard — 3-step project creation with 5 source types
Step 1 — Source: local folder (path browser + validation), git clone, GitHub URL, template (4 built-in), remote SSH Step 2 — Configure: name, branch selector, worktree toggle, group, icon, shell Step 3 — Agent: provider, model, permission mode, system prompt, auto-start - ProjectWizard.svelte: 3-step wizard with display toggle (rule 55) - PathBrowser.svelte: inline directory browser with breadcrumbs + shortcuts - git-handlers.ts: git.branches + git.clone RPC handlers - files.statEx RPC: path validation + git detection + writable check - 39 new i18n keys, 172 total TranslationKey entries - App.svelte: wizard overlay replaces simple add-project card
This commit is contained in:
parent
1d2975b07b
commit
45bca3b96f
9 changed files with 1203 additions and 45 deletions
|
|
@ -8,6 +8,7 @@
|
|||
import StatusBar from './StatusBar.svelte';
|
||||
import SearchOverlay from './SearchOverlay.svelte';
|
||||
import SplashScreen from './SplashScreen.svelte';
|
||||
import ProjectWizard from './ProjectWizard.svelte';
|
||||
import { themeStore } from './theme-store.svelte.ts';
|
||||
import { fontStore } from './font-store.svelte.ts';
|
||||
import { keybindingStore } from './keybinding-store.svelte.ts';
|
||||
|
|
@ -71,34 +72,37 @@
|
|||
]);
|
||||
|
||||
// ── Add/Remove project UI state ──────────────────────────────────
|
||||
let showAddProject = $state(false);
|
||||
let newProjectName = $state('');
|
||||
let newProjectCwd = $state('');
|
||||
let showWizard = $state(false);
|
||||
let projectToDelete = $state<string | null>(null);
|
||||
|
||||
async function addProject() {
|
||||
const name = newProjectName.trim();
|
||||
const cwd = newProjectCwd.trim();
|
||||
if (!name || !cwd) return;
|
||||
|
||||
const id = `p-${Date.now()}`;
|
||||
function handleWizardCreated(result: {
|
||||
id: string; name: string; cwd: string; provider?: string; model?: string;
|
||||
systemPrompt?: string; autoStart?: boolean; groupId?: string;
|
||||
useWorktrees?: boolean; shell?: string; icon?: string;
|
||||
}) {
|
||||
const accent = ACCENTS[PROJECTS.length % ACCENTS.length];
|
||||
const project: Project = {
|
||||
id, name, cwd, accent,
|
||||
status: 'idle', costUsd: 0, tokens: 0, messages: [],
|
||||
provider: 'claude', groupId: activeGroupId,
|
||||
id: result.id,
|
||||
name: result.name,
|
||||
cwd: result.cwd,
|
||||
accent,
|
||||
status: 'idle',
|
||||
costUsd: 0,
|
||||
tokens: 0,
|
||||
messages: [],
|
||||
provider: result.provider ?? 'claude',
|
||||
model: result.model,
|
||||
groupId: result.groupId ?? activeGroupId,
|
||||
};
|
||||
PROJECTS = [...PROJECTS, project];
|
||||
trackProject(id);
|
||||
trackProject(project.id);
|
||||
|
||||
await appRpc.request['settings.setProject']({
|
||||
id,
|
||||
config: JSON.stringify(project),
|
||||
appRpc.request['settings.setProject']({
|
||||
id: project.id,
|
||||
config: JSON.stringify({ ...project, ...result }),
|
||||
}).catch(console.error);
|
||||
|
||||
showAddProject = false;
|
||||
newProjectName = '';
|
||||
newProjectCwd = '';
|
||||
showWizard = false;
|
||||
}
|
||||
|
||||
async function confirmDeleteProject() {
|
||||
|
|
@ -357,7 +361,7 @@
|
|||
switch (detail) {
|
||||
case 'settings': settingsOpen = !settingsOpen; break;
|
||||
case 'search': searchOpen = !searchOpen; break;
|
||||
case 'new-project': showAddProject = true; break;
|
||||
case 'new-project': showWizard = true; break;
|
||||
case 'toggle-sidebar': settingsOpen = !settingsOpen; break;
|
||||
default: console.log(`[palette] unhandled command: ${detail}`);
|
||||
}
|
||||
|
|
@ -439,7 +443,7 @@
|
|||
<!-- Add project button -->
|
||||
<button
|
||||
class="sidebar-icon"
|
||||
onclick={() => showAddProject = !showAddProject}
|
||||
onclick={() => showWizard = !showWizard}
|
||||
aria-label="Add project"
|
||||
title="Add project"
|
||||
>
|
||||
|
|
@ -501,28 +505,14 @@
|
|||
<p class="empty-group-text">No projects in {activeGroup?.name ?? 'this group'}</p>
|
||||
</div>
|
||||
|
||||
<!-- Add project card -->
|
||||
<div class="add-card" role="listitem" style:display={showAddProject ? 'flex' : 'none'}>
|
||||
<div class="add-card-form">
|
||||
<input
|
||||
class="add-input"
|
||||
type="text"
|
||||
placeholder="Project name"
|
||||
bind:value={newProjectName}
|
||||
onkeydown={(e) => { if (e.key === 'Enter') addProject(); if (e.key === 'Escape') showAddProject = false; }}
|
||||
/>
|
||||
<input
|
||||
class="add-input"
|
||||
type="text"
|
||||
placeholder="Working directory (e.g. ~/code/myproject)"
|
||||
bind:value={newProjectCwd}
|
||||
onkeydown={(e) => { if (e.key === 'Enter') addProject(); if (e.key === 'Escape') showAddProject = false; }}
|
||||
/>
|
||||
<div class="add-card-actions">
|
||||
<button class="add-cancel" onclick={() => showAddProject = false}>Cancel</button>
|
||||
<button class="add-confirm" onclick={addProject}>Add</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Project wizard overlay (display toggle) -->
|
||||
<div style:display={showWizard ? 'contents' : 'none'}>
|
||||
<ProjectWizard
|
||||
onClose={() => showWizard = false}
|
||||
onCreated={handleWizardCreated}
|
||||
groupId={activeGroupId}
|
||||
groups={groups.map(g => ({ id: g.id, name: g.name }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Delete project confirmation -->
|
||||
|
|
|
|||
252
ui-electrobun/src/mainview/PathBrowser.svelte
Normal file
252
ui-electrobun/src/mainview/PathBrowser.svelte
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
<script lang="ts">
|
||||
import { appRpc } from './rpc.ts';
|
||||
import { t } from './i18n.svelte.ts';
|
||||
|
||||
interface Props {
|
||||
onSelect: (path: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
let { onSelect, onClose }: Props = $props();
|
||||
|
||||
const HOME = '/home/' + (typeof window !== 'undefined' ? '' : '');
|
||||
const SHORTCUTS = [
|
||||
{ label: 'Home', path: '~' },
|
||||
{ label: 'Desktop', path: '~/Desktop' },
|
||||
{ label: 'Documents', path: '~/Documents' },
|
||||
{ label: '/tmp', path: '/tmp' },
|
||||
];
|
||||
|
||||
let currentPath = $state('~');
|
||||
let entries = $state<Array<{ name: string; type: 'file' | 'dir'; size: number }>>([]);
|
||||
let loading = $state(false);
|
||||
let filter = $state('');
|
||||
let error = $state('');
|
||||
|
||||
// Breadcrumb segments
|
||||
let breadcrumbs = $derived(() => {
|
||||
const parts = currentPath.split('/').filter(Boolean);
|
||||
const crumbs: Array<{ label: string; path: string }> = [];
|
||||
let acc = '';
|
||||
for (const part of parts) {
|
||||
acc += (acc === '' && currentPath.startsWith('~') && crumbs.length === 0) ? part : '/' + part;
|
||||
if (crumbs.length === 0 && currentPath.startsWith('~')) acc = part;
|
||||
crumbs.push({ label: part, path: acc.startsWith('~') ? acc : '/' + acc });
|
||||
}
|
||||
return crumbs;
|
||||
});
|
||||
|
||||
let filteredEntries = $derived(
|
||||
entries.filter(e => e.type === 'dir' && (filter === '' || e.name.toLowerCase().includes(filter.toLowerCase())))
|
||||
);
|
||||
|
||||
async function loadDir(dirPath: string) {
|
||||
loading = true;
|
||||
error = '';
|
||||
try {
|
||||
const expandedPath = dirPath.replace(/^~/, process.env.HOME ?? '/home');
|
||||
const result = await appRpc.request['files.list']({ path: expandedPath });
|
||||
if (result?.error) {
|
||||
error = result.error;
|
||||
entries = [];
|
||||
} else {
|
||||
entries = result?.entries ?? [];
|
||||
}
|
||||
currentPath = dirPath;
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : String(err);
|
||||
entries = [];
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function navigateTo(dirPath: string) {
|
||||
filter = '';
|
||||
loadDir(dirPath);
|
||||
}
|
||||
|
||||
function selectFolder(name: string) {
|
||||
const sep = currentPath.endsWith('/') ? '' : '/';
|
||||
const newPath = currentPath + sep + name;
|
||||
navigateTo(newPath);
|
||||
}
|
||||
|
||||
function confirmSelect() {
|
||||
onSelect(currentPath);
|
||||
}
|
||||
|
||||
// Load initial directory
|
||||
$effect(() => {
|
||||
loadDir('~');
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="path-browser">
|
||||
<!-- Header -->
|
||||
<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>
|
||||
|
||||
<!-- Shortcuts -->
|
||||
<div class="pb-shortcuts">
|
||||
{#each SHORTCUTS as sc}
|
||||
<button class="pb-shortcut" onclick={() => navigateTo(sc.path)}>{sc.label}</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Breadcrumbs -->
|
||||
<div class="pb-breadcrumbs">
|
||||
{#each breadcrumbs() as crumb, i}
|
||||
{#if i > 0}<span class="pb-sep">/</span>{/if}
|
||||
<button class="pb-crumb" onclick={() => navigateTo(crumb.path)}>{crumb.label}</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Filter -->
|
||||
<input
|
||||
class="pb-filter"
|
||||
type="text"
|
||||
placeholder="Filter..."
|
||||
bind:value={filter}
|
||||
/>
|
||||
|
||||
<!-- Directory list -->
|
||||
<div class="pb-list">
|
||||
{#if loading}
|
||||
<div class="pb-loading">Loading...</div>
|
||||
{:else if error}
|
||||
<div class="pb-error">{error}</div>
|
||||
{:else if filteredEntries.length === 0}
|
||||
<div class="pb-empty">No directories</div>
|
||||
{:else}
|
||||
{#each filteredEntries as entry}
|
||||
<button class="pb-entry" onclick={() => selectFolder(entry.name)}>
|
||||
<span class="pb-icon">📁</span>
|
||||
<span class="pb-name">{entry.name}</span>
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Footer: current path + select -->
|
||||
<div class="pb-footer">
|
||||
<span class="pb-current" title={currentPath}>{currentPath}</span>
|
||||
<button class="pb-select-btn" onclick={confirmSelect}>Select</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.path-browser {
|
||||
position: absolute;
|
||||
top: 2.5rem;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
background: var(--ctp-mantle);
|
||||
border: 1px solid var(--ctp-surface1);
|
||||
border-radius: 0.375rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 20rem;
|
||||
box-shadow: 0 0.5rem 1.5rem color-mix(in srgb, var(--ctp-crust) 60%, transparent);
|
||||
}
|
||||
|
||||
.pb-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.375rem 0.5rem;
|
||||
border-bottom: 1px solid var(--ctp-surface0);
|
||||
}
|
||||
|
||||
.pb-title { font-size: 0.75rem; font-weight: 600; color: var(--ctp-text); }
|
||||
|
||||
.pb-close {
|
||||
background: none; border: none; color: var(--ctp-overlay1); cursor: pointer;
|
||||
font-size: 0.75rem; padding: 0.125rem 0.25rem; border-radius: 0.25rem;
|
||||
}
|
||||
.pb-close:hover { color: var(--ctp-text); background: var(--ctp-surface0); }
|
||||
|
||||
.pb-shortcuts {
|
||||
display: flex; gap: 0.25rem; padding: 0.25rem 0.5rem;
|
||||
border-bottom: 1px solid var(--ctp-surface0);
|
||||
}
|
||||
|
||||
.pb-shortcut {
|
||||
padding: 0.125rem 0.375rem; font-size: 0.625rem; border-radius: 0.25rem;
|
||||
background: var(--ctp-surface0); border: none; color: var(--ctp-subtext0);
|
||||
cursor: pointer; font-family: var(--ui-font-family);
|
||||
}
|
||||
.pb-shortcut:hover { background: var(--ctp-surface1); color: var(--ctp-text); }
|
||||
|
||||
.pb-breadcrumbs {
|
||||
display: flex; align-items: center; gap: 0.125rem;
|
||||
padding: 0.25rem 0.5rem; flex-wrap: wrap;
|
||||
font-size: 0.6875rem; color: var(--ctp-subtext0);
|
||||
}
|
||||
|
||||
.pb-sep { color: var(--ctp-overlay0); }
|
||||
|
||||
.pb-crumb {
|
||||
background: none; border: none; color: var(--ctp-blue); cursor: pointer;
|
||||
font-size: 0.6875rem; padding: 0; font-family: var(--ui-font-family);
|
||||
}
|
||||
.pb-crumb:hover { text-decoration: underline; }
|
||||
|
||||
.pb-filter {
|
||||
margin: 0.25rem 0.5rem; padding: 0.25rem 0.375rem;
|
||||
background: var(--ctp-surface0); border: 1px solid var(--ctp-surface1);
|
||||
border-radius: 0.25rem; color: var(--ctp-text);
|
||||
font-size: 0.75rem; font-family: var(--ui-font-family);
|
||||
}
|
||||
.pb-filter:focus { outline: none; border-color: var(--ctp-blue); }
|
||||
.pb-filter::placeholder { color: var(--ctp-overlay0); }
|
||||
|
||||
.pb-list {
|
||||
flex: 1; min-height: 0; overflow-y: auto;
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
.pb-list::-webkit-scrollbar { width: 0.25rem; }
|
||||
.pb-list::-webkit-scrollbar-thumb { background: var(--ctp-surface1); border-radius: 0.125rem; }
|
||||
|
||||
.pb-entry {
|
||||
display: flex; align-items: center; gap: 0.375rem;
|
||||
width: 100%; padding: 0.25rem 0.5rem;
|
||||
background: none; border: none; color: var(--ctp-text);
|
||||
cursor: pointer; font-size: 0.75rem; text-align: left;
|
||||
font-family: var(--ui-font-family);
|
||||
}
|
||||
.pb-entry:hover { background: var(--ctp-surface0); }
|
||||
|
||||
.pb-icon { font-size: 0.875rem; flex-shrink: 0; }
|
||||
.pb-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
.pb-loading, .pb-error, .pb-empty {
|
||||
padding: 1rem; text-align: center; font-size: 0.75rem;
|
||||
color: var(--ctp-overlay0);
|
||||
}
|
||||
.pb-error { color: var(--ctp-red); }
|
||||
|
||||
.pb-footer {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 0.375rem 0.5rem; border-top: 1px solid var(--ctp-surface0);
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.pb-current {
|
||||
font-size: 0.6875rem; color: var(--ctp-subtext0);
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
flex: 1; min-width: 0;
|
||||
}
|
||||
|
||||
.pb-select-btn {
|
||||
padding: 0.25rem 0.625rem; font-size: 0.75rem; border-radius: 0.25rem;
|
||||
background: color-mix(in srgb, var(--ctp-blue) 20%, transparent);
|
||||
border: 1px solid var(--ctp-blue); color: var(--ctp-blue);
|
||||
cursor: pointer; font-family: var(--ui-font-family); flex-shrink: 0;
|
||||
}
|
||||
.pb-select-btn:hover { background: color-mix(in srgb, var(--ctp-blue) 35%, transparent); }
|
||||
</style>
|
||||
626
ui-electrobun/src/mainview/ProjectWizard.svelte
Normal file
626
ui-electrobun/src/mainview/ProjectWizard.svelte
Normal file
|
|
@ -0,0 +1,626 @@
|
|||
<script lang="ts">
|
||||
import { t } from './i18n.svelte.ts';
|
||||
import { appRpc } from './rpc.ts';
|
||||
import PathBrowser from './PathBrowser.svelte';
|
||||
|
||||
interface ProjectResult {
|
||||
id: string;
|
||||
name: string;
|
||||
cwd: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
systemPrompt?: string;
|
||||
autoStart?: boolean;
|
||||
groupId?: string;
|
||||
useWorktrees?: boolean;
|
||||
shell?: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
onCreated: (project: ProjectResult) => void;
|
||||
groupId: string;
|
||||
groups: Array<{ id: string; name: string }>;
|
||||
}
|
||||
|
||||
let { onClose, onCreated, groupId, groups }: Props = $props();
|
||||
|
||||
// ── Wizard step ─────────────────────────────────────────────
|
||||
type SourceType = 'local' | 'git-clone' | 'github' | 'template' | 'remote';
|
||||
|
||||
let step = $state(1);
|
||||
|
||||
// ── Step 1: Source ──────────────────────────────────────────
|
||||
let sourceType = $state<SourceType>('local');
|
||||
let localPath = $state('');
|
||||
let repoUrl = $state('');
|
||||
let cloneTarget = $state('');
|
||||
let githubRepo = $state('');
|
||||
let selectedTemplate = $state('');
|
||||
let remoteHost = $state('');
|
||||
let remoteUser = $state('');
|
||||
let remotePath = $state('');
|
||||
|
||||
// Path validation
|
||||
let pathValid = $state<'idle' | 'checking' | 'valid' | 'invalid' | 'not-dir'>('idle');
|
||||
let isGitRepo = $state(false);
|
||||
let gitBranch = $state('');
|
||||
let showBrowser = $state(false);
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
// ── Step 2: Configure ──────────────────────────────────────
|
||||
let projectName = $state('');
|
||||
let selectedBranch = $state('');
|
||||
let branches = $state<string[]>([]);
|
||||
let useWorktrees = $state(false);
|
||||
let selectedGroupId = $state(groupId);
|
||||
let projectIcon = $state('📁');
|
||||
let shellChoice = $state('bash');
|
||||
|
||||
const SHELLS = ['bash', 'zsh', 'fish', 'sh'];
|
||||
const EMOJI_GRID = ['📁', '🚀', '⚡', '🔧', '🌐', '📦', '🎯', '💡', '🔬', '🎨', '📊', '🛡️', '🤖', '🧪', '🏗️', '📝'];
|
||||
|
||||
// ── Step 3: Agent ──────────────────────────────────────────
|
||||
let provider = $state<'claude' | 'codex' | 'ollama'>('claude');
|
||||
let model = $state('');
|
||||
let permissionMode = $state('default');
|
||||
let systemPrompt = $state('');
|
||||
let autoStart = $state(false);
|
||||
let cloning = $state(false);
|
||||
|
||||
// ── Templates ───────────────────────────────────────────────
|
||||
let templates = $state<Array<{ id: string; name: string; description: string; icon: string }>>([]);
|
||||
$effect(() => {
|
||||
if (sourceType === 'template' && templates.length === 0) {
|
||||
appRpc.request['project.templates']({}).then(r => {
|
||||
if (r?.templates) templates = r.templates;
|
||||
}).catch(console.error);
|
||||
}
|
||||
});
|
||||
|
||||
// ── Path validation (debounced 300ms) ──────────────────────
|
||||
function validatePath(p: string) {
|
||||
if (debounceTimer) clearTimeout(debounceTimer);
|
||||
if (!p.trim()) { pathValid = 'idle'; isGitRepo = false; return; }
|
||||
pathValid = 'checking';
|
||||
debounceTimer = setTimeout(async () => {
|
||||
try {
|
||||
const result = await appRpc.request['files.statEx']({ path: p });
|
||||
if (!result?.exists) {
|
||||
pathValid = 'invalid';
|
||||
isGitRepo = false;
|
||||
} else if (!result.isDirectory) {
|
||||
pathValid = 'not-dir';
|
||||
isGitRepo = false;
|
||||
} else {
|
||||
pathValid = 'valid';
|
||||
isGitRepo = result.isGitRepo;
|
||||
gitBranch = result.gitBranch ?? '';
|
||||
// Auto-populate name from folder name
|
||||
if (!projectName) {
|
||||
const parts = p.replace(/\/+$/, '').split('/');
|
||||
projectName = parts[parts.length - 1] || '';
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
pathValid = 'invalid';
|
||||
isGitRepo = false;
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (sourceType === 'local') validatePath(localPath);
|
||||
});
|
||||
|
||||
// ── Branch loading ─────────────────────────────────────────
|
||||
async function loadBranches(repoPath: string) {
|
||||
try {
|
||||
const result = await appRpc.request['git.branches']({ path: repoPath });
|
||||
if (result?.branches) {
|
||||
branches = result.branches;
|
||||
selectedBranch = result.current || '';
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// ── Step navigation ────────────────────────────────────────
|
||||
let step1Valid = $derived(() => {
|
||||
switch (sourceType) {
|
||||
case 'local': return pathValid === 'valid';
|
||||
case 'git-clone': return repoUrl.trim().length > 0 && cloneTarget.trim().length > 0;
|
||||
case 'github': return /^[\w.-]+\/[\w.-]+$/.test(githubRepo.trim());
|
||||
case 'template': return selectedTemplate !== '';
|
||||
case 'remote': return remoteHost.trim().length > 0 && remoteUser.trim().length > 0 && remotePath.trim().length > 0;
|
||||
default: return false;
|
||||
}
|
||||
});
|
||||
|
||||
async function goToStep2() {
|
||||
if (sourceType === 'git-clone') {
|
||||
cloning = true;
|
||||
try {
|
||||
const result = await appRpc.request['git.clone']({
|
||||
url: repoUrl.trim(),
|
||||
target: cloneTarget.trim(),
|
||||
});
|
||||
if (!result?.ok) {
|
||||
cloning = false;
|
||||
return; // stay on step 1
|
||||
}
|
||||
localPath = cloneTarget.trim();
|
||||
isGitRepo = true;
|
||||
} catch {
|
||||
cloning = false;
|
||||
return;
|
||||
}
|
||||
cloning = false;
|
||||
} else if (sourceType === 'github') {
|
||||
const url = `https://github.com/${githubRepo.trim()}.git`;
|
||||
const target = `~/projects/${githubRepo.trim().split('/')[1] || 'project'}`;
|
||||
cloning = true;
|
||||
try {
|
||||
const result = await appRpc.request['git.clone']({ url, target });
|
||||
if (!result?.ok) { cloning = false; return; }
|
||||
localPath = target;
|
||||
isGitRepo = true;
|
||||
} catch { cloning = false; return; }
|
||||
cloning = false;
|
||||
}
|
||||
|
||||
// Auto-populate name
|
||||
if (!projectName && localPath) {
|
||||
const parts = localPath.replace(/\/+$/, '').split('/');
|
||||
projectName = parts[parts.length - 1] || '';
|
||||
}
|
||||
|
||||
// Load branches if git repo
|
||||
if (isGitRepo && localPath) {
|
||||
await loadBranches(localPath);
|
||||
}
|
||||
|
||||
step = 2;
|
||||
}
|
||||
|
||||
function goToStep3() { step = 3; }
|
||||
function goBack() { step = Math.max(1, step - 1); }
|
||||
|
||||
async function createProject() {
|
||||
const cwd = sourceType === 'remote'
|
||||
? `ssh://${remoteUser}@${remoteHost}:${remotePath}`
|
||||
: localPath.trim();
|
||||
|
||||
const project: ProjectResult = {
|
||||
id: `p-${Date.now()}`,
|
||||
name: projectName.trim() || 'Untitled',
|
||||
cwd,
|
||||
provider,
|
||||
model: model || undefined,
|
||||
systemPrompt: systemPrompt || undefined,
|
||||
autoStart,
|
||||
groupId: selectedGroupId,
|
||||
useWorktrees: useWorktrees || undefined,
|
||||
shell: shellChoice,
|
||||
icon: projectIcon,
|
||||
};
|
||||
|
||||
onCreated(project);
|
||||
}
|
||||
|
||||
function handleBrowserSelect(path: string) {
|
||||
localPath = path;
|
||||
showBrowser = false;
|
||||
validatePath(path);
|
||||
}
|
||||
|
||||
// Validation indicator
|
||||
function validationIcon(state: typeof pathValid): string {
|
||||
switch (state) {
|
||||
case 'valid': return '✓';
|
||||
case 'invalid': return '✗';
|
||||
case 'not-dir': return '⚠';
|
||||
case 'checking': return '…';
|
||||
default: return '';
|
||||
}
|
||||
}
|
||||
|
||||
function validationColor(state: typeof pathValid): string {
|
||||
switch (state) {
|
||||
case 'valid': return 'var(--ctp-green)';
|
||||
case 'invalid': return 'var(--ctp-red)';
|
||||
case 'not-dir': return 'var(--ctp-peach)';
|
||||
default: return 'var(--ctp-overlay0)';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="wizard-overlay" role="dialog" aria-label={t('wizard.title' as any)}>
|
||||
<div class="wizard-panel">
|
||||
<!-- Header -->
|
||||
<div class="wz-header">
|
||||
<h2 class="wz-title">{t('wizard.title' as any)}</h2>
|
||||
<div class="wz-steps">
|
||||
{#each [1, 2, 3] as s}
|
||||
<span class="wz-step-dot" class:active={step === s} class:done={step > s}>{s}</span>
|
||||
{/each}
|
||||
</div>
|
||||
<button class="wz-close" onclick={onClose} aria-label={t('common.close' as any)}>✕</button>
|
||||
</div>
|
||||
|
||||
<!-- Step 1: Source -->
|
||||
<div class="wz-body" style:display={step === 1 ? 'flex' : 'none'}>
|
||||
<h3 class="wz-step-title">{t('wizard.step1.title' as any)}</h3>
|
||||
|
||||
<!-- Source type radios -->
|
||||
<div class="wz-radios">
|
||||
{#each [
|
||||
{ value: 'local', label: t('wizard.step1.local' as any), icon: '📁' },
|
||||
{ value: 'git-clone', label: t('wizard.step1.gitClone' as any), icon: '🔀' },
|
||||
{ value: 'github', label: t('wizard.step1.github' as any), icon: '🐙' },
|
||||
{ value: 'template', label: t('wizard.step1.template' as any), icon: '📋' },
|
||||
{ value: 'remote', label: t('wizard.step1.remote' as any), icon: '🖥️' },
|
||||
] as opt}
|
||||
<label class="wz-radio" class:selected={sourceType === opt.value}>
|
||||
<input type="radio" name="source" value={opt.value}
|
||||
checked={sourceType === opt.value}
|
||||
onchange={() => sourceType = opt.value as SourceType} />
|
||||
<span class="wz-radio-icon">{opt.icon}</span>
|
||||
<span>{opt.label}</span>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Source-specific inputs -->
|
||||
<div class="wz-source-fields">
|
||||
{#if sourceType === 'local'}
|
||||
<label class="wz-label">{t('wizard.step1.pathLabel' as any)}</label>
|
||||
<div class="wz-path-row" style="position: relative;">
|
||||
<input class="wz-input" type="text"
|
||||
placeholder={t('wizard.step1.pathPlaceholder' as any)}
|
||||
bind:value={localPath}
|
||||
oninput={() => validatePath(localPath)} />
|
||||
<button class="wz-browse-btn" onclick={() => showBrowser = !showBrowser}
|
||||
aria-label={t('wizard.step1.browse' as any)}>🔍</button>
|
||||
{#if pathValid !== 'idle'}
|
||||
<span class="wz-validation" style:color={validationColor(pathValid)}>
|
||||
{validationIcon(pathValid)}
|
||||
</span>
|
||||
{/if}
|
||||
{#if showBrowser}
|
||||
<PathBrowser onSelect={handleBrowserSelect} onClose={() => showBrowser = false} />
|
||||
{/if}
|
||||
</div>
|
||||
{#if pathValid === 'valid' && isGitRepo}
|
||||
<span class="wz-badge git-badge">{t('wizard.step1.gitDetected' as any)} ({gitBranch})</span>
|
||||
{/if}
|
||||
{#if pathValid === 'invalid'}
|
||||
<span class="wz-hint error">{t('wizard.step1.invalidPath' as any)}</span>
|
||||
{/if}
|
||||
{#if pathValid === 'not-dir'}
|
||||
<span class="wz-hint warn">{t('wizard.step1.notDir' as any)}</span>
|
||||
{/if}
|
||||
|
||||
{:else if sourceType === 'git-clone'}
|
||||
<label class="wz-label">{t('wizard.step1.repoUrl' as any)}</label>
|
||||
<input class="wz-input" type="text" placeholder="https://github.com/user/repo.git" bind:value={repoUrl} />
|
||||
<label class="wz-label">{t('wizard.step1.targetDir' as any)}</label>
|
||||
<input class="wz-input" type="text" placeholder="~/projects/my-repo" bind:value={cloneTarget} />
|
||||
|
||||
{:else if sourceType === 'github'}
|
||||
<label class="wz-label">{t('wizard.step1.githubRepo' as any)}</label>
|
||||
<input class="wz-input" type="text" placeholder="owner/repo" bind:value={githubRepo} />
|
||||
|
||||
{:else if sourceType === 'template'}
|
||||
<div class="wz-template-grid">
|
||||
{#each templates as tmpl}
|
||||
<button class="wz-template-card" class:selected={selectedTemplate === tmpl.id}
|
||||
onclick={() => selectedTemplate = tmpl.id}>
|
||||
<span class="wz-template-icon">{tmpl.icon}</span>
|
||||
<span class="wz-template-name">{tmpl.name}</span>
|
||||
<span class="wz-template-desc">{tmpl.description}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{:else if sourceType === 'remote'}
|
||||
<label class="wz-label">{t('wizard.step1.hostLabel' as any)}</label>
|
||||
<input class="wz-input" type="text" placeholder="192.168.1.100" bind:value={remoteHost} />
|
||||
<label class="wz-label">{t('wizard.step1.userLabel' as any)}</label>
|
||||
<input class="wz-input" type="text" placeholder="user" bind:value={remoteUser} />
|
||||
<label class="wz-label">{t('wizard.step1.pathLabel' as any)}</label>
|
||||
<input class="wz-input" type="text" placeholder="/home/user/project" bind:value={remotePath} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Step 1 footer -->
|
||||
<div class="wz-footer">
|
||||
<button class="wz-btn secondary" onclick={onClose}>{t('common.cancel' as any)}</button>
|
||||
<div class="wz-footer-right">
|
||||
{#if cloning}
|
||||
<span class="wz-cloning">{t('wizard.cloning' as any)}</span>
|
||||
{/if}
|
||||
<button class="wz-btn primary" disabled={!step1Valid() || cloning} onclick={goToStep2}>
|
||||
{t('wizard.next' as any)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Configure -->
|
||||
<div class="wz-body" style:display={step === 2 ? 'flex' : 'none'}>
|
||||
<h3 class="wz-step-title">{t('wizard.step2.title' as any)}</h3>
|
||||
|
||||
<label class="wz-label">{t('wizard.step2.name' as any)}</label>
|
||||
<input class="wz-input" type="text" bind:value={projectName} placeholder="my-project" />
|
||||
|
||||
{#if isGitRepo && branches.length > 0}
|
||||
<label class="wz-label">{t('wizard.step2.branch' as any)}</label>
|
||||
<select class="wz-select" bind:value={selectedBranch}>
|
||||
{#each branches as br}
|
||||
<option value={br}>{br}</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
<label class="wz-toggle-row">
|
||||
<input type="checkbox" bind:checked={useWorktrees} />
|
||||
<span>{t('wizard.step2.worktree' as any)}</span>
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
<label class="wz-label">{t('wizard.step2.group' as any)}</label>
|
||||
<select class="wz-select" bind:value={selectedGroupId}>
|
||||
{#each groups as g}
|
||||
<option value={g.id}>{g.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
<label class="wz-label">{t('wizard.step2.icon' as any)}</label>
|
||||
<div class="wz-emoji-grid">
|
||||
{#each EMOJI_GRID as emoji}
|
||||
<button class="wz-emoji" class:selected={projectIcon === emoji}
|
||||
onclick={() => projectIcon = emoji}>{emoji}</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<label class="wz-label">{t('wizard.step2.shell' as any)}</label>
|
||||
<select class="wz-select" bind:value={shellChoice}>
|
||||
{#each SHELLS as sh}
|
||||
<option value={sh}>{sh}</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
<div class="wz-footer">
|
||||
<button class="wz-btn secondary" onclick={goBack}>{t('wizard.back' as any)}</button>
|
||||
<button class="wz-btn primary" onclick={goToStep3}>{t('wizard.next' as any)}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Agent -->
|
||||
<div class="wz-body" style:display={step === 3 ? 'flex' : 'none'}>
|
||||
<h3 class="wz-step-title">{t('wizard.step3.title' as any)}</h3>
|
||||
|
||||
<label class="wz-label">{t('wizard.step3.provider' as any)}</label>
|
||||
<div class="wz-segmented">
|
||||
{#each (['claude', 'codex', 'ollama'] as const) as prov}
|
||||
<button class="wz-seg-btn" class:active={provider === prov}
|
||||
onclick={() => provider = prov}>{prov}</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<label class="wz-label">{t('wizard.step3.model' as any)}</label>
|
||||
<input class="wz-input" type="text" bind:value={model}
|
||||
placeholder={provider === 'claude' ? 'claude-sonnet-4-20250514' : provider === 'codex' ? 'gpt-5.4' : 'qwen3:8b'} />
|
||||
|
||||
<label class="wz-label">{t('wizard.step3.permission' as any)}</label>
|
||||
<div class="wz-segmented">
|
||||
{#each ['restricted', 'default', 'bypassPermissions'] as pm}
|
||||
<button class="wz-seg-btn" class:active={permissionMode === pm}
|
||||
onclick={() => permissionMode = pm}>{pm}</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<label class="wz-label">{t('wizard.step3.systemPrompt' as any)}</label>
|
||||
<textarea class="wz-textarea" bind:value={systemPrompt} rows="3"
|
||||
placeholder="Optional system instructions..."></textarea>
|
||||
|
||||
<label class="wz-toggle-row">
|
||||
<input type="checkbox" bind:checked={autoStart} />
|
||||
<span>{t('wizard.step3.autoStart' as any)}</span>
|
||||
</label>
|
||||
|
||||
<div class="wz-footer">
|
||||
<button class="wz-btn secondary" onclick={goBack}>{t('wizard.back' as any)}</button>
|
||||
<div class="wz-footer-right">
|
||||
<button class="wz-btn ghost" onclick={createProject}>{t('wizard.skip' as any)}</button>
|
||||
<button class="wz-btn primary" onclick={createProject}>{t('wizard.create' as any)}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wizard-overlay {
|
||||
position: fixed; inset: 0; z-index: 100;
|
||||
background: color-mix(in srgb, var(--ctp-crust) 70%, transparent);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
|
||||
.wizard-panel {
|
||||
width: min(32rem, 90vw);
|
||||
max-height: 85vh;
|
||||
background: var(--ctp-base);
|
||||
border: 1px solid var(--ctp-surface1);
|
||||
border-radius: 0.75rem;
|
||||
display: flex; flex-direction: column;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1rem 3rem color-mix(in srgb, var(--ctp-crust) 50%, transparent);
|
||||
}
|
||||
|
||||
.wz-header {
|
||||
display: flex; align-items: center; gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--ctp-surface0);
|
||||
}
|
||||
|
||||
.wz-title { font-size: 1rem; font-weight: 700; color: var(--ctp-text); margin: 0; flex: 1; }
|
||||
|
||||
.wz-steps { display: flex; gap: 0.375rem; }
|
||||
|
||||
.wz-step-dot {
|
||||
width: 1.5rem; height: 1.5rem; border-radius: 50%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 0.6875rem; font-weight: 600;
|
||||
background: var(--ctp-surface0); color: var(--ctp-subtext0);
|
||||
border: 1.5px solid var(--ctp-surface1);
|
||||
}
|
||||
.wz-step-dot.active { background: color-mix(in srgb, var(--ctp-blue) 20%, transparent); border-color: var(--ctp-blue); color: var(--ctp-blue); }
|
||||
.wz-step-dot.done { background: color-mix(in srgb, var(--ctp-green) 20%, transparent); border-color: var(--ctp-green); color: var(--ctp-green); }
|
||||
|
||||
.wz-close {
|
||||
background: none; border: none; color: var(--ctp-overlay1); cursor: pointer;
|
||||
font-size: 0.875rem; padding: 0.25rem; border-radius: 0.25rem;
|
||||
}
|
||||
.wz-close:hover { color: var(--ctp-text); background: var(--ctp-surface0); }
|
||||
|
||||
.wz-body {
|
||||
flex: 1; min-height: 0; overflow-y: auto;
|
||||
flex-direction: column; gap: 0.5rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.wz-step-title { font-size: 0.875rem; font-weight: 600; color: var(--ctp-text); margin: 0 0 0.25rem; }
|
||||
|
||||
.wz-label { font-size: 0.75rem; font-weight: 500; color: var(--ctp-subtext0); margin-top: 0.25rem; }
|
||||
|
||||
.wz-input, .wz-select, .wz-textarea {
|
||||
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);
|
||||
width: 100%;
|
||||
}
|
||||
.wz-input:focus, .wz-select:focus, .wz-textarea:focus { outline: none; border-color: var(--ctp-blue); }
|
||||
.wz-input::placeholder, .wz-textarea::placeholder { color: var(--ctp-overlay0); }
|
||||
.wz-textarea { resize: vertical; min-height: 3rem; }
|
||||
.wz-select { cursor: pointer; }
|
||||
|
||||
.wz-radios { display: flex; flex-wrap: wrap; gap: 0.375rem; }
|
||||
|
||||
.wz-radio {
|
||||
display: flex; align-items: center; gap: 0.375rem;
|
||||
padding: 0.375rem 0.625rem; border-radius: 0.375rem;
|
||||
border: 1px solid var(--ctp-surface1); cursor: pointer;
|
||||
font-size: 0.75rem; color: var(--ctp-subtext0);
|
||||
transition: border-color 0.12s, background 0.12s;
|
||||
}
|
||||
.wz-radio:hover { border-color: var(--ctp-overlay1); color: var(--ctp-text); }
|
||||
.wz-radio.selected { border-color: var(--ctp-blue); background: color-mix(in srgb, var(--ctp-blue) 10%, transparent); color: var(--ctp-text); }
|
||||
.wz-radio input[type="radio"] { display: none; }
|
||||
.wz-radio-icon { font-size: 1rem; }
|
||||
|
||||
.wz-path-row { display: flex; gap: 0.375rem; align-items: center; }
|
||||
.wz-path-row .wz-input { flex: 1; }
|
||||
|
||||
.wz-browse-btn {
|
||||
padding: 0.375rem; background: var(--ctp-surface0);
|
||||
border: 1px solid var(--ctp-surface1); border-radius: 0.25rem;
|
||||
cursor: pointer; font-size: 0.875rem; flex-shrink: 0;
|
||||
}
|
||||
.wz-browse-btn:hover { background: var(--ctp-surface1); }
|
||||
|
||||
.wz-validation { font-size: 0.875rem; font-weight: 700; flex-shrink: 0; }
|
||||
|
||||
.wz-badge {
|
||||
display: inline-flex; align-items: center; gap: 0.25rem;
|
||||
padding: 0.125rem 0.5rem; border-radius: 0.25rem;
|
||||
font-size: 0.6875rem; width: fit-content;
|
||||
}
|
||||
.git-badge {
|
||||
background: color-mix(in srgb, var(--ctp-green) 15%, transparent);
|
||||
color: var(--ctp-green); border: 1px solid color-mix(in srgb, var(--ctp-green) 30%, transparent);
|
||||
}
|
||||
|
||||
.wz-hint { font-size: 0.6875rem; }
|
||||
.wz-hint.error { color: var(--ctp-red); }
|
||||
.wz-hint.warn { color: var(--ctp-peach); }
|
||||
|
||||
.wz-source-fields { display: flex; flex-direction: column; gap: 0.375rem; margin-top: 0.25rem; }
|
||||
|
||||
.wz-template-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.375rem; }
|
||||
|
||||
.wz-template-card {
|
||||
display: flex; flex-direction: column; align-items: center; gap: 0.25rem;
|
||||
padding: 0.75rem 0.5rem; border-radius: 0.375rem;
|
||||
border: 1px solid var(--ctp-surface1); background: var(--ctp-surface0);
|
||||
cursor: pointer; text-align: center;
|
||||
}
|
||||
.wz-template-card:hover { border-color: var(--ctp-overlay1); }
|
||||
.wz-template-card.selected { border-color: var(--ctp-blue); background: color-mix(in srgb, var(--ctp-blue) 10%, transparent); }
|
||||
.wz-template-icon { font-size: 1.5rem; }
|
||||
.wz-template-name { font-size: 0.75rem; font-weight: 600; color: var(--ctp-text); }
|
||||
.wz-template-desc { font-size: 0.625rem; color: var(--ctp-subtext0); }
|
||||
|
||||
.wz-emoji-grid { display: flex; flex-wrap: wrap; gap: 0.25rem; }
|
||||
|
||||
.wz-emoji {
|
||||
width: 2rem; height: 2rem; border-radius: 0.25rem;
|
||||
border: 1px solid var(--ctp-surface1); background: var(--ctp-surface0);
|
||||
cursor: pointer; font-size: 1rem;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.wz-emoji:hover { border-color: var(--ctp-overlay1); }
|
||||
.wz-emoji.selected { border-color: var(--ctp-blue); background: color-mix(in srgb, var(--ctp-blue) 15%, 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-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-footer {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding-top: 0.75rem; margin-top: auto;
|
||||
border-top: 1px solid var(--ctp-surface0);
|
||||
}
|
||||
|
||||
.wz-footer-right { display: flex; gap: 0.375rem; align-items: center; }
|
||||
|
||||
.wz-cloning { font-size: 0.75rem; color: var(--ctp-blue); font-style: italic; }
|
||||
|
||||
.wz-btn {
|
||||
padding: 0.375rem 0.75rem; border-radius: 0.25rem;
|
||||
font-size: 0.8125rem; font-weight: 500; cursor: pointer;
|
||||
font-family: var(--ui-font-family); border: 1px solid transparent;
|
||||
}
|
||||
.wz-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
|
||||
.wz-btn.primary {
|
||||
background: color-mix(in srgb, var(--ctp-blue) 20%, transparent);
|
||||
border-color: var(--ctp-blue); color: var(--ctp-blue);
|
||||
}
|
||||
.wz-btn.primary:hover:not(:disabled) { background: color-mix(in srgb, var(--ctp-blue) 35%, transparent); }
|
||||
|
||||
.wz-btn.secondary {
|
||||
background: transparent; border-color: var(--ctp-surface1); color: var(--ctp-subtext0);
|
||||
}
|
||||
.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>
|
||||
|
|
@ -137,4 +137,42 @@ export type TranslationKey =
|
|||
| 'terminal.closeTab'
|
||||
| 'terminal.collapse'
|
||||
| 'terminal.expand'
|
||||
| 'terminal.shell';
|
||||
| 'terminal.shell'
|
||||
| 'wizard.back'
|
||||
| 'wizard.cloning'
|
||||
| 'wizard.create'
|
||||
| 'wizard.next'
|
||||
| 'wizard.skip'
|
||||
| 'wizard.step1.browse'
|
||||
| 'wizard.step1.gitClone'
|
||||
| 'wizard.step1.gitDetected'
|
||||
| 'wizard.step1.github'
|
||||
| 'wizard.step1.githubRepo'
|
||||
| 'wizard.step1.hostLabel'
|
||||
| 'wizard.step1.invalidPath'
|
||||
| 'wizard.step1.local'
|
||||
| 'wizard.step1.notDir'
|
||||
| 'wizard.step1.pathLabel'
|
||||
| 'wizard.step1.pathPlaceholder'
|
||||
| 'wizard.step1.remote'
|
||||
| 'wizard.step1.repoUrl'
|
||||
| 'wizard.step1.targetDir'
|
||||
| 'wizard.step1.template'
|
||||
| 'wizard.step1.title'
|
||||
| 'wizard.step1.userLabel'
|
||||
| 'wizard.step1.validDir'
|
||||
| 'wizard.step2.branch'
|
||||
| 'wizard.step2.group'
|
||||
| 'wizard.step2.icon'
|
||||
| 'wizard.step2.name'
|
||||
| 'wizard.step2.newGroup'
|
||||
| 'wizard.step2.shell'
|
||||
| 'wizard.step2.title'
|
||||
| 'wizard.step2.worktree'
|
||||
| 'wizard.step3.autoStart'
|
||||
| 'wizard.step3.model'
|
||||
| 'wizard.step3.permission'
|
||||
| 'wizard.step3.provider'
|
||||
| 'wizard.step3.systemPrompt'
|
||||
| 'wizard.step3.title'
|
||||
| 'wizard.title';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue