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:
Haileyesus
2026-03-02 22:58:50 +03:00
parent ff176a9368
commit b5f7d2eada
20 changed files with 815 additions and 772 deletions

View File

@@ -1,3 +0,0 @@
import NextTaskBanner from './task-master/view/NextTaskBanner';
export default NextTaskBanner;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 = {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 = {

View File

@@ -0,0 +1,289 @@
import { Check, ChevronLeft, ChevronRight, Loader2 } from 'lucide-react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { authenticatedFetch } from '../../../utils/api';
import ProviderLoginModal from '../../provider-auth/view/ProviderLoginModal';
import AgentConnectionsStep from './subcomponents/AgentConnectionsStep';
import GitConfigurationStep from './subcomponents/GitConfigurationStep';
import OnboardingStepProgress from './subcomponents/OnboardingStepProgress';
import type { CliProvider, ProviderStatusMap } from './types';
import {
cliProviders,
createInitialProviderStatuses,
gitEmailPattern,
readErrorMessageFromResponse,
selectedProject,
} from './utils';
type OnboardingProps = {
onComplete?: () => void | Promise<void>;
};
export default function Onboarding({ onComplete }: OnboardingProps) {
const [currentStep, setCurrentStep] = useState(0);
const [gitName, setGitName] = useState('');
const [gitEmail, setGitEmail] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const [activeLoginProvider, setActiveLoginProvider] = useState<CliProvider | null>(null);
const [providerStatuses, setProviderStatuses] = useState<ProviderStatusMap>(createInitialProviderStatuses);
const previousActiveLoginProviderRef = useRef<CliProvider | null | undefined>(undefined);
const checkProviderAuthStatus = useCallback(async (provider: CliProvider) => {
try {
const response = await authenticatedFetch(`/api/cli/${provider}/status`);
if (!response.ok) {
setProviderStatuses((previous) => ({
...previous,
[provider]: {
authenticated: false,
email: null,
loading: false,
error: 'Failed to check authentication status',
},
}));
return;
}
const payload = (await response.json()) as {
authenticated?: boolean;
email?: string | null;
error?: string | null;
};
setProviderStatuses((previous) => ({
...previous,
[provider]: {
authenticated: Boolean(payload.authenticated),
email: payload.email ?? null,
loading: false,
error: payload.error ?? null,
},
}));
} catch (caughtError) {
console.error(`Error checking ${provider} auth status:`, caughtError);
setProviderStatuses((previous) => ({
...previous,
[provider]: {
authenticated: false,
email: null,
loading: false,
error: caughtError instanceof Error ? caughtError.message : 'Unknown error',
},
}));
}
}, []);
const refreshAllProviderStatuses = useCallback(async () => {
await Promise.all(cliProviders.map((provider) => checkProviderAuthStatus(provider)));
}, [checkProviderAuthStatus]);
const loadGitConfig = useCallback(async () => {
try {
const response = await authenticatedFetch('/api/user/git-config');
if (!response.ok) {
return;
}
const payload = (await response.json()) as { gitName?: string; gitEmail?: string };
if (payload.gitName) {
setGitName(payload.gitName);
}
if (payload.gitEmail) {
setGitEmail(payload.gitEmail);
}
} catch (caughtError) {
console.error('Error loading git config:', caughtError);
}
}, []);
useEffect(() => {
void loadGitConfig();
void refreshAllProviderStatuses();
}, [loadGitConfig, refreshAllProviderStatuses]);
useEffect(() => {
const previousProvider = previousActiveLoginProviderRef.current;
previousActiveLoginProviderRef.current = activeLoginProvider;
const isInitialMount = previousProvider === undefined;
const didCloseModal = previousProvider !== null && activeLoginProvider === null;
// Refresh statuses once on mount and again after the login modal is closed.
if (isInitialMount || didCloseModal) {
void refreshAllProviderStatuses();
}
}, [activeLoginProvider, refreshAllProviderStatuses]);
const handleProviderLoginOpen = (provider: CliProvider) => {
setActiveLoginProvider(provider);
};
const handleLoginComplete = (exitCode: number) => {
if (exitCode === 0 && activeLoginProvider) {
void checkProviderAuthStatus(activeLoginProvider);
}
};
const handleNextStep = async () => {
setErrorMessage('');
if (currentStep !== 0) {
setCurrentStep((previous) => previous + 1);
return;
}
if (!gitName.trim() || !gitEmail.trim()) {
setErrorMessage('Both git name and email are required.');
return;
}
if (!gitEmailPattern.test(gitEmail)) {
setErrorMessage('Please enter a valid email address.');
return;
}
setIsSubmitting(true);
try {
const response = await authenticatedFetch('/api/user/git-config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ gitName, gitEmail }),
});
if (!response.ok) {
const message = await readErrorMessageFromResponse(response, 'Failed to save git configuration');
throw new Error(message);
}
setCurrentStep((previous) => previous + 1);
} catch (caughtError) {
setErrorMessage(caughtError instanceof Error ? caughtError.message : 'Failed to save git configuration');
} finally {
setIsSubmitting(false);
}
};
const handlePreviousStep = () => {
setErrorMessage('');
setCurrentStep((previous) => previous - 1);
};
const handleFinish = async () => {
setIsSubmitting(true);
setErrorMessage('');
try {
const response = await authenticatedFetch('/api/user/complete-onboarding', { method: 'POST' });
if (!response.ok) {
const message = await readErrorMessageFromResponse(response, 'Failed to complete onboarding');
throw new Error(message);
}
await onComplete?.();
} catch (caughtError) {
setErrorMessage(caughtError instanceof Error ? caughtError.message : 'Failed to complete onboarding');
} finally {
setIsSubmitting(false);
}
};
const isCurrentStepValid = currentStep === 0
? Boolean(gitName.trim() && gitEmail.trim() && gitEmailPattern.test(gitEmail))
: true;
return (
<>
<div className="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}
/>
)}
</>
);
}

