Files
CodePilot/src/components/setup/ProjectDirCard.tsx
T
7418andClaude Opus 4.6 8154130035 feat: add Setup Center onboarding and move commit/push to Git panel
Add a skippable first-use Setup Center that guides users through three
prerequisites: Claude Code CLI detection, API provider configuration,
and default project directory selection. Replace passive fail-then-hint
patterns with proactive guidance and actionable error recovery.

Setup Center (13 new files):
- SetupCenter overlay with 4 cards (Welcome, ClaudeCode, Provider, ProjectDir)
- Auto-close only when user completes last card in current session
- Re-openable from Settings > General > "首次设置引导"
- Claude Code conflict detection with per-install-type uninstall commands
- Provider detection covers DB providers, env vars, and legacy app-settings token
- ANTHROPIC_BASE_URL excluded from credential check (not a real credential)
- skipped != completed: /chat still prompts for provider after skip

Project directory:
- Validation via /api/files/browse before trusting saved paths
- Fallback chain: localStorage → setup_default_project → empty state
- Cross-component sync via project-directory-changed CustomEvent

Toast system:
- Global showToast() API with success/error/warning/info types
- FIFO eviction (max 3), action button support

Git panel:
- Move Commit + Push buttons from UnifiedTopBar into GitStatusSection
- Push enabled for new branches (backend auto-handles -u origin)

Other:
- Windows top-bar safe zone (138px spacer for caption buttons)
- Cross-platform path splitting with /[\\/]/ regex
- Platform-aware file manager name (Finder/Explorer/Files)
- ~80 new i18n keys (en + zh)
- Handover doc: docs/handover/onboarding-setup-center.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 15:19:52 +08:00

129 lines
4.4 KiB
TypeScript

'use client';
import { useState, useEffect, useCallback } from 'react';
import { Button } from '@/components/ui/button';
import { SetupCard } from './SetupCard';
import { FolderPicker } from '@/components/chat/FolderPicker';
import { useTranslation } from '@/hooks/useTranslation';
import { useNativeFolderPicker } from '@/hooks/useNativeFolderPicker';
import type { SetupCardStatus } from '@/types';
interface ProjectDirCardProps {
status: SetupCardStatus;
onStatusChange: (status: SetupCardStatus, value?: string) => void;
defaultProject?: string;
}
export function ProjectDirCard({ status, onStatusChange, defaultProject }: ProjectDirCardProps) {
const { t } = useTranslation();
const { isElectron, openNativePicker } = useNativeFolderPicker();
const [recentProjects, setRecentProjects] = useState<string[]>([]);
const [folderPickerOpen, setFolderPickerOpen] = useState(false);
const [selectedPath, setSelectedPath] = useState<string>(defaultProject || '');
// Sync selectedPath when defaultProject prop arrives asynchronously
/* eslint-disable react-hooks/set-state-in-effect */
useEffect(() => {
if (defaultProject && !selectedPath) {
setSelectedPath(defaultProject);
}
}, [defaultProject, selectedPath]);
/* eslint-enable react-hooks/set-state-in-effect */
useEffect(() => {
fetch('/api/setup/recent-projects')
.then(r => r.ok ? r.json() : { projects: [] })
.then(data => setRecentProjects(data.projects || []))
.catch(() => {});
}, []);
const handleSelect = useCallback(async (path: string) => {
setSelectedPath(path);
onStatusChange('completed', path);
// Persist
localStorage.setItem('codepilot:last-working-directory', path);
// Notify other components (e.g. /chat page) that a project directory was selected
window.dispatchEvent(new CustomEvent('project-directory-changed', { detail: { path } }));
try {
await fetch('/api/setup', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ card: 'project', status: 'completed', value: path }),
});
} catch { /* ignore */ }
}, [onStatusChange]);
const handleBrowse = useCallback(async () => {
if (isElectron) {
const path = await openNativePicker({ title: t('folderPicker.title') });
if (path) handleSelect(path);
} else {
setFolderPickerOpen(true);
}
}, [isElectron, openNativePicker, t, handleSelect]);
const handleSkip = useCallback(async () => {
onStatusChange('skipped');
try {
await fetch('/api/setup', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ card: 'project', status: 'skipped' }),
});
} catch { /* ignore */ }
}, [onStatusChange]);
const description = status === 'completed'
? `${t('setup.project.selected')}: ${selectedPath}`
: t('setup.project.description');
return (
<>
<SetupCard
title={t('setup.project.title')}
description={description}
status={status}
onSkip={status === 'not-configured' ? handleSkip : undefined}
>
{status === 'completed' ? (
<p className="text-xs font-mono truncate">{selectedPath}</p>
) : (
<div className="space-y-3">
{recentProjects.length > 0 && (
<div>
<p className="text-xs text-muted-foreground mb-1.5">{t('setup.project.recentProjects')}</p>
<div className="flex flex-wrap gap-1.5">
{recentProjects.map(p => {
const name = p.split(/[\\/]/).filter(Boolean).pop() || p;
return (
<Button
key={p}
variant="outline"
size="sm"
className="h-6 px-2 text-[11px] font-mono"
onClick={() => handleSelect(p)}
title={p}
>
{name}
</Button>
);
})}
</div>
</div>
)}
<Button size="sm" className="text-xs" onClick={handleBrowse}>
{t('setup.project.selectDirectory')}
</Button>
</div>
)}
</SetupCard>
<FolderPicker
open={folderPickerOpen}
onOpenChange={setFolderPickerOpen}
onSelect={handleSelect}
/>
</>
);
}