Refactor/shared and tasks components (#473)

* refactor: remove unused TasksSettings component

* refactor: migrate TodoList component to a new file with improved structure and normalization logic

* refactor: Move Tooltip and DarkModeToggle to shared/ui

* refactor: Move Tooltip and DarkModeToggle to shared/view/ui

* refactor: move GeminiLogo to llm-logo-provider and update imports

* refactor: remove unused GeminiStatus component

* refactor: move components in src/components/ui to src/shared/view/ui

* refactor: move ErrorBoundary component to main-content/view and update imports

* refactor: move VersionUpgradeModal to its own module

* refactor(wizard): rebuild project creation flow as modular TypeScript components

Replace the monolithic `ProjectCreationWizard.jsx` with a feature-based TS
implementation under `src/components/project-creation-wizard`, while preserving
existing behavior and improving readability, maintainability, and state isolation.

Why:
- The previous wizard mixed API logic, flow state, folder browsing, and UI in one file.
- Refactoring and testing were difficult due to tightly coupled concerns.
- We needed stronger type safety and localized component state.

What changed:
- Deleted:
  - `src/components/ProjectCreationWizard.jsx`
- Added new modular structure:
  - `src/components/project-creation-wizard/index.ts`
  - `src/components/project-creation-wizard/ProjectCreationWizard.tsx`
  - `src/components/project-creation-wizard/types.ts`
  - `src/components/project-creation-wizard/data/workspaceApi.ts`
  - `src/components/project-creation-wizard/hooks/useGithubTokens.ts`
  - `src/components/project-creation-wizard/utils/pathUtils.ts`
  - `src/components/project-creation-wizard/components/*`
    - `WizardProgress`, `WizardFooter`, `ErrorBanner`
    - `StepTypeSelection`, `StepConfiguration`, `StepReview`
    - `WorkspacePathField`, `GithubAuthenticationCard`, `FolderBrowserModal`
- Updated import usage:
  - `src/components/sidebar/view/subcomponents/SidebarModals.tsx`
    now imports from `../../../project-creation-wizard`.

Implementation details:
- Migrated wizard logic to TypeScript using `type` aliases only.
- Kept component prop types colocated in each component file.
- Split responsibilities by feature:
  - container/orchestration in `ProjectCreationWizard.tsx`
  - API/SSE and request parsing in `data/workspaceApi.ts`
  - GitHub token loading/caching behavior in `useGithubTokens`
  - path/URL helpers in `utils/pathUtils.ts`
- Localized UI-only state to child components:
  - folder browser modal state (current path, hidden folders, create-folder input)
  - path suggestion dropdown state with debounced lookup
- Preserved existing UX flows:
  - step navigation and validation
  - existing/new workspace modes
  - optional GitHub clone + auth modes
  - clone progress via SSE
  - folder browsing + folder creation
- Added focused comments for non-obvious logic (debounce, SSE auth constraint, path edge cases).

* refactor(quick-settings): migrate panel to typed feature-based modules

Refactor QuickSettingsPanel from a single JSX component into a modular TypeScript feature structure while preserving behavior and translations.

Highlights:
- Replace legacy src/components/QuickSettingsPanel.jsx with a typed entrypoint (src/components/QuickSettingsPanel.tsx).
- Introduce src/components/quick-settings-panel/ with clear separation of concerns:
  - view/: panel shell, header, handle, section wrappers, toggle rows, and content sections.
  - hooks/: drag interactions and whisper mode persistence.
  - constants.ts and types.ts for shared config and strict local typing.
- Move drag logic into useQuickSettingsDrag with explicit touch/mouse handling, drag threshold detection, click suppression after drag, position clamping, and localStorage persistence.
- Keep user-visible behavior intact:
  - same open/close panel interactions.
  - same mobile/desktop drag behavior and persisted handle position.
  - same quick preference toggles and wiring to useUiPreferences.
  - same hidden whisper section behavior and localStorage/event updates.
- Improve readability and maintainability by extracting repetitive setting rows and section scaffolding into reusable components.
- Add focused comments around non-obvious behavior (drag click suppression, touch scroll lock, hidden whisper section intent).
- Keep files small and reviewable (all new/changed files are under 300 lines).

Validation:
- npm run typecheck
- npm run build

* refactor(quick-settings-panel): restructure QuickSettingsPanel import and create index file

* refactor(shared): move shared ui components to share/view/ui without subfolders

* refactor(LanguageSelector): move LanguageSelector to shared UI components

* refactor(prd-editor): modularize PRD editor with typed feature modules

Break the legacy PRDEditor.jsx monolith into a feature-based TypeScript architecture under src/components/prd-editor while keeping behavior parity and readability.

Key changes:

- Replace PRDEditor.jsx with a typed orchestrator component and a compatibility export bridge at src/components/PRDEditor.tsx.

- Split responsibilities into dedicated hooks: document loading/init, existing PRD registry fetching, save workflow with overwrite detection, and keyboard shortcuts.

- Split UI into focused view components: header, editor/preview body, footer stats, loading state, generate-tasks modal, and overwrite-confirm modal.

- Move filename concerns into utility helpers (sanitize, extension handling, default naming) and centralize template/constants.

- Keep component-local state close to the UI that owns it (workspace controls/modal toggles), while shared workflow state remains in the feature container.

- Reuse the existing MarkdownPreview component for safer markdown rendering instead of ad-hoc HTML conversion.

- Update TaskMasterPanel integration to consume typed PRDEditor directly (remove any-cast) and pass isExisting metadata for correct overwrite behavior.

- Keep all new/changed files below 300 lines and add targeted comments where behavior needs clarification.

Validation:

- npm run typecheck

- npm run build

* refactor(TaskMasterPanel): update PRDEditor import path to match new structure

* refactor(TaskMaster): Remove unused TaskMasterSetupWizard and TaskMasterStatus components

* refactor(TaskDetail): remove unused TaskIndicator import

* refactor(task-master): migrate tasks to a typed feature module

- introduce a new feature-oriented TaskMaster domain under src/components/task-master

- add typed TaskMaster context/provider with explicit project, task, MCP, and loading state handling

- split task UI into focused components (panel, board, toolbar, content, card, detail modal, setup/help modals, banner)

- move task board filtering/sorting/kanban derivation into dedicated hooks and utilities

- relocate CreateTaskModal into the feature module and keep task views modular/readable

- remove legacy monolithic TaskList/TaskDetail/TaskCard files and route main task panel to the new feature panel

- replace contexts/TaskMasterContext.jsx with a typed contexts/TaskMasterContext.ts re-export to the feature context

- update MainContent project sync logic to compare by project name to avoid state churn

- validation: npm run typecheck, npm run build

* refactor(MobileNav): remove unused React import and TaskMasterContext

* refactor(auth): migrate login and setup flows to typed feature module

- Introduce a new feature-based auth module under src/components/auth with clear separation of concerns:\n  - context/AuthContext.tsx for session lifecycle, onboarding status checks, token persistence, and auth actions\n  - view/* components for loading, route guarding, form layout, input fields, and error display\n  - shared auth constants, utility helpers, and type aliases (no interfaces)\n- Convert login and setup UIs to TypeScript and keep form state local to each component for readability and component-level ownership\n- Add explicit API payload typing and safe JSON parsing helpers to improve resilience when backend responses are malformed or incomplete\n- Centralize error fallback handling for auth requests to reduce repeated logic

- Replace legacy auth entrypoints with the new feature module in app wiring:\n  - App now imports AuthProvider and ProtectedRoute from src/components/auth\n  - WebSocketContext, TaskMasterContext, and Onboarding now consume useAuth from the new typed auth context\n- Remove duplicated legacy auth screens (LoginForm.jsx, SetupForm.jsx, ProtectedRoute.jsx)\n- Keep backward compatibility by turning src/contexts/AuthContext.jsx into a thin re-export of the new provider/hook

Result: auth code now follows a feature/domain structure, is fully typed, easier to navigate, and cleaner to extend without touching unrelated UI areas.

* refactor(AppContent): update MobileNav import path and add MobileNav component

* refactor(DiffViewer): rename different diff viewers and place them in different components

* refactor(components): reorganize onboarding/provider auth/sidebar indicator into domain features

- Move onboarding out of root-level components into a dedicated feature module:
  - add src/components/onboarding/view/Onboarding.tsx
  - split onboarding UI into focused subcomponents:
    - OnboardingStepProgress
    - GitConfigurationStep
    - AgentConnectionsStep
    - AgentConnectionCard
  - add onboarding-local types and utils for provider status and validation helpers

- Move multi-provider login modal into a dedicated provider-auth feature:
  - add src/components/provider-auth/view/ProviderLoginModal.tsx
  - add src/components/provider-auth/types.ts
  - keep provider-specific command/title behavior and Gemini setup guidance
  - preserve compatibility for both onboarding flow and settings login flow

- Move TaskIndicator into the sidebar domain:
  - add src/components/sidebar/view/subcomponents/TaskIndicator.tsx
  - update SidebarProjectItem to consume local sidebar TaskIndicator

- Update integration points to the new structure:
  - ProtectedRoute now imports onboarding from onboarding feature
  - Settings now imports ProviderLoginModal directly (remove legacy cast wrapper)
  - git panel consumers now import shared GitDiffViewer by explicit name

- Rename git shared diff view to clearer domain naming:
  - replace shared DiffViewer with shared GitDiffViewer
  - update FileChangeItem and CommitHistoryItem imports accordingly

- Remove superseded root-level legacy components:
  - delete src/components/LoginModal.jsx
  - delete src/components/Onboarding.jsx
  - delete src/components/TaskIndicator.jsx
  - delete old src/components/git-panel/view/shared/DiffViewer.tsx

- Result:
  - clearer feature boundaries (auth vs onboarding vs provider-auth vs sidebar)
  - easier navigation and ownership by domain
  - preserved runtime behavior with improved readability and modularity

* refactor(MainContent): remove TaskMasterPanel import and relocate to task-master component

* fix: update import paths for Input component in FileTree and FileTreeNode

* refactor(FileTree): make file tree context menu a typescript component and move it inside the file tree view

* refactor(FileTree): remove unused ScrollArea import

* feat: setup eslint with typescript and react rules, add unused imports plugin

* fix: remove unused imports, functions, and types after discovering using `npm run lint`

* feat: setup eslint-plugin-react, react-refresh, import-x, and tailwindcss plugins with recommended rules and configurations

* chore: reformat files after running `npm run lint:fix`

* chore: add omments about eslint config plugin uses

* feat: add husky and lint-staged for pre-commit linting

* feat: setup commitlint with conventional config

* fix: i18n translations

---------

Co-authored-by: Haileyesus <something@gmail.com>
Co-authored-by: viper151 <simosmik@gmail.com>
This commit is contained in:
Haileyesus
2026-03-06 01:47:58 +03:00
committed by GitHub
parent 8d28438fe7
commit 844de26ada
254 changed files with 14571 additions and 9347 deletions

View File

@@ -0,0 +1,289 @@
import { Check, ChevronLeft, ChevronRight, Loader2 } from 'lucide-react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { authenticatedFetch } from '../../../utils/api';
import ProviderLoginModal from '../../provider-auth/view/ProviderLoginModal';
import AgentConnectionsStep from './subcomponents/AgentConnectionsStep';
import GitConfigurationStep from './subcomponents/GitConfigurationStep';
import OnboardingStepProgress from './subcomponents/OnboardingStepProgress';
import type { CliProvider, ProviderStatusMap } from './types';
import {
cliProviders,
createInitialProviderStatuses,
gitEmailPattern,
readErrorMessageFromResponse,
selectedProject,
} from './utils';
type OnboardingProps = {
onComplete?: () => void | Promise<void>;
};
export default function Onboarding({ onComplete }: OnboardingProps) {
const [currentStep, setCurrentStep] = useState(0);
const [gitName, setGitName] = useState('');
const [gitEmail, setGitEmail] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const [activeLoginProvider, setActiveLoginProvider] = useState<CliProvider | null>(null);
const [providerStatuses, setProviderStatuses] = useState<ProviderStatusMap>(createInitialProviderStatuses);
const previousActiveLoginProviderRef = useRef<CliProvider | null | undefined>(undefined);
const checkProviderAuthStatus = useCallback(async (provider: CliProvider) => {
try {
const response = await authenticatedFetch(`/api/cli/${provider}/status`);
if (!response.ok) {
setProviderStatuses((previous) => ({
...previous,
[provider]: {
authenticated: false,
email: null,
loading: false,
error: 'Failed to check authentication status',
},
}));
return;
}
const payload = (await response.json()) as {
authenticated?: boolean;
email?: string | null;
error?: string | null;
};
setProviderStatuses((previous) => ({
...previous,
[provider]: {
authenticated: Boolean(payload.authenticated),
email: payload.email ?? null,
loading: false,
error: payload.error ?? null,
},
}));
} catch (caughtError) {
console.error(`Error checking ${provider} auth status:`, caughtError);
setProviderStatuses((previous) => ({
...previous,
[provider]: {
authenticated: false,
email: null,
loading: false,
error: caughtError instanceof Error ? caughtError.message : 'Unknown error',
},
}));
}
}, []);
const refreshAllProviderStatuses = useCallback(async () => {
await Promise.all(cliProviders.map((provider) => checkProviderAuthStatus(provider)));
}, [checkProviderAuthStatus]);
const loadGitConfig = useCallback(async () => {
try {
const response = await authenticatedFetch('/api/user/git-config');
if (!response.ok) {
return;
}
const payload = (await response.json()) as { gitName?: string; gitEmail?: string };
if (payload.gitName) {
setGitName(payload.gitName);
}
if (payload.gitEmail) {
setGitEmail(payload.gitEmail);
}
} catch (caughtError) {
console.error('Error loading git config:', caughtError);
}
}, []);
useEffect(() => {
void loadGitConfig();
void refreshAllProviderStatuses();
}, [loadGitConfig, refreshAllProviderStatuses]);
useEffect(() => {
const previousProvider = previousActiveLoginProviderRef.current;
previousActiveLoginProviderRef.current = activeLoginProvider;
const isInitialMount = previousProvider === undefined;
const didCloseModal = previousProvider !== null && activeLoginProvider === null;
// Refresh statuses once on mount and again after the login modal is closed.
if (isInitialMount || didCloseModal) {
void refreshAllProviderStatuses();
}
}, [activeLoginProvider, refreshAllProviderStatuses]);
const handleProviderLoginOpen = (provider: CliProvider) => {
setActiveLoginProvider(provider);
};
const handleLoginComplete = (exitCode: number) => {
if (exitCode === 0 && activeLoginProvider) {
void checkProviderAuthStatus(activeLoginProvider);
}
};
const handleNextStep = async () => {
setErrorMessage('');
if (currentStep !== 0) {
setCurrentStep((previous) => previous + 1);
return;
}
if (!gitName.trim() || !gitEmail.trim()) {
setErrorMessage('Both git name and email are required.');
return;
}
if (!gitEmailPattern.test(gitEmail)) {
setErrorMessage('Please enter a valid email address.');
return;
}
setIsSubmitting(true);
try {
const response = await authenticatedFetch('/api/user/git-config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ gitName, gitEmail }),
});
if (!response.ok) {
const message = await readErrorMessageFromResponse(response, 'Failed to save git configuration');
throw new Error(message);
}
setCurrentStep((previous) => previous + 1);
} catch (caughtError) {
setErrorMessage(caughtError instanceof Error ? caughtError.message : 'Failed to save git configuration');
} finally {
setIsSubmitting(false);
}
};
const handlePreviousStep = () => {
setErrorMessage('');
setCurrentStep((previous) => previous - 1);
};
const handleFinish = async () => {
setIsSubmitting(true);
setErrorMessage('');
try {
const response = await authenticatedFetch('/api/user/complete-onboarding', { method: 'POST' });
if (!response.ok) {
const message = await readErrorMessageFromResponse(response, 'Failed to complete onboarding');
throw new Error(message);
}
await onComplete?.();
} catch (caughtError) {
setErrorMessage(caughtError instanceof Error ? caughtError.message : 'Failed to complete onboarding');
} finally {
setIsSubmitting(false);
}
};
const isCurrentStepValid = currentStep === 0
? Boolean(gitName.trim() && gitEmail.trim() && gitEmailPattern.test(gitEmail))
: true;
return (
<>
<div className="flex min-h-screen items-center justify-center bg-background p-4">
<div className="w-full max-w-2xl">
<OnboardingStepProgress currentStep={currentStep} />
<div className="rounded-lg border border-border bg-card p-8 shadow-lg">
{currentStep === 0 ? (
<GitConfigurationStep
gitName={gitName}
gitEmail={gitEmail}
isSubmitting={isSubmitting}
onGitNameChange={setGitName}
onGitEmailChange={setGitEmail}
/>
) : (
<AgentConnectionsStep
providerStatuses={providerStatuses}
onOpenProviderLogin={handleProviderLoginOpen}
/>
)}
{errorMessage && (
<div className="mt-6 rounded-lg border border-red-300 bg-red-100 p-4 dark:border-red-800 dark:bg-red-900/20">
<p className="text-sm text-red-700 dark:text-red-400">{errorMessage}</p>
</div>
)}
<div className="mt-8 flex items-center justify-between border-t border-border pt-6">
<button
onClick={handlePreviousStep}
disabled={currentStep === 0 || isSubmitting}
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-muted-foreground transition-colors duration-200 hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
>
<ChevronLeft className="h-4 w-4" />
Previous
</button>
<div className="flex items-center gap-3">
{currentStep < 1 ? (
<button
onClick={handleNextStep}
disabled={!isCurrentStepValid || isSubmitting}
className="flex items-center gap-2 rounded-lg bg-blue-600 px-6 py-3 font-medium text-white transition-colors duration-200 hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-blue-400"
>
{isSubmitting ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Saving...
</>
) : (
<>
Next
<ChevronRight className="h-4 w-4" />
</>
)}
</button>
) : (
<button
onClick={handleFinish}
disabled={isSubmitting}
className="flex items-center gap-2 rounded-lg bg-green-600 px-6 py-3 font-medium text-white transition-colors duration-200 hover:bg-green-700 disabled:cursor-not-allowed disabled:bg-green-400"
>
{isSubmitting ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Completing...
</>
) : (
<>
<Check className="h-4 w-4" />
Complete Setup
</>
)}
</button>
)}
</div>
</div>
</div>
</div>
</div>
{activeLoginProvider && (
<ProviderLoginModal
isOpen={Boolean(activeLoginProvider)}
onClose={() => setActiveLoginProvider(null)}
provider={activeLoginProvider}
project={selectedProject}
onComplete={handleLoginComplete}
isOnboarding={true}
/>
)}
</>
);
}

View File

@@ -0,0 +1,60 @@
import { Check } from 'lucide-react';
import SessionProviderLogo from '../../../llm-logo-provider/SessionProviderLogo';
import type { CliProvider, ProviderAuthStatus } from '../types';
type AgentConnectionCardProps = {
provider: CliProvider;
title: string;
status: ProviderAuthStatus;
connectedClassName: string;
iconContainerClassName: string;
loginButtonClassName: string;
onLogin: () => void;
};
export default function AgentConnectionCard({
provider,
title,
status,
connectedClassName,
iconContainerClassName,
loginButtonClassName,
onLogin,
}: AgentConnectionCardProps) {
const containerClassName = status.authenticated ? connectedClassName : 'border-border bg-card';
const statusText = status.loading
? 'Checking...'
: status.authenticated
? status.email || 'Connected'
: status.error || 'Not connected';
return (
<div className={`rounded-lg border p-4 transition-colors ${containerClassName}`}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={`flex h-10 w-10 items-center justify-center rounded-full ${iconContainerClassName}`}>
<SessionProviderLogo provider={provider} className="h-5 w-5" />
</div>
<div>
<div className="flex items-center gap-2 font-medium text-foreground">
{title}
{status.authenticated && <Check className="h-4 w-4 text-green-500" />}
</div>
<div className="text-xs text-muted-foreground">{statusText}</div>
</div>
</div>
{!status.authenticated && !status.loading && (
<button
onClick={onLogin}
className={`${loginButtonClassName} rounded-lg px-4 py-2 text-sm font-medium text-white transition-colors`}
>
Login
</button>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,73 @@
import type { CliProvider, ProviderStatusMap } from '../types';
import AgentConnectionCard from './AgentConnectionCard';
type AgentConnectionsStepProps = {
providerStatuses: ProviderStatusMap;
onOpenProviderLogin: (provider: CliProvider) => void;
};
const providerCards = [
{
provider: 'claude' as const,
title: 'Claude Code',
connectedClassName: 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800',
iconContainerClassName: 'bg-blue-100 dark:bg-blue-900/30',
loginButtonClassName: 'bg-blue-600 hover:bg-blue-700',
},
{
provider: 'cursor' as const,
title: 'Cursor',
connectedClassName: 'bg-purple-50 dark:bg-purple-900/20 border-purple-200 dark:border-purple-800',
iconContainerClassName: 'bg-purple-100 dark:bg-purple-900/30',
loginButtonClassName: 'bg-purple-600 hover:bg-purple-700',
},
{
provider: 'codex' as const,
title: 'OpenAI Codex',
connectedClassName: 'bg-gray-100 dark:bg-gray-800/50 border-gray-300 dark:border-gray-600',
iconContainerClassName: 'bg-gray-100 dark:bg-gray-800',
loginButtonClassName: 'bg-gray-800 hover:bg-gray-900 dark:bg-gray-700 dark:hover:bg-gray-600',
},
{
provider: 'gemini' as const,
title: 'Gemini',
connectedClassName: 'bg-teal-50 dark:bg-teal-900/20 border-teal-200 dark:border-teal-800',
iconContainerClassName: 'bg-teal-100 dark:bg-teal-900/30',
loginButtonClassName: 'bg-teal-600 hover:bg-teal-700',
},
];
export default function AgentConnectionsStep({
providerStatuses,
onOpenProviderLogin,
}: AgentConnectionsStepProps) {
return (
<div className="space-y-6">
<div className="mb-6 text-center">
<h2 className="mb-2 text-2xl font-bold text-foreground">Connect Your AI Agents</h2>
<p className="text-muted-foreground">
Login to one or more AI coding assistants. All are optional.
</p>
</div>
<div className="space-y-3">
{providerCards.map((providerCard) => (
<AgentConnectionCard
key={providerCard.provider}
provider={providerCard.provider}
title={providerCard.title}
status={providerStatuses[providerCard.provider]}
connectedClassName={providerCard.connectedClassName}
iconContainerClassName={providerCard.iconContainerClassName}
loginButtonClassName={providerCard.loginButtonClassName}
onLogin={() => onOpenProviderLogin(providerCard.provider)}
/>
))}
</div>
<div className="pt-2 text-center text-sm text-muted-foreground">
<p>You can configure these later in Settings.</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,69 @@
import { GitBranch, Mail, User } from 'lucide-react';
type GitConfigurationStepProps = {
gitName: string;
gitEmail: string;
isSubmitting: boolean;
onGitNameChange: (value: string) => void;
onGitEmailChange: (value: string) => void;
};
export default function GitConfigurationStep({
gitName,
gitEmail,
isSubmitting,
onGitNameChange,
onGitEmailChange,
}: GitConfigurationStepProps) {
return (
<div className="space-y-6">
<div className="mb-8 text-center">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900/30">
<GitBranch className="h-8 w-8 text-blue-600 dark:text-blue-400" />
</div>
<h2 className="mb-2 text-2xl font-bold text-foreground">Git Configuration</h2>
<p className="text-muted-foreground">
Configure your git identity to ensure proper attribution for commits.
</p>
</div>
<div className="space-y-4">
<div>
<label htmlFor="gitName" className="mb-2 flex items-center gap-2 text-sm font-medium text-foreground">
<User className="h-4 w-4" />
Git Name <span className="text-red-500">*</span>
</label>
<input
type="text"
id="gitName"
value={gitName}
onChange={(event) => onGitNameChange(event.target.value)}
className="w-full rounded-lg border border-border bg-background px-4 py-3 text-foreground focus:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="John Doe"
required
disabled={isSubmitting}
/>
<p className="mt-1 text-xs text-muted-foreground">Saved as `git config --global user.name`.</p>
</div>
<div>
<label htmlFor="gitEmail" className="mb-2 flex items-center gap-2 text-sm font-medium text-foreground">
<Mail className="h-4 w-4" />
Git Email <span className="text-red-500">*</span>
</label>
<input
type="email"
id="gitEmail"
value={gitEmail}
onChange={(event) => onGitEmailChange(event.target.value)}
className="w-full rounded-lg border border-border bg-background px-4 py-3 text-foreground focus:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="john@example.com"
required
disabled={isSubmitting}
/>
<p className="mt-1 text-xs text-muted-foreground">Saved as `git config --global user.email`.</p>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,53 @@
import { Check, GitBranch, LogIn } from 'lucide-react';
type OnboardingStepProgressProps = {
currentStep: number;
};
const onboardingSteps = [
{ title: 'Git Configuration', icon: GitBranch, required: true },
{ title: 'Connect Agents', icon: LogIn, required: false },
];
export default function OnboardingStepProgress({ currentStep }: OnboardingStepProgressProps) {
return (
<div className="mb-8">
<div className="flex items-center justify-between">
{onboardingSteps.map((step, index) => {
const isCompleted = index < currentStep;
const isActive = index === currentStep;
const Icon = step.icon;
return (
<div key={step.title} className="contents">
<div className="flex flex-1 flex-col items-center">
<div
className={`flex h-12 w-12 items-center justify-center rounded-full border-2 transition-colors duration-200 ${
isCompleted
? 'border-green-500 bg-green-500 text-white'
: isActive
? 'border-blue-600 bg-blue-600 text-white'
: 'border-border bg-background text-muted-foreground'
}`}
>
{isCompleted ? <Check className="h-6 w-6" /> : <Icon className="h-6 w-6" />}
</div>
<div className="mt-2 text-center">
<p className={`text-sm font-medium ${isActive ? 'text-foreground' : 'text-muted-foreground'}`}>
{step.title}
</p>
{step.required && <span className="text-xs text-red-500">Required</span>}
</div>
</div>
{index < onboardingSteps.length - 1 && (
<div className={`mx-2 h-0.5 flex-1 transition-colors duration-200 ${isCompleted ? 'bg-green-500' : 'bg-border'}`} />
)}
</div>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,12 @@
import type { CliProvider } from '../../provider-auth/types';
export type { CliProvider };
export type ProviderAuthStatus = {
authenticated: boolean;
email: string | null;
loading: boolean;
error: string | null;
};
export type ProviderStatusMap = Record<CliProvider, ProviderAuthStatus>;

View File

@@ -0,0 +1,29 @@
import { IS_PLATFORM } from '../../../constants/config';
import type { CliProvider, ProviderStatusMap } from './types';
export const cliProviders: CliProvider[] = ['claude', 'cursor', 'codex', 'gemini'];
export const gitEmailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
export const selectedProject = {
name: 'default',
displayName: 'default',
fullPath: IS_PLATFORM ? '/workspace' : '',
path: IS_PLATFORM ? '/workspace' : '',
};
export const createInitialProviderStatuses = (): ProviderStatusMap => ({
claude: { authenticated: false, email: null, loading: true, error: null },
cursor: { authenticated: false, email: null, loading: true, error: null },
codex: { authenticated: false, email: null, loading: true, error: null },
gemini: { authenticated: false, email: null, loading: true, error: null },
});
export const readErrorMessageFromResponse = async (response: Response, fallback: string) => {
try {
const payload = (await response.json()) as { error?: string };
return payload.error || fallback;
} catch {
return fallback;
}
};