View File

@@ -0,0 +1,60 @@
import { Check } from 'lucide-react';
import SessionProviderLogo from '../../../llm-logo-provider/SessionProviderLogo';
import type { CliProvider, ProviderAuthStatus } from '../types';
type AgentConnectionCardProps = {
provider: CliProvider;
title: string;
status: ProviderAuthStatus;
connectedClassName: string;
iconContainerClassName: string;
loginButtonClassName: string;
onLogin: () => void;
};
export default function AgentConnectionCard({
provider,
title,
status,
connectedClassName,
iconContainerClassName,
loginButtonClassName,
onLogin,
}: AgentConnectionCardProps) {
const containerClassName = status.authenticated ? connectedClassName : 'border-border bg-card';
const statusText = status.loading
? 'Checking...'
: status.authenticated
? status.email || 'Connected'
: status.error || 'Not connected';
return (
<div className={`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>
);
}

View File

@@ -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>
);
}

View File

@@ -0,0 +1,69 @@
import { GitBranch, Mail, User } from 'lucide-react';
type GitConfigurationStepProps = {
gitName: string;
gitEmail: string;
isSubmitting: boolean;
onGitNameChange: (value: string) => void;
onGitEmailChange: (value: string) => void;
};
export default function GitConfigurationStep({
gitName,
gitEmail,
isSubmitting,
onGitNameChange,
onGitEmailChange,
}: GitConfigurationStepProps) {
return (
<div className="space-y-6">
<div className="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>
);
}

View File

@@ -0,0 +1,53 @@
import { Check, GitBranch, LogIn } from 'lucide-react';
type OnboardingStepProgressProps = {
currentStep: number;
};
const onboardingSteps = [
{ title: 'Git Configuration', icon: GitBranch, required: true },
{ title: 'Connect Agents', icon: LogIn, required: false },
];
export default function OnboardingStepProgress({ currentStep }: OnboardingStepProgressProps) {
return (
<div className="mb-8">
<div className="flex items-center justify-between">
{onboardingSteps.map((step, index) => {
const isCompleted = index < currentStep;
const isActive = index === currentStep;
const Icon = step.icon;
return (
<div key={step.title} className="contents">
<div className="flex flex-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>
);
}

View File

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

View File

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

View File

@@ -0,0 +1 @@
export type CliProvider = 'claude' | 'cursor' | 'codex' | 'gemini';

View File

@@ -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;

View File

@@ -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}

View File

@@ -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';

View 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>
);
}