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 Onboarding from '../../Onboarding';
|
||||
import { IS_PLATFORM } from '../../../constants/config';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import AuthLoadingScreen from './AuthLoadingScreen';
|
||||
import LoginForm from './LoginForm';
|
||||
import Onboarding from '../../onboarding/view/Onboarding';
|
||||
import SetupForm from './SetupForm';
|
||||
|
||||
type ProtectedRouteProps = {
|
||||
|
||||
@@ -2,9 +2,9 @@ import React from 'react';
|
||||
import { Check, ChevronDown } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
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 type { ProjectSession, SessionProvider } from '../../../../types/app';
|
||||
import { NextTaskBanner } from '../../../task-master';
|
||||
|
||||
interface ProviderSelectionEmptyStateProps {
|
||||
selectedSession: ProjectSession | null;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ChevronRight, Trash2 } from 'lucide-react';
|
||||
import type { FileStatusCode } from '../../types/types';
|
||||
import { getStatusBadgeClass, getStatusLabel } from '../../utils/gitPanelUtils';
|
||||
import GitDiffViewer from '../shared/DiffViewer';
|
||||
import GitDiffViewer from '../shared/GitDiffViewer';
|
||||
|
||||
type FileChangeItemProps = {
|
||||
filePath: string;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
|
||||
import type { GitCommitSummary } from '../../types/types';
|
||||
import GitDiffViewer from '../shared/DiffViewer';
|
||||
import GitDiffViewer from '../shared/GitDiffViewer';
|
||||
|
||||
|
||||
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 StandaloneShell from './standalone-shell/view/StandaloneShell';
|
||||
import { IS_PLATFORM } from '../constants/config';
|
||||
import { ExternalLink, KeyRound, X } from 'lucide-react';
|
||||
import StandaloneShell from '../../standalone-shell/view/StandaloneShell';
|
||||
import { IS_PLATFORM } from '../../../constants/config';
|
||||
import type { CliProvider } from '../types';
|
||||
|
||||
/**
|
||||
* Reusable login modal component for Claude, Cursor, Codex, and Gemini CLI authentication
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {boolean} props.isOpen - Whether the modal is visible
|
||||
* @param {Function} props.onClose - Callback when modal is closed
|
||||
* @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)
|
||||
* @param {string} props.customCommand - Optional custom command to override defaults
|
||||
* @param {boolean} props.isAuthenticated - Whether user is already authenticated (for re-auth flow)
|
||||
*/
|
||||
function LoginModal({
|
||||
type LoginModalProject = {
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
fullPath?: string;
|
||||
path?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
type ProviderLoginModalProps = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
provider?: CliProvider;
|
||||
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,
|
||||
onClose,
|
||||
provider = 'claude',
|
||||
project,
|
||||
project = null,
|
||||
onComplete,
|
||||
customCommand,
|
||||
isAuthenticated = false,
|
||||
isOnboarding = false
|
||||
}) {
|
||||
if (!isOpen) return null;
|
||||
isOnboarding = false,
|
||||
}: ProviderLoginModalProps) {
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const getCommand = () => {
|
||||
if (customCommand) return customCommand;
|
||||
const command = getProviderCommand({ provider, customCommand, isAuthenticated, isOnboarding });
|
||||
const title = getProviderTitle(provider);
|
||||
const shellProject = normalizeProject(project);
|
||||
|
||||
switch (provider) {
|
||||
case 'claude':
|
||||
return isAuthenticated ? 'claude setup-token --dangerously-skip-permissions' : isOnboarding ? 'claude /exit --dangerously-skip-permissions' : 'claude /login --dangerously-skip-permissions';
|
||||
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.
|
||||
const handleComplete = (exitCode: number) => {
|
||||
onComplete?.(exitCode);
|
||||
// Keep the modal open so users can read terminal output before closing.
|
||||
};
|
||||
|
||||
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="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">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{getTitle()}
|
||||
</h3>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">{title}</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
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" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{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">
|
||||
@@ -88,12 +120,10 @@ function LoginModal({
|
||||
<KeyRound className="w-8 h-8 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
|
||||
<h4 className="text-xl font-medium text-gray-900 dark:text-white mb-3">
|
||||
Setup Gemini API Access
|
||||
</h4>
|
||||
<h4 className="text-xl font-medium text-gray-900 dark:text-white mb-3">Setup Gemini API Access</h4>
|
||||
|
||||
<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>
|
||||
|
||||
<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
|
||||
</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
|
||||
href="https://aistudio.google.com/app/apikey"
|
||||
target="_blank"
|
||||
@@ -137,17 +167,10 @@ function LoginModal({
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<StandaloneShell
|
||||
project={project}
|
||||
command={getCommand()}
|
||||
onComplete={handleComplete}
|
||||
minimal={true}
|
||||
/>
|
||||
<StandaloneShell project={shellProject} command={command} onComplete={handleComplete} minimal={true} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LoginModal;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Settings as SettingsIcon, X } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import LoginModal from '../../LoginModal';
|
||||
import ProviderLoginModal from '../../provider-auth/view/ProviderLoginModal';
|
||||
import { Button } from '../../../shared/view/ui';
|
||||
import ClaudeMcpFormModal from '../view/modals/ClaudeMcpFormModal';
|
||||
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 TasksSettingsTab from '../view/tabs/tasks-settings/TasksSettingsTab';
|
||||
import { useSettingsController } from '../hooks/useSettingsController';
|
||||
import type { AgentProvider, SettingsProject, 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;
|
||||
import type { SettingsProps } from '../types/types';
|
||||
|
||||
function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: SettingsProps) {
|
||||
const { t } = useTranslation('settings');
|
||||
@@ -225,11 +214,11 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LoginModalComponent
|
||||
key={loginProvider}
|
||||
<ProviderLoginModal
|
||||
key={loginProvider || 'claude'}
|
||||
isOpen={showLoginModal}
|
||||
onClose={() => setShowLoginModal(false)}
|
||||
provider={loginProvider}
|
||||
provider={loginProvider || 'claude'}
|
||||
project={selectedProject}
|
||||
onComplete={handleLoginComplete}
|
||||
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 type { TFunction } from 'i18next';
|
||||
import { cn } from '../../../../lib/utils';
|
||||
import TaskIndicator from '../../../TaskIndicator';
|
||||
import TaskIndicator from './TaskIndicator';
|
||||
import type { Project, ProjectSession, SessionProvider } from '../../../../types/app';
|
||||
import type { MCPServerStatus, SessionWithProvider, TouchHandlerFactory } from '../../types/types';
|
||||
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