diff --git a/src/components/NextTaskBanner.jsx b/src/components/NextTaskBanner.jsx
deleted file mode 100644
index 12c4ac47..00000000
--- a/src/components/NextTaskBanner.jsx
+++ /dev/null
@@ -1,3 +0,0 @@
-import NextTaskBanner from './task-master/view/NextTaskBanner';
-
-export default NextTaskBanner;
diff --git a/src/components/Onboarding.jsx b/src/components/Onboarding.jsx
deleted file mode 100644
index 556e3c77..00000000
--- a/src/components/Onboarding.jsx
+++ /dev/null
@@ -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 (
-
-
-
-
-
-
Git Configuration
-
- Configure your git identity to ensure proper attribution for your commits
-
-
-
-
-
-
-
- Git Name *
-
-
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}
- />
-
- This will be used as: git config --global user.name
-
-
-
-
-
-
- Git Email *
-
-
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}
- />
-
- This will be used as: git config --global user.email
-
-
-
-
- );
-
- case 1:
- return (
-
-
-
Connect Your AI Agents
-
- Login to one or more AI coding assistants. All are optional.
-
-
-
- {/* Agent Cards Grid */}
-
- {/* Claude */}
-
-
-
-
-
-
-
-
- Claude Code
- {claudeAuthStatus.authenticated && }
-
-
- {claudeAuthStatus.loading ? 'Checking...' :
- claudeAuthStatus.authenticated ? claudeAuthStatus.email || 'Connected' : 'Not connected'}
-
-
-
- {!claudeAuthStatus.authenticated && !claudeAuthStatus.loading && (
-
- Login
-
- )}
-
-
-
- {/* Cursor */}
-
-
-
-
-
-
-
-
- Cursor
- {cursorAuthStatus.authenticated && }
-
-
- {cursorAuthStatus.loading ? 'Checking...' :
- cursorAuthStatus.authenticated ? cursorAuthStatus.email || 'Connected' : 'Not connected'}
-
-
-
- {!cursorAuthStatus.authenticated && !cursorAuthStatus.loading && (
-
- Login
-
- )}
-
-
-
- {/* Codex */}
-
-
-
-
-
-
-
-
- OpenAI Codex
- {codexAuthStatus.authenticated && }
-
-
- {codexAuthStatus.loading ? 'Checking...' :
- codexAuthStatus.authenticated ? codexAuthStatus.email || 'Connected' : 'Not connected'}
-
-
-
- {!codexAuthStatus.authenticated && !codexAuthStatus.loading && (
-
- Login
-
- )}
-
-
-
- {/* Gemini */}
-
-
-
-
-
-
-
-
- Gemini
- {geminiAuthStatus.authenticated && }
-
-
- {geminiAuthStatus.loading ? 'Checking...' :
- geminiAuthStatus.authenticated ? geminiAuthStatus.email || 'Connected' : 'Not connected'}
-
-
-
- {!geminiAuthStatus.authenticated && !geminiAuthStatus.loading && (
-
- Login
-
- )}
-
-
-
-
-
-
You can configure these later in Settings.
-
-
- );
-
- 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 (
- <>
-
-
- {/* Progress Steps */}
-
-
- {steps.map((step, index) => (
-
-
-
- {index < currentStep ? (
-
- ) : typeof step.icon === 'function' ? (
-
- ) : (
-
- )}
-
-
-
- {step.title}
-
- {step.required && (
-
Required
- )}
-
-
- {index < steps.length - 1 && (
-
- )}
-
- ))}
-
-
-
- {/* Main Card */}
-
- {renderStepContent()}
-
- {/* Error Message */}
- {error && (
-
- )}
-
- {/* Navigation Buttons */}
-
-
-
- Previous
-
-
-
- {currentStep < steps.length - 1 ? (
-
- {isSubmitting ? (
- <>
-
- Saving...
- >
- ) : (
- <>
- Next
-
- >
- )}
-
- ) : (
-
- {isSubmitting ? (
- <>
-
- Completing...
- >
- ) : (
- <>
-
- Complete Setup
- >
- )}
-
- )}
-
-
-
-
-
-
- {activeLoginProvider && (
- setActiveLoginProvider(null)}
- provider={activeLoginProvider}
- project={selectedProject}
- onComplete={handleLoginComplete}
- isOnboarding={true}
- />
- )}
- >
- );
-};
-
-export default Onboarding;
diff --git a/src/components/TaskIndicator.jsx b/src/components/TaskIndicator.jsx
deleted file mode 100644
index 9b12b48a..00000000
--- a/src/components/TaskIndicator.jsx
+++ /dev/null
@@ -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 (
-
-
- {config.label}
-
- );
- }
-
- return (
-
-
-
- );
-};
-
-export default TaskIndicator;
\ No newline at end of file
diff --git a/src/components/auth/view/ProtectedRoute.tsx b/src/components/auth/view/ProtectedRoute.tsx
index 10431e5c..bd76482a 100644
--- a/src/components/auth/view/ProtectedRoute.tsx
+++ b/src/components/auth/view/ProtectedRoute.tsx
@@ -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 = {
diff --git a/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx b/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx
index e17b9a4b..6c26a391 100644
--- a/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx
+++ b/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx
@@ -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;
diff --git a/src/components/git-panel/view/changes/FileChangeItem.tsx b/src/components/git-panel/view/changes/FileChangeItem.tsx
index e07a563b..7f5982d2 100644
--- a/src/components/git-panel/view/changes/FileChangeItem.tsx
+++ b/src/components/git-panel/view/changes/FileChangeItem.tsx
@@ -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;
diff --git a/src/components/git-panel/view/history/CommitHistoryItem.tsx b/src/components/git-panel/view/history/CommitHistoryItem.tsx
index 937bd90a..1d941492 100644
--- a/src/components/git-panel/view/history/CommitHistoryItem.tsx
+++ b/src/components/git-panel/view/history/CommitHistoryItem.tsx
@@ -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 = {
diff --git a/src/components/git-panel/view/shared/DiffViewer.tsx b/src/components/git-panel/view/shared/GitDiffViewer.tsx
similarity index 100%
rename from src/components/git-panel/view/shared/DiffViewer.tsx
rename to src/components/git-panel/view/shared/GitDiffViewer.tsx
diff --git a/src/components/onboarding/view/Onboarding.tsx b/src/components/onboarding/view/Onboarding.tsx
new file mode 100644
index 00000000..8b49d705
--- /dev/null
+++ b/src/components/onboarding/view/Onboarding.tsx
@@ -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;
+};
+
+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(null);
+ const [providerStatuses, setProviderStatuses] = useState(createInitialProviderStatuses);
+
+ const previousActiveLoginProviderRef = useRef(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 (
+ <>
+
+
+
+
+
+ {currentStep === 0 ? (
+
+ ) : (
+
+ )}
+
+ {errorMessage && (
+
+ )}
+
+
+
+
+ Previous
+
+
+
+ {currentStep < 1 ? (
+
+ {isSubmitting ? (
+ <>
+
+ Saving...
+ >
+ ) : (
+ <>
+ Next
+
+ >
+ )}
+
+ ) : (
+
+ {isSubmitting ? (
+ <>
+
+ Completing...
+ >
+ ) : (
+ <>
+
+ Complete Setup
+ >
+ )}
+
+ )}
+
+
+
+
+
+
+ {activeLoginProvider && (
+ setActiveLoginProvider(null)}
+ provider={activeLoginProvider}
+ project={selectedProject}
+ onComplete={handleLoginComplete}
+ isOnboarding={true}
+ />
+ )}
+ >
+ );
+}
diff --git a/src/components/onboarding/view/subcomponents/AgentConnectionCard.tsx b/src/components/onboarding/view/subcomponents/AgentConnectionCard.tsx
new file mode 100644
index 00000000..cdd99c39
--- /dev/null
+++ b/src/components/onboarding/view/subcomponents/AgentConnectionCard.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+ {title}
+ {status.authenticated && }
+
+
{statusText}
+
+
+
+ {!status.authenticated && !status.loading && (
+
+ Login
+
+ )}
+
+
+ );
+}
diff --git a/src/components/onboarding/view/subcomponents/AgentConnectionsStep.tsx b/src/components/onboarding/view/subcomponents/AgentConnectionsStep.tsx
new file mode 100644
index 00000000..bdcfb538
--- /dev/null
+++ b/src/components/onboarding/view/subcomponents/AgentConnectionsStep.tsx
@@ -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 (
+
+
+
Connect Your AI Agents
+
+ Login to one or more AI coding assistants. All are optional.
+
+
+
+
+ {providerCards.map((providerCard) => (
+
onOpenProviderLogin(providerCard.provider)}
+ />
+ ))}
+
+
+
+
You can configure these later in Settings.
+
+
+ );
+}
diff --git a/src/components/onboarding/view/subcomponents/GitConfigurationStep.tsx b/src/components/onboarding/view/subcomponents/GitConfigurationStep.tsx
new file mode 100644
index 00000000..5ab23f36
--- /dev/null
+++ b/src/components/onboarding/view/subcomponents/GitConfigurationStep.tsx
@@ -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 (
+
+
+
+
+
+
Git Configuration
+
+ Configure your git identity to ensure proper attribution for commits.
+
+
+
+
+
+
+
+ Git Name *
+
+
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}
+ />
+
Saved as `git config --global user.name`.
+
+
+
+
+
+ Git Email *
+
+
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}
+ />
+
Saved as `git config --global user.email`.
+
+
+
+ );
+}
diff --git a/src/components/onboarding/view/subcomponents/OnboardingStepProgress.tsx b/src/components/onboarding/view/subcomponents/OnboardingStepProgress.tsx
new file mode 100644
index 00000000..7476664f
--- /dev/null
+++ b/src/components/onboarding/view/subcomponents/OnboardingStepProgress.tsx
@@ -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 (
+
+
+ {onboardingSteps.map((step, index) => {
+ const isCompleted = index < currentStep;
+ const isActive = index === currentStep;
+ const Icon = step.icon;
+
+ return (
+
+
+
+ {isCompleted ? : }
+
+
+
+
+ {step.title}
+
+ {step.required &&
Required }
+
+
+
+ {index < onboardingSteps.length - 1 && (
+
+ )}
+
+ );
+ })}
+
+
+ );
+}
diff --git a/src/components/onboarding/view/types.ts b/src/components/onboarding/view/types.ts
new file mode 100644
index 00000000..46800813
--- /dev/null
+++ b/src/components/onboarding/view/types.ts
@@ -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;
diff --git a/src/components/onboarding/view/utils.ts b/src/components/onboarding/view/utils.ts
new file mode 100644
index 00000000..adb3c4bc
--- /dev/null
+++ b/src/components/onboarding/view/utils.ts
@@ -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;
+ }
+};
diff --git a/src/components/provider-auth/types.ts b/src/components/provider-auth/types.ts
new file mode 100644
index 00000000..e39a9796
--- /dev/null
+++ b/src/components/provider-auth/types.ts
@@ -0,0 +1 @@
+export type CliProvider = 'claude' | 'cursor' | 'codex' | 'gemini';
diff --git a/src/components/LoginModal.jsx b/src/components/provider-auth/view/ProviderLoginModal.tsx
similarity index 57%
rename from src/components/LoginModal.jsx
rename to src/components/provider-auth/view/ProviderLoginModal.tsx
index 9106140f..5dad1c2c 100644
--- a/src/components/LoginModal.jsx
+++ b/src/components/provider-auth/view/ProviderLoginModal.tsx
@@ -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 (
-
- {getTitle()}
-
+ {title}
+
{provider === 'gemini' ? (
@@ -88,12 +120,10 @@ function LoginModal({
-
- Setup Gemini API Access
-
+
Setup Gemini API Access
- 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.
@@ -103,7 +133,7 @@ function LoginModal({
1
-
Get your API Key
+
Get your API key
) : (
-
+
)}
);
}
-
-export default LoginModal;
diff --git a/src/components/settings/view/Settings.tsx b/src/components/settings/view/Settings.tsx
index 2401dd7b..88cea76f 100644
--- a/src/components/settings/view/Settings.tsx
+++ b/src/components/settings/view/Settings.tsx
@@ -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
- setShowLoginModal(false)}
- provider={loginProvider}
+ provider={loginProvider || 'claude'}
project={selectedProject}
onComplete={handleLoginComplete}
isAuthenticated={isAuthenticated}
diff --git a/src/components/sidebar/view/subcomponents/SidebarProjectItem.tsx b/src/components/sidebar/view/subcomponents/SidebarProjectItem.tsx
index 94dc7919..82b85a0f 100644
--- a/src/components/sidebar/view/subcomponents/SidebarProjectItem.tsx
+++ b/src/components/sidebar/view/subcomponents/SidebarProjectItem.tsx
@@ -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';
diff --git a/src/components/sidebar/view/subcomponents/TaskIndicator.tsx b/src/components/sidebar/view/subcomponents/TaskIndicator.tsx
new file mode 100644
index 00000000..1bcf3169
--- /dev/null
+++ b/src/components/sidebar/view/subcomponents/TaskIndicator.tsx
@@ -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 = {
+ xs: 'w-3 h-3',
+ sm: 'w-4 h-4',
+ md: 'w-5 h-5',
+ lg: 'w-6 h-6',
+};
+
+const paddingClassNames: Record = {
+ 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 (
+
+
+ {indicatorConfig.label}
+
+ );
+ }
+
+ return (
+
+
+
+ );
+}