mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-03-07 23:17:37 +00:00
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
This commit is contained in:
@@ -1,3 +0,0 @@
|
|||||||
import NextTaskBanner from './task-master/view/NextTaskBanner';
|
|
||||||
|
|
||||||
export default NextTaskBanner;
|
|
||||||
@@ -1,567 +0,0 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
|
||||||
import { ChevronRight, ChevronLeft, Check, GitBranch, User, Mail, LogIn, ExternalLink, Loader2 } from 'lucide-react';
|
|
||||||
import SessionProviderLogo from './llm-logo-provider/SessionProviderLogo';
|
|
||||||
import LoginModal from './LoginModal';
|
|
||||||
import { authenticatedFetch } from '../utils/api';
|
|
||||||
import { useAuth } from './auth/context/AuthContext';
|
|
||||||
import { IS_PLATFORM } from '../constants/config';
|
|
||||||
|
|
||||||
const Onboarding = ({ onComplete }) => {
|
|
||||||
const [currentStep, setCurrentStep] = useState(0);
|
|
||||||
const [gitName, setGitName] = useState('');
|
|
||||||
const [gitEmail, setGitEmail] = useState('');
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
const [error, setError] = useState('');
|
|
||||||
|
|
||||||
const [activeLoginProvider, setActiveLoginProvider] = useState(null);
|
|
||||||
const [selectedProject] = useState({ name: 'default', fullPath: IS_PLATFORM ? '/workspace' : '' });
|
|
||||||
|
|
||||||
const [claudeAuthStatus, setClaudeAuthStatus] = useState({
|
|
||||||
authenticated: false,
|
|
||||||
email: null,
|
|
||||||
loading: true,
|
|
||||||
error: null
|
|
||||||
});
|
|
||||||
|
|
||||||
const [cursorAuthStatus, setCursorAuthStatus] = useState({
|
|
||||||
authenticated: false,
|
|
||||||
email: null,
|
|
||||||
loading: true,
|
|
||||||
error: null
|
|
||||||
});
|
|
||||||
|
|
||||||
const [codexAuthStatus, setCodexAuthStatus] = useState({
|
|
||||||
authenticated: false,
|
|
||||||
email: null,
|
|
||||||
loading: true,
|
|
||||||
error: null
|
|
||||||
});
|
|
||||||
|
|
||||||
const [geminiAuthStatus, setGeminiAuthStatus] = useState({
|
|
||||||
authenticated: false,
|
|
||||||
email: null,
|
|
||||||
loading: true,
|
|
||||||
error: null
|
|
||||||
});
|
|
||||||
|
|
||||||
const { user } = useAuth();
|
|
||||||
|
|
||||||
const prevActiveLoginProviderRef = useRef(undefined);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadGitConfig();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const loadGitConfig = async () => {
|
|
||||||
try {
|
|
||||||
const response = await authenticatedFetch('/api/user/git-config');
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.gitName) setGitName(data.gitName);
|
|
||||||
if (data.gitEmail) setGitEmail(data.gitEmail);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading git config:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const prevProvider = prevActiveLoginProviderRef.current;
|
|
||||||
prevActiveLoginProviderRef.current = activeLoginProvider;
|
|
||||||
|
|
||||||
const isInitialMount = prevProvider === undefined;
|
|
||||||
const isModalClosing = prevProvider !== null && activeLoginProvider === null;
|
|
||||||
|
|
||||||
if (isInitialMount || isModalClosing) {
|
|
||||||
checkClaudeAuthStatus();
|
|
||||||
checkCursorAuthStatus();
|
|
||||||
checkCodexAuthStatus();
|
|
||||||
checkGeminiAuthStatus();
|
|
||||||
}
|
|
||||||
}, [activeLoginProvider]);
|
|
||||||
|
|
||||||
const checkProviderAuthStatus = async (provider, setter) => {
|
|
||||||
try {
|
|
||||||
const response = await authenticatedFetch(`/api/cli/${provider}/status`);
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
setter({
|
|
||||||
authenticated: data.authenticated,
|
|
||||||
email: data.email,
|
|
||||||
loading: false,
|
|
||||||
error: data.error || null
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setter({
|
|
||||||
authenticated: false,
|
|
||||||
email: null,
|
|
||||||
loading: false,
|
|
||||||
error: 'Failed to check authentication status'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error checking ${provider} auth status:`, error);
|
|
||||||
setter({
|
|
||||||
authenticated: false,
|
|
||||||
email: null,
|
|
||||||
loading: false,
|
|
||||||
error: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkClaudeAuthStatus = () => checkProviderAuthStatus('claude', setClaudeAuthStatus);
|
|
||||||
const checkCursorAuthStatus = () => checkProviderAuthStatus('cursor', setCursorAuthStatus);
|
|
||||||
const checkCodexAuthStatus = () => checkProviderAuthStatus('codex', setCodexAuthStatus);
|
|
||||||
const checkGeminiAuthStatus = () => checkProviderAuthStatus('gemini', setGeminiAuthStatus);
|
|
||||||
|
|
||||||
const handleClaudeLogin = () => setActiveLoginProvider('claude');
|
|
||||||
const handleCursorLogin = () => setActiveLoginProvider('cursor');
|
|
||||||
const handleCodexLogin = () => setActiveLoginProvider('codex');
|
|
||||||
const handleGeminiLogin = () => setActiveLoginProvider('gemini');
|
|
||||||
|
|
||||||
const handleLoginComplete = (exitCode) => {
|
|
||||||
if (exitCode === 0) {
|
|
||||||
if (activeLoginProvider === 'claude') {
|
|
||||||
checkClaudeAuthStatus();
|
|
||||||
} else if (activeLoginProvider === 'cursor') {
|
|
||||||
checkCursorAuthStatus();
|
|
||||||
} else if (activeLoginProvider === 'codex') {
|
|
||||||
checkCodexAuthStatus();
|
|
||||||
} else if (activeLoginProvider === 'gemini') {
|
|
||||||
checkGeminiAuthStatus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNextStep = async () => {
|
|
||||||
setError('');
|
|
||||||
|
|
||||||
// Step 0: Git config validation and submission
|
|
||||||
if (currentStep === 0) {
|
|
||||||
if (!gitName.trim() || !gitEmail.trim()) {
|
|
||||||
setError('Both git name and email are required');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate email format
|
|
||||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
||||||
if (!emailRegex.test(gitEmail)) {
|
|
||||||
setError('Please enter a valid email address');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSubmitting(true);
|
|
||||||
try {
|
|
||||||
// Save git config to backend (which will also apply git config --global)
|
|
||||||
const response = await authenticatedFetch('/api/user/git-config', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ gitName, gitEmail })
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
throw new Error(data.error || 'Failed to save git configuration');
|
|
||||||
}
|
|
||||||
|
|
||||||
setCurrentStep(currentStep + 1);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err.message);
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setCurrentStep(currentStep + 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePrevStep = () => {
|
|
||||||
setError('');
|
|
||||||
setCurrentStep(currentStep - 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFinish = async () => {
|
|
||||||
setIsSubmitting(true);
|
|
||||||
setError('');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await authenticatedFetch('/api/user/complete-onboarding', {
|
|
||||||
method: 'POST'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
throw new Error(data.error || 'Failed to complete onboarding');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onComplete) {
|
|
||||||
onComplete();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setError(err.message);
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const steps = [
|
|
||||||
{
|
|
||||||
title: 'Git Configuration',
|
|
||||||
description: 'Set up your git identity for commits',
|
|
||||||
icon: GitBranch,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Connect Agents',
|
|
||||||
description: 'Connect your AI coding assistants',
|
|
||||||
icon: LogIn,
|
|
||||||
required: false
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const renderStepContent = () => {
|
|
||||||
switch (currentStep) {
|
|
||||||
case 0:
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="text-center mb-8">
|
|
||||||
<div className="w-16 h-16 bg-blue-100 dark:bg-blue-900/30 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
||||||
<GitBranch className="w-8 h-8 text-blue-600 dark:text-blue-400" />
|
|
||||||
</div>
|
|
||||||
<h2 className="text-2xl font-bold text-foreground mb-2">Git Configuration</h2>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Configure your git identity to ensure proper attribution for your commits
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label htmlFor="gitName" className="flex items-center gap-2 text-sm font-medium text-foreground mb-2">
|
|
||||||
<User className="w-4 h-4" />
|
|
||||||
Git Name <span className="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="gitName"
|
|
||||||
value={gitName}
|
|
||||||
onChange={(e) => setGitName(e.target.value)}
|
|
||||||
className="w-full px-4 py-3 border border-border rounded-lg bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
placeholder="John Doe"
|
|
||||||
required
|
|
||||||
disabled={isSubmitting}
|
|
||||||
/>
|
|
||||||
<p className="mt-1 text-xs text-muted-foreground">
|
|
||||||
This will be used as: git config --global user.name
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="gitEmail" className="flex items-center gap-2 text-sm font-medium text-foreground mb-2">
|
|
||||||
<Mail className="w-4 h-4" />
|
|
||||||
Git Email <span className="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
id="gitEmail"
|
|
||||||
value={gitEmail}
|
|
||||||
onChange={(e) => setGitEmail(e.target.value)}
|
|
||||||
className="w-full px-4 py-3 border border-border rounded-lg bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
placeholder="john@example.com"
|
|
||||||
required
|
|
||||||
disabled={isSubmitting}
|
|
||||||
/>
|
|
||||||
<p className="mt-1 text-xs text-muted-foreground">
|
|
||||||
This will be used as: git config --global user.email
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 1:
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="text-center mb-6">
|
|
||||||
<h2 className="text-2xl font-bold text-foreground mb-2">Connect Your AI Agents</h2>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Login to one or more AI coding assistants. All are optional.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Agent Cards Grid */}
|
|
||||||
<div className="space-y-3">
|
|
||||||
{/* Claude */}
|
|
||||||
<div className={`border rounded-lg p-4 transition-colors ${claudeAuthStatus.authenticated
|
|
||||||
? 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800'
|
|
||||||
: 'border-border bg-card'
|
|
||||||
}`}>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-10 h-10 bg-blue-100 dark:bg-blue-900/30 rounded-full flex items-center justify-center">
|
|
||||||
<SessionProviderLogo provider="claude" className="w-5 h-5" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="font-medium text-foreground flex items-center gap-2">
|
|
||||||
Claude Code
|
|
||||||
{claudeAuthStatus.authenticated && <Check className="w-4 h-4 text-green-500" />}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-muted-foreground">
|
|
||||||
{claudeAuthStatus.loading ? 'Checking...' :
|
|
||||||
claudeAuthStatus.authenticated ? claudeAuthStatus.email || 'Connected' : 'Not connected'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{!claudeAuthStatus.authenticated && !claudeAuthStatus.loading && (
|
|
||||||
<button
|
|
||||||
onClick={handleClaudeLogin}
|
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium py-2 px-4 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
Login
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Cursor */}
|
|
||||||
<div className={`border rounded-lg p-4 transition-colors ${cursorAuthStatus.authenticated
|
|
||||||
? 'bg-purple-50 dark:bg-purple-900/20 border-purple-200 dark:border-purple-800'
|
|
||||||
: 'border-border bg-card'
|
|
||||||
}`}>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-10 h-10 bg-purple-100 dark:bg-purple-900/30 rounded-full flex items-center justify-center">
|
|
||||||
<SessionProviderLogo provider="cursor" className="w-5 h-5" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="font-medium text-foreground flex items-center gap-2">
|
|
||||||
Cursor
|
|
||||||
{cursorAuthStatus.authenticated && <Check className="w-4 h-4 text-green-500" />}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-muted-foreground">
|
|
||||||
{cursorAuthStatus.loading ? 'Checking...' :
|
|
||||||
cursorAuthStatus.authenticated ? cursorAuthStatus.email || 'Connected' : 'Not connected'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{!cursorAuthStatus.authenticated && !cursorAuthStatus.loading && (
|
|
||||||
<button
|
|
||||||
onClick={handleCursorLogin}
|
|
||||||
className="bg-purple-600 hover:bg-purple-700 text-white text-sm font-medium py-2 px-4 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
Login
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Codex */}
|
|
||||||
<div className={`border rounded-lg p-4 transition-colors ${codexAuthStatus.authenticated
|
|
||||||
? 'bg-gray-100 dark:bg-gray-800/50 border-gray-300 dark:border-gray-600'
|
|
||||||
: 'border-border bg-card'
|
|
||||||
}`}>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-10 h-10 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center">
|
|
||||||
<SessionProviderLogo provider="codex" className="w-5 h-5" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="font-medium text-foreground flex items-center gap-2">
|
|
||||||
OpenAI Codex
|
|
||||||
{codexAuthStatus.authenticated && <Check className="w-4 h-4 text-green-500" />}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-muted-foreground">
|
|
||||||
{codexAuthStatus.loading ? 'Checking...' :
|
|
||||||
codexAuthStatus.authenticated ? codexAuthStatus.email || 'Connected' : 'Not connected'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{!codexAuthStatus.authenticated && !codexAuthStatus.loading && (
|
|
||||||
<button
|
|
||||||
onClick={handleCodexLogin}
|
|
||||||
className="bg-gray-800 hover:bg-gray-900 dark:bg-gray-700 dark:hover:bg-gray-600 text-white text-sm font-medium py-2 px-4 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
Login
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Gemini */}
|
|
||||||
<div className={`border rounded-lg p-4 transition-colors ${geminiAuthStatus.authenticated
|
|
||||||
? 'bg-teal-50 dark:bg-teal-900/20 border-teal-200 dark:border-teal-800'
|
|
||||||
: 'border-border bg-card'
|
|
||||||
}`}>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-10 h-10 bg-teal-100 dark:bg-teal-900/30 rounded-full flex items-center justify-center">
|
|
||||||
<SessionProviderLogo provider="gemini" className="w-5 h-5 text-teal-600 dark:text-teal-400" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="font-medium text-foreground flex items-center gap-2">
|
|
||||||
Gemini
|
|
||||||
{geminiAuthStatus.authenticated && <Check className="w-4 h-4 text-green-500" />}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-muted-foreground">
|
|
||||||
{geminiAuthStatus.loading ? 'Checking...' :
|
|
||||||
geminiAuthStatus.authenticated ? geminiAuthStatus.email || 'Connected' : 'Not connected'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{!geminiAuthStatus.authenticated && !geminiAuthStatus.loading && (
|
|
||||||
<button
|
|
||||||
onClick={handleGeminiLogin}
|
|
||||||
className="bg-teal-600 hover:bg-teal-700 text-white text-sm font-medium py-2 px-4 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
Login
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center text-sm text-muted-foreground pt-2">
|
|
||||||
<p>You can configure these later in Settings.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isStepValid = () => {
|
|
||||||
switch (currentStep) {
|
|
||||||
case 0:
|
|
||||||
return gitName.trim() && gitEmail.trim() && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(gitEmail);
|
|
||||||
case 1:
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
|
||||||
<div className="w-full max-w-2xl">
|
|
||||||
{/* Progress Steps */}
|
|
||||||
<div className="mb-8">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
{steps.map((step, index) => (
|
|
||||||
<React.Fragment key={index}>
|
|
||||||
<div className="flex flex-col items-center flex-1">
|
|
||||||
<div className={`w-12 h-12 rounded-full flex items-center justify-center border-2 transition-colors duration-200 ${index < currentStep ? 'bg-green-500 border-green-500 text-white' :
|
|
||||||
index === currentStep ? 'bg-blue-600 border-blue-600 text-white' :
|
|
||||||
'bg-background border-border text-muted-foreground'
|
|
||||||
}`}>
|
|
||||||
{index < currentStep ? (
|
|
||||||
<Check className="w-6 h-6" />
|
|
||||||
) : typeof step.icon === 'function' ? (
|
|
||||||
<step.icon />
|
|
||||||
) : (
|
|
||||||
<step.icon className="w-6 h-6" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 text-center">
|
|
||||||
<p className={`text-sm font-medium ${index === currentStep ? 'text-foreground' : 'text-muted-foreground'
|
|
||||||
}`}>
|
|
||||||
{step.title}
|
|
||||||
</p>
|
|
||||||
{step.required && (
|
|
||||||
<span className="text-xs text-red-500">Required</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{index < steps.length - 1 && (
|
|
||||||
<div className={`flex-1 h-0.5 mx-2 transition-colors duration-200 ${index < currentStep ? 'bg-green-500' : 'bg-border'
|
|
||||||
}`} />
|
|
||||||
)}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Card */}
|
|
||||||
<div className="bg-card rounded-lg shadow-lg border border-border p-8">
|
|
||||||
{renderStepContent()}
|
|
||||||
|
|
||||||
{/* Error Message */}
|
|
||||||
{error && (
|
|
||||||
<div className="mt-6 p-4 bg-red-100 dark:bg-red-900/20 border border-red-300 dark:border-red-800 rounded-lg">
|
|
||||||
<p className="text-sm text-red-700 dark:text-red-400">{error}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Navigation Buttons */}
|
|
||||||
<div className="flex items-center justify-between mt-8 pt-6 border-t border-border">
|
|
||||||
<button
|
|
||||||
onClick={handlePrevStep}
|
|
||||||
disabled={currentStep === 0 || isSubmitting}
|
|
||||||
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-muted-foreground hover:text-foreground disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200"
|
|
||||||
>
|
|
||||||
<ChevronLeft className="w-4 h-4" />
|
|
||||||
Previous
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
{currentStep < steps.length - 1 ? (
|
|
||||||
<button
|
|
||||||
onClick={handleNextStep}
|
|
||||||
disabled={!isStepValid() || isSubmitting}
|
|
||||||
className="flex items-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 disabled:cursor-not-allowed text-white font-medium rounded-lg transition-colors duration-200"
|
|
||||||
>
|
|
||||||
{isSubmitting ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
|
||||||
Saving...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
Next
|
|
||||||
<ChevronRight className="w-4 h-4" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
onClick={handleFinish}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
className="flex items-center gap-2 px-6 py-3 bg-green-600 hover:bg-green-700 disabled:bg-green-400 disabled:cursor-not-allowed text-white font-medium rounded-lg transition-colors duration-200"
|
|
||||||
>
|
|
||||||
{isSubmitting ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
|
||||||
Completing...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Check className="w-4 h-4" />
|
|
||||||
Complete Setup
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{activeLoginProvider && (
|
|
||||||
<LoginModal
|
|
||||||
isOpen={!!activeLoginProvider}
|
|
||||||
onClose={() => setActiveLoginProvider(null)}
|
|
||||||
provider={activeLoginProvider}
|
|
||||||
project={selectedProject}
|
|
||||||
onComplete={handleLoginComplete}
|
|
||||||
isOnboarding={true}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Onboarding;
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { CheckCircle, Settings, X, AlertCircle } from 'lucide-react';
|
|
||||||
import { cn } from '../lib/utils';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TaskIndicator Component
|
|
||||||
*
|
|
||||||
* Displays TaskMaster status for projects in the sidebar with appropriate
|
|
||||||
* icons and colors based on the project's TaskMaster configuration state.
|
|
||||||
*/
|
|
||||||
const TaskIndicator = ({
|
|
||||||
status = 'not-configured',
|
|
||||||
size = 'sm',
|
|
||||||
className = '',
|
|
||||||
showLabel = false
|
|
||||||
}) => {
|
|
||||||
const getIndicatorConfig = () => {
|
|
||||||
switch (status) {
|
|
||||||
case 'fully-configured':
|
|
||||||
return {
|
|
||||||
icon: CheckCircle,
|
|
||||||
color: 'text-green-500 dark:text-green-400',
|
|
||||||
bgColor: 'bg-green-50 dark:bg-green-950',
|
|
||||||
label: 'TaskMaster Ready',
|
|
||||||
title: 'TaskMaster fully configured with MCP server'
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'taskmaster-only':
|
|
||||||
return {
|
|
||||||
icon: Settings,
|
|
||||||
color: 'text-blue-500 dark:text-blue-400',
|
|
||||||
bgColor: 'bg-blue-50 dark:bg-blue-950',
|
|
||||||
label: 'TaskMaster Init',
|
|
||||||
title: 'TaskMaster initialized, MCP server needs setup'
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'mcp-only':
|
|
||||||
return {
|
|
||||||
icon: AlertCircle,
|
|
||||||
color: 'text-amber-500 dark:text-amber-400',
|
|
||||||
bgColor: 'bg-amber-50 dark:bg-amber-950',
|
|
||||||
label: 'MCP Ready',
|
|
||||||
title: 'MCP server configured, TaskMaster needs initialization'
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'not-configured':
|
|
||||||
case 'error':
|
|
||||||
default:
|
|
||||||
return {
|
|
||||||
icon: X,
|
|
||||||
color: 'text-gray-400 dark:text-gray-500',
|
|
||||||
bgColor: 'bg-gray-50 dark:bg-gray-900',
|
|
||||||
label: 'No TaskMaster',
|
|
||||||
title: 'TaskMaster not configured'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const config = getIndicatorConfig();
|
|
||||||
const Icon = config.icon;
|
|
||||||
|
|
||||||
const sizeClasses = {
|
|
||||||
xs: 'w-3 h-3',
|
|
||||||
sm: 'w-4 h-4',
|
|
||||||
md: 'w-5 h-5',
|
|
||||||
lg: 'w-6 h-6'
|
|
||||||
};
|
|
||||||
|
|
||||||
const paddingClasses = {
|
|
||||||
xs: 'p-0.5',
|
|
||||||
sm: 'p-1',
|
|
||||||
md: 'p-1.5',
|
|
||||||
lg: 'p-2'
|
|
||||||
};
|
|
||||||
|
|
||||||
if (showLabel) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'inline-flex items-center gap-1.5 text-xs rounded-md px-2 py-1 transition-colors',
|
|
||||||
config.bgColor,
|
|
||||||
config.color,
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
title={config.title}
|
|
||||||
>
|
|
||||||
<Icon className={sizeClasses[size]} />
|
|
||||||
<span className="font-medium">{config.label}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'inline-flex items-center justify-center rounded-full transition-colors',
|
|
||||||
config.bgColor,
|
|
||||||
paddingClasses[size],
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
title={config.title}
|
|
||||||
>
|
|
||||||
<Icon className={cn(sizeClasses[size], config.color)} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TaskIndicator;
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import Onboarding from '../../Onboarding';
|
|
||||||
import { IS_PLATFORM } from '../../../constants/config';
|
import { IS_PLATFORM } from '../../../constants/config';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
import AuthLoadingScreen from './AuthLoadingScreen';
|
import AuthLoadingScreen from './AuthLoadingScreen';
|
||||||
import LoginForm from './LoginForm';
|
import LoginForm from './LoginForm';
|
||||||
|
import Onboarding from '../../onboarding/view/Onboarding';
|
||||||
import SetupForm from './SetupForm';
|
import SetupForm from './SetupForm';
|
||||||
|
|
||||||
type ProtectedRouteProps = {
|
type ProtectedRouteProps = {
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import React from 'react';
|
|||||||
import { Check, ChevronDown } from 'lucide-react';
|
import { Check, ChevronDown } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import SessionProviderLogo from '../../../llm-logo-provider/SessionProviderLogo';
|
import SessionProviderLogo from '../../../llm-logo-provider/SessionProviderLogo';
|
||||||
import NextTaskBanner from '../../../NextTaskBanner.jsx';
|
|
||||||
import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS, GEMINI_MODELS } from '../../../../../shared/modelConstants';
|
import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS, GEMINI_MODELS } from '../../../../../shared/modelConstants';
|
||||||
import type { ProjectSession, SessionProvider } from '../../../../types/app';
|
import type { ProjectSession, SessionProvider } from '../../../../types/app';
|
||||||
|
import { NextTaskBanner } from '../../../task-master';
|
||||||
|
|
||||||
interface ProviderSelectionEmptyStateProps {
|
interface ProviderSelectionEmptyStateProps {
|
||||||
selectedSession: ProjectSession | null;
|
selectedSession: ProjectSession | null;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ChevronRight, Trash2 } from 'lucide-react';
|
import { ChevronRight, Trash2 } from 'lucide-react';
|
||||||
import type { FileStatusCode } from '../../types/types';
|
import type { FileStatusCode } from '../../types/types';
|
||||||
import { getStatusBadgeClass, getStatusLabel } from '../../utils/gitPanelUtils';
|
import { getStatusBadgeClass, getStatusLabel } from '../../utils/gitPanelUtils';
|
||||||
import GitDiffViewer from '../shared/DiffViewer';
|
import GitDiffViewer from '../shared/GitDiffViewer';
|
||||||
|
|
||||||
type FileChangeItemProps = {
|
type FileChangeItemProps = {
|
||||||
filePath: string;
|
filePath: string;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ChevronDown, ChevronRight } from 'lucide-react';
|
import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||||
|
|
||||||
import type { GitCommitSummary } from '../../types/types';
|
import type { GitCommitSummary } from '../../types/types';
|
||||||
import GitDiffViewer from '../shared/DiffViewer';
|
import GitDiffViewer from '../shared/GitDiffViewer';
|
||||||
|
|
||||||
|
|
||||||
type CommitHistoryItemProps = {
|
type CommitHistoryItemProps = {
|
||||||
|
|||||||
289
src/components/onboarding/view/Onboarding.tsx
Normal file
289
src/components/onboarding/view/Onboarding.tsx
Normal 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="min-h-screen bg-background flex items-center justify-center p-4">
|
||||||
|
<div className="w-full max-w-2xl">
|
||||||
|
<OnboardingStepProgress currentStep={currentStep} />
|
||||||
|
|
||||||
|
<div className="bg-card rounded-lg shadow-lg border border-border p-8">
|
||||||
|
{currentStep === 0 ? (
|
||||||
|
<GitConfigurationStep
|
||||||
|
gitName={gitName}
|
||||||
|
gitEmail={gitEmail}
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
|
onGitNameChange={setGitName}
|
||||||
|
onGitEmailChange={setGitEmail}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<AgentConnectionsStep
|
||||||
|
providerStatuses={providerStatuses}
|
||||||
|
onOpenProviderLogin={handleProviderLoginOpen}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{errorMessage && (
|
||||||
|
<div className="mt-6 p-4 bg-red-100 dark:bg-red-900/20 border border-red-300 dark:border-red-800 rounded-lg">
|
||||||
|
<p className="text-sm text-red-700 dark:text-red-400">{errorMessage}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between mt-8 pt-6 border-t border-border">
|
||||||
|
<button
|
||||||
|
onClick={handlePreviousStep}
|
||||||
|
disabled={currentStep === 0 || isSubmitting}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-muted-foreground hover:text-foreground disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-4 h-4" />
|
||||||
|
Previous
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{currentStep < 1 ? (
|
||||||
|
<button
|
||||||
|
onClick={handleNextStep}
|
||||||
|
disabled={!isCurrentStepValid || isSubmitting}
|
||||||
|
className="flex items-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 disabled:cursor-not-allowed text-white font-medium rounded-lg transition-colors duration-200"
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
Saving...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
Next
|
||||||
|
<ChevronRight className="w-4 h-4" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={handleFinish}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="flex items-center gap-2 px-6 py-3 bg-green-600 hover:bg-green-700 disabled:bg-green-400 disabled:cursor-not-allowed text-white font-medium rounded-lg transition-colors duration-200"
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
Completing...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Check className="w-4 h-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}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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={`border rounded-lg p-4 transition-colors ${containerClassName}`}>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${iconContainerClassName}`}>
|
||||||
|
<SessionProviderLogo provider={provider} className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-foreground flex items-center gap-2">
|
||||||
|
{title}
|
||||||
|
{status.authenticated && <Check className="w-4 h-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} text-white text-sm font-medium py-2 px-4 rounded-lg transition-colors`}
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import AgentConnectionCard from './AgentConnectionCard';
|
||||||
|
import type { CliProvider, ProviderStatusMap } from '../types';
|
||||||
|
|
||||||
|
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="text-center mb-6">
|
||||||
|
<h2 className="text-2xl font-bold text-foreground mb-2">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="text-center text-sm text-muted-foreground pt-2">
|
||||||
|
<p>You can configure these later in Settings.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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="text-center mb-8">
|
||||||
|
<div className="w-16 h-16 bg-blue-100 dark:bg-blue-900/30 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<GitBranch className="w-8 h-8 text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold text-foreground mb-2">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="flex items-center gap-2 text-sm font-medium text-foreground mb-2">
|
||||||
|
<User className="w-4 h-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 px-4 py-3 border border-border rounded-lg bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
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="flex items-center gap-2 text-sm font-medium text-foreground mb-2">
|
||||||
|
<Mail className="w-4 h-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 px-4 py-3 border border-border rounded-lg bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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-col items-center flex-1">
|
||||||
|
<div
|
||||||
|
className={`w-12 h-12 rounded-full flex items-center justify-center border-2 transition-colors duration-200 ${
|
||||||
|
isCompleted
|
||||||
|
? 'bg-green-500 border-green-500 text-white'
|
||||||
|
: isActive
|
||||||
|
? 'bg-blue-600 border-blue-600 text-white'
|
||||||
|
: 'bg-background border-border text-muted-foreground'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isCompleted ? <Check className="w-6 h-6" /> : <Icon className="w-6 h-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={`flex-1 h-0.5 mx-2 transition-colors duration-200 ${isCompleted ? 'bg-green-500' : 'bg-border'}`} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
src/components/onboarding/view/types.ts
Normal file
12
src/components/onboarding/view/types.ts
Normal 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>;
|
||||||
29
src/components/onboarding/view/utils.ts
Normal file
29
src/components/onboarding/view/utils.ts
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
1
src/components/provider-auth/types.ts
Normal file
1
src/components/provider-auth/types.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export type CliProvider = 'claude' | 'cursor' | 'codex' | 'gemini';
|
||||||
@@ -1,78 +1,109 @@
|
|||||||
import { X, ExternalLink, KeyRound } from 'lucide-react';
|
import { ExternalLink, KeyRound, X } from 'lucide-react';
|
||||||
import StandaloneShell from './standalone-shell/view/StandaloneShell';
|
import StandaloneShell from '../../standalone-shell/view/StandaloneShell';
|
||||||
import { IS_PLATFORM } from '../constants/config';
|
import { IS_PLATFORM } from '../../../constants/config';
|
||||||
|
import type { CliProvider } from '../types';
|
||||||
|
|
||||||
/**
|
type LoginModalProject = {
|
||||||
* Reusable login modal component for Claude, Cursor, Codex, and Gemini CLI authentication
|
name?: string;
|
||||||
*
|
displayName?: string;
|
||||||
* @param {Object} props
|
fullPath?: string;
|
||||||
* @param {boolean} props.isOpen - Whether the modal is visible
|
path?: string;
|
||||||
* @param {Function} props.onClose - Callback when modal is closed
|
[key: string]: unknown;
|
||||||
* @param {'claude'|'cursor'|'codex'|'gemini'} props.provider - Which CLI provider to authenticate with
|
};
|
||||||
* @param {Object} props.project - Project object containing name and path information
|
|
||||||
* @param {Function} props.onComplete - Callback when login process completes (receives exitCode)
|
type ProviderLoginModalProps = {
|
||||||
* @param {string} props.customCommand - Optional custom command to override defaults
|
isOpen: boolean;
|
||||||
* @param {boolean} props.isAuthenticated - Whether user is already authenticated (for re-auth flow)
|
onClose: () => void;
|
||||||
*/
|
provider?: CliProvider;
|
||||||
function LoginModal({
|
project?: LoginModalProject | null;
|
||||||
|
onComplete?: (exitCode: number) => void;
|
||||||
|
customCommand?: string;
|
||||||
|
isAuthenticated?: boolean;
|
||||||
|
isOnboarding?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProviderCommand = ({
|
||||||
|
provider,
|
||||||
|
customCommand,
|
||||||
|
isAuthenticated,
|
||||||
|
isOnboarding,
|
||||||
|
}: {
|
||||||
|
provider: CliProvider;
|
||||||
|
customCommand?: string;
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
isOnboarding: boolean;
|
||||||
|
}) => {
|
||||||
|
if (customCommand) {
|
||||||
|
return customCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provider === 'claude') {
|
||||||
|
if (isAuthenticated) {
|
||||||
|
return 'claude setup-token --dangerously-skip-permissions';
|
||||||
|
}
|
||||||
|
return isOnboarding
|
||||||
|
? 'claude /exit --dangerously-skip-permissions'
|
||||||
|
: 'claude /login --dangerously-skip-permissions';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provider === 'cursor') {
|
||||||
|
return 'cursor-agent login';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provider === 'codex') {
|
||||||
|
return IS_PLATFORM ? 'codex login --device-auth' : 'codex login';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'gemini status';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProviderTitle = (provider: CliProvider) => {
|
||||||
|
if (provider === 'claude') return 'Claude CLI Login';
|
||||||
|
if (provider === 'cursor') return 'Cursor CLI Login';
|
||||||
|
if (provider === 'codex') return 'Codex CLI Login';
|
||||||
|
return 'Gemini CLI Configuration';
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeProject = (project?: LoginModalProject | null) => {
|
||||||
|
const normalizedName = project?.name || 'default';
|
||||||
|
const normalizedFullPath = project?.fullPath ?? project?.path ?? (IS_PLATFORM ? '/workspace' : '');
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: normalizedName,
|
||||||
|
displayName: project?.displayName || normalizedName,
|
||||||
|
fullPath: normalizedFullPath,
|
||||||
|
path: project?.path ?? normalizedFullPath,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ProviderLoginModal({
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
provider = 'claude',
|
provider = 'claude',
|
||||||
project,
|
project = null,
|
||||||
onComplete,
|
onComplete,
|
||||||
customCommand,
|
customCommand,
|
||||||
isAuthenticated = false,
|
isAuthenticated = false,
|
||||||
isOnboarding = false
|
isOnboarding = false,
|
||||||
}) {
|
}: ProviderLoginModalProps) {
|
||||||
if (!isOpen) return null;
|
if (!isOpen) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const getCommand = () => {
|
const command = getProviderCommand({ provider, customCommand, isAuthenticated, isOnboarding });
|
||||||
if (customCommand) return customCommand;
|
const title = getProviderTitle(provider);
|
||||||
|
const shellProject = normalizeProject(project);
|
||||||
|
|
||||||
switch (provider) {
|
const handleComplete = (exitCode: number) => {
|
||||||
case 'claude':
|
onComplete?.(exitCode);
|
||||||
return isAuthenticated ? 'claude setup-token --dangerously-skip-permissions' : isOnboarding ? 'claude /exit --dangerously-skip-permissions' : 'claude /login --dangerously-skip-permissions';
|
// Keep the modal open so users can read terminal output before closing.
|
||||||
case 'cursor':
|
|
||||||
return 'cursor-agent login';
|
|
||||||
case 'codex':
|
|
||||||
return IS_PLATFORM ? 'codex login --device-auth' : 'codex login';
|
|
||||||
case 'gemini':
|
|
||||||
// No explicit interactive login command for gemini CLI exists yet similar to Claude, so we'll just check status or instruct the user to configure `.gemini.json`
|
|
||||||
return 'gemini status';
|
|
||||||
default:
|
|
||||||
return isAuthenticated ? 'claude setup-token --dangerously-skip-permissions' : isOnboarding ? 'claude /exit --dangerously-skip-permissions' : 'claude /login --dangerously-skip-permissions';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTitle = () => {
|
|
||||||
switch (provider) {
|
|
||||||
case 'claude':
|
|
||||||
return 'Claude CLI Login';
|
|
||||||
case 'cursor':
|
|
||||||
return 'Cursor CLI Login';
|
|
||||||
case 'codex':
|
|
||||||
return 'Codex CLI Login';
|
|
||||||
case 'gemini':
|
|
||||||
return 'Gemini CLI Configuration';
|
|
||||||
default:
|
|
||||||
return 'CLI Login';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleComplete = (exitCode) => {
|
|
||||||
if (onComplete) {
|
|
||||||
onComplete(exitCode);
|
|
||||||
}
|
|
||||||
// Keep modal open so users can read login output and close explicitly.
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999] max-md:items-stretch max-md:justify-stretch">
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999] max-md:items-stretch max-md:justify-stretch">
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-4xl h-3/4 flex flex-col md:max-w-4xl md:h-3/4 md:rounded-lg md:m-4 max-md:max-w-none max-md:h-full max-md:rounded-none max-md:m-0">
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-4xl h-3/4 flex flex-col md:max-w-4xl md:h-3/4 md:rounded-lg md:m-4 max-md:max-w-none max-md:h-full max-md:rounded-none max-md:m-0">
|
||||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">{title}</h3>
|
||||||
{getTitle()}
|
|
||||||
</h3>
|
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
||||||
@@ -81,6 +112,7 @@ function LoginModal({
|
|||||||
<X className="w-6 h-6" />
|
<X className="w-6 h-6" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
{provider === 'gemini' ? (
|
{provider === 'gemini' ? (
|
||||||
<div className="flex flex-col items-center justify-center h-full p-8 text-center bg-gray-50 dark:bg-gray-900/50">
|
<div className="flex flex-col items-center justify-center h-full p-8 text-center bg-gray-50 dark:bg-gray-900/50">
|
||||||
@@ -88,12 +120,10 @@ function LoginModal({
|
|||||||
<KeyRound className="w-8 h-8 text-blue-600 dark:text-blue-400" />
|
<KeyRound className="w-8 h-8 text-blue-600 dark:text-blue-400" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4 className="text-xl font-medium text-gray-900 dark:text-white mb-3">
|
<h4 className="text-xl font-medium text-gray-900 dark:text-white mb-3">Setup Gemini API Access</h4>
|
||||||
Setup Gemini API Access
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
<p className="text-gray-600 dark:text-gray-400 max-w-md mb-8">
|
<p className="text-gray-600 dark:text-gray-400 max-w-md mb-8">
|
||||||
The Gemini CLI requires an API key to function. Unlike Claude, you'll need to configure this directly in your terminal first.
|
The Gemini CLI requires an API key to function. Configure it in your terminal first.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 max-w-lg w-full text-left shadow-sm">
|
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 max-w-lg w-full text-left shadow-sm">
|
||||||
@@ -103,7 +133,7 @@ function LoginModal({
|
|||||||
1
|
1
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-gray-900 dark:text-white mb-1">Get your API Key</p>
|
<p className="text-sm font-medium text-gray-900 dark:text-white mb-1">Get your API key</p>
|
||||||
<a
|
<a
|
||||||
href="https://aistudio.google.com/app/apikey"
|
href="https://aistudio.google.com/app/apikey"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -137,17 +167,10 @@ function LoginModal({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<StandaloneShell
|
<StandaloneShell project={shellProject} command={command} onComplete={handleComplete} minimal={true} />
|
||||||
project={project}
|
|
||||||
command={getCommand()}
|
|
||||||
onComplete={handleComplete}
|
|
||||||
minimal={true}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LoginModal;
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Settings as SettingsIcon, X } from 'lucide-react';
|
import { Settings as SettingsIcon, X } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import LoginModal from '../../LoginModal';
|
import ProviderLoginModal from '../../provider-auth/view/ProviderLoginModal';
|
||||||
import { Button } from '../../../shared/view/ui';
|
import { Button } from '../../../shared/view/ui';
|
||||||
import ClaudeMcpFormModal from '../view/modals/ClaudeMcpFormModal';
|
import ClaudeMcpFormModal from '../view/modals/ClaudeMcpFormModal';
|
||||||
import CodexMcpFormModal from '../view/modals/CodexMcpFormModal';
|
import CodexMcpFormModal from '../view/modals/CodexMcpFormModal';
|
||||||
@@ -11,18 +11,7 @@ import CredentialsSettingsTab from '../view/tabs/api-settings/CredentialsSetting
|
|||||||
import GitSettingsTab from '../view/tabs/git-settings/GitSettingsTab';
|
import GitSettingsTab from '../view/tabs/git-settings/GitSettingsTab';
|
||||||
import TasksSettingsTab from '../view/tabs/tasks-settings/TasksSettingsTab';
|
import TasksSettingsTab from '../view/tabs/tasks-settings/TasksSettingsTab';
|
||||||
import { useSettingsController } from '../hooks/useSettingsController';
|
import { useSettingsController } from '../hooks/useSettingsController';
|
||||||
import type { AgentProvider, SettingsProject, SettingsProps } from '../types/types';
|
import type { SettingsProps } from '../types/types';
|
||||||
|
|
||||||
type LoginModalProps = {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
provider: AgentProvider | '';
|
|
||||||
project: SettingsProject | null;
|
|
||||||
onComplete: (exitCode: number) => void;
|
|
||||||
isAuthenticated: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const LoginModalComponent = LoginModal as unknown as (props: LoginModalProps) => JSX.Element;
|
|
||||||
|
|
||||||
function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: SettingsProps) {
|
function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: SettingsProps) {
|
||||||
const { t } = useTranslation('settings');
|
const { t } = useTranslation('settings');
|
||||||
@@ -225,11 +214,11 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LoginModalComponent
|
<ProviderLoginModal
|
||||||
key={loginProvider}
|
key={loginProvider || 'claude'}
|
||||||
isOpen={showLoginModal}
|
isOpen={showLoginModal}
|
||||||
onClose={() => setShowLoginModal(false)}
|
onClose={() => setShowLoginModal(false)}
|
||||||
provider={loginProvider}
|
provider={loginProvider || 'claude'}
|
||||||
project={selectedProject}
|
project={selectedProject}
|
||||||
onComplete={handleLoginComplete}
|
onComplete={handleLoginComplete}
|
||||||
isAuthenticated={isAuthenticated}
|
isAuthenticated={isAuthenticated}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Button } from '../../../../shared/view/ui';
|
|||||||
import { Check, ChevronDown, ChevronRight, Edit3, Folder, FolderOpen, Star, Trash2, X } from 'lucide-react';
|
import { Check, ChevronDown, ChevronRight, Edit3, Folder, FolderOpen, Star, Trash2, X } from 'lucide-react';
|
||||||
import type { TFunction } from 'i18next';
|
import type { TFunction } from 'i18next';
|
||||||
import { cn } from '../../../../lib/utils';
|
import { cn } from '../../../../lib/utils';
|
||||||
import TaskIndicator from '../../../TaskIndicator';
|
import TaskIndicator from './TaskIndicator';
|
||||||
import type { Project, ProjectSession, SessionProvider } from '../../../../types/app';
|
import type { Project, ProjectSession, SessionProvider } from '../../../../types/app';
|
||||||
import type { MCPServerStatus, SessionWithProvider, TouchHandlerFactory } from '../../types/types';
|
import type { MCPServerStatus, SessionWithProvider, TouchHandlerFactory } from '../../types/types';
|
||||||
import { getTaskIndicatorStatus } from '../../utils/utils';
|
import { getTaskIndicatorStatus } from '../../utils/utils';
|
||||||
|
|||||||
123
src/components/sidebar/view/subcomponents/TaskIndicator.tsx
Normal file
123
src/components/sidebar/view/subcomponents/TaskIndicator.tsx
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import { AlertCircle, CheckCircle, Settings, X } from 'lucide-react';
|
||||||
|
import type { LucideIcon } from 'lucide-react';
|
||||||
|
import { cn } from '../../../../lib/utils';
|
||||||
|
|
||||||
|
type TaskIndicatorStatus =
|
||||||
|
| 'fully-configured'
|
||||||
|
| 'taskmaster-only'
|
||||||
|
| 'mcp-only'
|
||||||
|
| 'not-configured'
|
||||||
|
| 'error';
|
||||||
|
|
||||||
|
type TaskIndicatorSize = 'xs' | 'sm' | 'md' | 'lg';
|
||||||
|
|
||||||
|
type TaskIndicatorProps = {
|
||||||
|
status?: TaskIndicatorStatus;
|
||||||
|
size?: TaskIndicatorSize;
|
||||||
|
className?: string;
|
||||||
|
showLabel?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type IndicatorConfig = {
|
||||||
|
icon: LucideIcon;
|
||||||
|
colorClassName: string;
|
||||||
|
backgroundClassName: string;
|
||||||
|
label: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizeClassNames: Record<TaskIndicatorSize, string> = {
|
||||||
|
xs: 'w-3 h-3',
|
||||||
|
sm: 'w-4 h-4',
|
||||||
|
md: 'w-5 h-5',
|
||||||
|
lg: 'w-6 h-6',
|
||||||
|
};
|
||||||
|
|
||||||
|
const paddingClassNames: Record<TaskIndicatorSize, string> = {
|
||||||
|
xs: 'p-0.5',
|
||||||
|
sm: 'p-1',
|
||||||
|
md: 'p-1.5',
|
||||||
|
lg: 'p-2',
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIndicatorConfig = (status: TaskIndicatorStatus): IndicatorConfig => {
|
||||||
|
// Keep color and label mapping centralized so status display remains consistent in sidebar UIs.
|
||||||
|
if (status === 'fully-configured') {
|
||||||
|
return {
|
||||||
|
icon: CheckCircle,
|
||||||
|
colorClassName: 'text-green-500 dark:text-green-400',
|
||||||
|
backgroundClassName: 'bg-green-50 dark:bg-green-950',
|
||||||
|
label: 'TaskMaster Ready',
|
||||||
|
title: 'TaskMaster fully configured with MCP server',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'taskmaster-only') {
|
||||||
|
return {
|
||||||
|
icon: Settings,
|
||||||
|
colorClassName: 'text-blue-500 dark:text-blue-400',
|
||||||
|
backgroundClassName: 'bg-blue-50 dark:bg-blue-950',
|
||||||
|
label: 'TaskMaster Init',
|
||||||
|
title: 'TaskMaster initialized, MCP server needs setup',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'mcp-only') {
|
||||||
|
return {
|
||||||
|
icon: AlertCircle,
|
||||||
|
colorClassName: 'text-amber-500 dark:text-amber-400',
|
||||||
|
backgroundClassName: 'bg-amber-50 dark:bg-amber-950',
|
||||||
|
label: 'MCP Ready',
|
||||||
|
title: 'MCP server configured, TaskMaster needs initialization',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
icon: X,
|
||||||
|
colorClassName: 'text-gray-400 dark:text-gray-500',
|
||||||
|
backgroundClassName: 'bg-gray-50 dark:bg-gray-900',
|
||||||
|
label: 'No TaskMaster',
|
||||||
|
title: 'TaskMaster not configured',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function TaskIndicator({
|
||||||
|
status = 'not-configured',
|
||||||
|
size = 'sm',
|
||||||
|
className = '',
|
||||||
|
showLabel = false,
|
||||||
|
}: TaskIndicatorProps) {
|
||||||
|
const indicatorConfig = getIndicatorConfig(status);
|
||||||
|
const Icon = indicatorConfig.icon;
|
||||||
|
|
||||||
|
if (showLabel) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'inline-flex items-center gap-1.5 text-xs rounded-md px-2 py-1 transition-colors',
|
||||||
|
indicatorConfig.backgroundClassName,
|
||||||
|
indicatorConfig.colorClassName,
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
title={indicatorConfig.title}
|
||||||
|
>
|
||||||
|
<Icon className={sizeClassNames[size]} />
|
||||||
|
<span className="font-medium">{indicatorConfig.label}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'inline-flex items-center justify-center rounded-full transition-colors',
|
||||||
|
indicatorConfig.backgroundClassName,
|
||||||
|
paddingClassNames[size],
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
title={indicatorConfig.title}
|
||||||
|
>
|
||||||
|
<Icon className={cn(sizeClassNames[size], indicatorConfig.colorClassName)} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user