mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-04-22 21:41:29 +00:00
Feature/backend ts support andunification of auth settings on frontend (#654)
* fix: remove project dependency from settings controller and onboarding * fix(settings): remove onClose prop from useSettingsController args * chore: tailwind classes order * refactor: move provider auth status management to custom hook * refactor: rename SessionProvider to LLMProvider * feat(frontend): support for @ alias based imports) * fix: replace init.sql with schema.js * fix: refactor database initialization to use schema.js for SQL statements * feat(server): add a real backend TypeScript build and enforce module boundaries The backend had started to grow beyond what the frontend-only tooling setup could support safely. We were still running server code directly from /server, linting mainly the client, and relying on path assumptions such as "../.." that only worked in the source layout. That created three problems: - backend alias imports were hard to resolve consistently in the editor, ESLint, and the runtime - server code had no enforced module boundary rules, so cross-module deep imports could bypass intended public entry points - building the backend into a separate output directory would break repo-level lookups for package.json, .env, dist, and public assets because those paths were derived from source-only relative assumptions This change makes the backend tooling explicit and runtime-safe. A dedicated backend TypeScript config now lives in server/tsconfig.json, with tsconfig.server.json reduced to a compatibility shim. This gives the language service and backend tooling a canonical project rooted in /server while still preserving top-level compatibility for any existing references. The backend alias mapping now resolves relative to /server, which avoids colliding with the frontend's "@/..." -> "src/*" mapping. The package scripts were updated so development runs through tsx with the backend tsconfig, build now produces a compiled backend in dist-server, and typecheck/lint cover both client and server. A new build-server.mjs script runs TypeScript and tsc-alias and cleans dist-server first, which prevents stale compiled files from shadowing current source files after refactors. To make the compiled backend behave the same as the source backend, runtime path resolution was centralized in server/utils/runtime-paths.js. Instead of assuming fixed relative paths from each module, server entry points now resolve the actual app root and server root at runtime. That keeps package.json, .env, dist, public, and default database paths stable whether code is executed from /server or from /dist-server/server. ESLint was expanded from a frontend-only setup into a backend-aware one. The backend now uses import resolution tied to the backend tsconfig so aliased imports resolve correctly in linting, import ordering matches the frontend style, and unused/duplicate imports are surfaced consistently. Most importantly, eslint-plugin-boundaries now enforces server module boundaries. Files under server/modules can no longer import another module's internals directly. Cross-module imports must go through that module's barrel file (index.ts/index.js). boundaries/no-unknown was also enabled so alias-resolution gaps cannot silently bypass the rule. Together, these changes make the backend buildable, keep runtime path resolution stable after compilation, align server tooling with the client where appropriate, and enforce a stricter modular architecture for server code. * fix: update package.json to include dist-server in files and remove tsconfig.server.json * refactor: remove build-server.mjs and inline its logic into package.json scripts * fix: update paths in package.json and bin.js to use dist-server directory * feat(eslint): add backend shared types and enforce compile-time contract for imports * fix(eslint): update shared types pattern --------- Co-authored-by: Haileyesus <something@gmail.com>
This commit is contained in:
@@ -19,7 +19,7 @@ import type {
|
||||
PendingPermissionRequest,
|
||||
PermissionMode,
|
||||
} from '../types/types';
|
||||
import type { Project, ProjectSession, SessionProvider } from '../../../types/app';
|
||||
import type { Project, ProjectSession, LLMProvider } from '../../../types/app';
|
||||
import { escapeRegExp } from '../utils/chatFormatting';
|
||||
import { useFileMentions } from './useFileMentions';
|
||||
import { type SlashCommand, useSlashCommands } from './useSlashCommands';
|
||||
@@ -33,7 +33,7 @@ interface UseChatComposerStateArgs {
|
||||
selectedProject: Project | null;
|
||||
selectedSession: ProjectSession | null;
|
||||
currentSessionId: string | null;
|
||||
provider: SessionProvider;
|
||||
provider: LLMProvider;
|
||||
permissionMode: PermissionMode | string;
|
||||
cyclePermissionMode: () => void;
|
||||
cursorModel: string;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { authenticatedFetch } from '../../../utils/api';
|
||||
import { CLAUDE_MODELS, CODEX_MODELS, CURSOR_MODELS, GEMINI_MODELS } from '../../../../shared/modelConstants';
|
||||
import type { PendingPermissionRequest, PermissionMode } from '../types/types';
|
||||
import type { ProjectSession, SessionProvider } from '../../../types/app';
|
||||
import type { ProjectSession, LLMProvider } from '../../../types/app';
|
||||
|
||||
interface UseChatProviderStateArgs {
|
||||
selectedSession: ProjectSession | null;
|
||||
@@ -11,8 +11,8 @@ interface UseChatProviderStateArgs {
|
||||
export function useChatProviderState({ selectedSession }: UseChatProviderStateArgs) {
|
||||
const [permissionMode, setPermissionMode] = useState<PermissionMode>('default');
|
||||
const [pendingPermissionRequests, setPendingPermissionRequests] = useState<PendingPermissionRequest[]>([]);
|
||||
const [provider, setProvider] = useState<SessionProvider>(() => {
|
||||
return (localStorage.getItem('selected-provider') as SessionProvider) || 'claude';
|
||||
const [provider, setProvider] = useState<LLMProvider>(() => {
|
||||
return (localStorage.getItem('selected-provider') as LLMProvider) || 'claude';
|
||||
});
|
||||
const [cursorModel, setCursorModel] = useState<string>(() => {
|
||||
return localStorage.getItem('cursor-model') || CURSOR_MODELS.DEFAULT;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
|
||||
import type { PendingPermissionRequest } from '../types/types';
|
||||
import type { Project, ProjectSession, SessionProvider } from '../../../types/app';
|
||||
import type { Project, ProjectSession, LLMProvider } from '../../../types/app';
|
||||
import type { SessionStore, NormalizedMessage } from '../../../stores/useSessionStore';
|
||||
|
||||
type PendingViewSession = {
|
||||
@@ -48,7 +48,7 @@ type LatestChatMessage = {
|
||||
|
||||
interface UseChatRealtimeHandlersArgs {
|
||||
latestMessage: LatestChatMessage | null;
|
||||
provider: SessionProvider;
|
||||
provider: LLMProvider;
|
||||
selectedProject: Project | null;
|
||||
selectedSession: ProjectSession | null;
|
||||
currentSessionId: string | null;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } fr
|
||||
import type { MutableRefObject } from 'react';
|
||||
import { authenticatedFetch } from '../../../utils/api';
|
||||
import type { ChatMessage, Provider } from '../types/types';
|
||||
import type { Project, ProjectSession, SessionProvider } from '../../../types/app';
|
||||
import type { Project, ProjectSession, LLMProvider } from '../../../types/app';
|
||||
import { createCachedDiffCalculator, type DiffCalculator } from '../utils/messageTransforms';
|
||||
import { normalizedToChatMessages } from './useChatMessages';
|
||||
import type { SessionStore, NormalizedMessage } from '../../../stores/useSessionStore';
|
||||
@@ -40,7 +40,7 @@ interface ScrollRestoreState {
|
||||
function chatMessageToNormalized(
|
||||
msg: ChatMessage,
|
||||
sessionId: string,
|
||||
provider: SessionProvider,
|
||||
provider: LLMProvider,
|
||||
): NormalizedMessage | null {
|
||||
const id = `local_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
const ts = msg.timestamp instanceof Date
|
||||
@@ -151,7 +151,7 @@ export function useChatSessionState({
|
||||
// When a real session ID arrives and we have a pending user message, flush it to the store
|
||||
const prevActiveSessionRef = useRef<string | null>(null);
|
||||
if (activeSessionId && activeSessionId !== prevActiveSessionRef.current && pendingUserMessage) {
|
||||
const prov = (localStorage.getItem('selected-provider') as SessionProvider) || 'claude';
|
||||
const prov = (localStorage.getItem('selected-provider') as LLMProvider) || 'claude';
|
||||
const normalized = chatMessageToNormalized(pendingUserMessage, activeSessionId, prov);
|
||||
if (normalized) {
|
||||
sessionStore.appendRealtime(activeSessionId, normalized);
|
||||
@@ -189,7 +189,7 @@ export function useChatSessionState({
|
||||
setPendingUserMessage(msg);
|
||||
return;
|
||||
}
|
||||
const prov = (localStorage.getItem('selected-provider') as SessionProvider) || 'claude';
|
||||
const prov = (localStorage.getItem('selected-provider') as LLMProvider) || 'claude';
|
||||
const normalized = chatMessageToNormalized(msg, activeSessionId, prov);
|
||||
if (normalized) {
|
||||
sessionStore.appendRealtime(activeSessionId, normalized);
|
||||
@@ -240,7 +240,7 @@ export function useChatSessionState({
|
||||
|
||||
try {
|
||||
const slot = await sessionStore.fetchMore(selectedSession.id, {
|
||||
provider: sessionProvider as SessionProvider,
|
||||
provider: sessionProvider as LLMProvider,
|
||||
projectName: selectedProject.name,
|
||||
projectPath: selectedProject.fullPath || selectedProject.path || '',
|
||||
limit: MESSAGES_PER_PAGE,
|
||||
@@ -374,7 +374,7 @@ export function useChatSessionState({
|
||||
// Fetch from server → store updates → chatMessages re-derives automatically
|
||||
setIsLoadingSessionMessages(true);
|
||||
sessionStore.fetchFromServer(selectedSession.id, {
|
||||
provider: (selectedSession.__provider || provider) as SessionProvider,
|
||||
provider: (selectedSession.__provider || provider) as LLMProvider,
|
||||
projectName: selectedProject.name,
|
||||
projectPath: selectedProject.fullPath || selectedProject.path || '',
|
||||
limit: MESSAGES_PER_PAGE,
|
||||
@@ -410,7 +410,7 @@ export function useChatSessionState({
|
||||
// Skip store refresh during active streaming
|
||||
if (!isLoading) {
|
||||
await sessionStore.refreshFromServer(selectedSession.id, {
|
||||
provider: (selectedSession.__provider || provider) as SessionProvider,
|
||||
provider: (selectedSession.__provider || provider) as LLMProvider,
|
||||
projectName: selectedProject.name,
|
||||
projectPath: selectedProject.fullPath || selectedProject.path || '',
|
||||
});
|
||||
@@ -468,7 +468,7 @@ export function useChatSessionState({
|
||||
try {
|
||||
// Load all messages into the store for search navigation
|
||||
const slot = await sessionStore.fetchFromServer(selectedSession.id, {
|
||||
provider: sessionProvider as SessionProvider,
|
||||
provider: sessionProvider as LLMProvider,
|
||||
projectName: selectedProject.name,
|
||||
projectPath: selectedProject.fullPath || selectedProject.path || '',
|
||||
limit: null,
|
||||
@@ -655,7 +655,7 @@ export function useChatSessionState({
|
||||
|
||||
try {
|
||||
const slot = await sessionStore.fetchFromServer(requestSessionId, {
|
||||
provider: sessionProvider as SessionProvider,
|
||||
provider: sessionProvider as LLMProvider,
|
||||
projectName: selectedProject.name,
|
||||
projectPath: selectedProject.fullPath || selectedProject.path || '',
|
||||
limit: null,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Project, ProjectSession, SessionProvider } from '../../../types/app';
|
||||
import type { Project, ProjectSession, LLMProvider } from '../../../types/app';
|
||||
|
||||
export type Provider = SessionProvider;
|
||||
export type Provider = LLMProvider;
|
||||
|
||||
export type PermissionMode = 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan';
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useTasksSettings } from '../../../contexts/TasksSettingsContext';
|
||||
import { QuickSettingsPanel } from '../../quick-settings-panel';
|
||||
import type { ChatInterfaceProps, Provider } from '../types/types';
|
||||
import type { SessionProvider } from '../../../types/app';
|
||||
import type { LLMProvider } from '../../../types/app';
|
||||
import { useChatProviderState } from '../hooks/useChatProviderState';
|
||||
import { useChatSessionState } from '../hooks/useChatSessionState';
|
||||
import { useChatRealtimeHandlers } from '../hooks/useChatRealtimeHandlers';
|
||||
@@ -206,9 +206,9 @@ function ChatInterface({
|
||||
// so missed streaming events are shown. Also reset isLoading.
|
||||
const handleWebSocketReconnect = useCallback(async () => {
|
||||
if (!selectedProject || !selectedSession) return;
|
||||
const providerVal = (localStorage.getItem('selected-provider') as SessionProvider) || 'claude';
|
||||
const providerVal = (localStorage.getItem('selected-provider') as LLMProvider) || 'claude';
|
||||
await sessionStore.refreshFromServer(selectedSession.id, {
|
||||
provider: (selectedSession.__provider || providerVal) as SessionProvider,
|
||||
provider: (selectedSession.__provider || providerVal) as LLMProvider,
|
||||
projectName: selectedProject.name,
|
||||
projectPath: selectedProject.fullPath || selectedProject.path || '',
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import type { Dispatch, RefObject, SetStateAction } from 'react';
|
||||
import type { ChatMessage } from '../../types/types';
|
||||
import type { Project, ProjectSession, SessionProvider } from '../../../../types/app';
|
||||
import type { Project, ProjectSession, LLMProvider } from '../../../../types/app';
|
||||
import { getIntrinsicMessageKey } from '../../utils/messageKeys';
|
||||
import MessageComponent from './MessageComponent';
|
||||
import ProviderSelectionEmptyState from './ProviderSelectionEmptyState';
|
||||
@@ -15,8 +15,8 @@ interface ChatMessagesPaneProps {
|
||||
chatMessages: ChatMessage[];
|
||||
selectedSession: ProjectSession | null;
|
||||
currentSessionId: string | null;
|
||||
provider: SessionProvider;
|
||||
setProvider: (provider: SessionProvider) => void;
|
||||
provider: LLMProvider;
|
||||
setProvider: (provider: LLMProvider) => void;
|
||||
textareaRef: RefObject<HTMLTextAreaElement>;
|
||||
claudeModel: string;
|
||||
setClaudeModel: (model: string) => void;
|
||||
|
||||
@@ -8,14 +8,14 @@ import {
|
||||
CODEX_MODELS,
|
||||
GEMINI_MODELS,
|
||||
} from "../../../../../shared/modelConstants";
|
||||
import type { ProjectSession, SessionProvider } from "../../../../types/app";
|
||||
import type { ProjectSession, LLMProvider } from "../../../../types/app";
|
||||
import { NextTaskBanner } from "../../../task-master";
|
||||
|
||||
type ProviderSelectionEmptyStateProps = {
|
||||
selectedSession: ProjectSession | null;
|
||||
currentSessionId: string | null;
|
||||
provider: SessionProvider;
|
||||
setProvider: (next: SessionProvider) => void;
|
||||
provider: LLMProvider;
|
||||
setProvider: (next: LLMProvider) => void;
|
||||
textareaRef: React.RefObject<HTMLTextAreaElement>;
|
||||
claudeModel: string;
|
||||
setClaudeModel: (model: string) => void;
|
||||
@@ -32,7 +32,7 @@ type ProviderSelectionEmptyStateProps = {
|
||||
};
|
||||
|
||||
type ProviderDef = {
|
||||
id: SessionProvider;
|
||||
id: LLMProvider;
|
||||
name: string;
|
||||
infoKey: string;
|
||||
accent: string;
|
||||
@@ -75,7 +75,7 @@ const PROVIDERS: ProviderDef[] = [
|
||||
},
|
||||
];
|
||||
|
||||
function getModelConfig(p: SessionProvider) {
|
||||
function getModelConfig(p: LLMProvider) {
|
||||
if (p === "claude") return CLAUDE_MODELS;
|
||||
if (p === "codex") return CODEX_MODELS;
|
||||
if (p === "gemini") return GEMINI_MODELS;
|
||||
@@ -83,7 +83,7 @@ function getModelConfig(p: SessionProvider) {
|
||||
}
|
||||
|
||||
function getModelValue(
|
||||
p: SessionProvider,
|
||||
p: LLMProvider,
|
||||
c: string,
|
||||
cu: string,
|
||||
co: string,
|
||||
@@ -119,7 +119,7 @@ export default function ProviderSelectionEmptyState({
|
||||
defaultValue: "Start the next task",
|
||||
});
|
||||
|
||||
const selectProvider = (next: SessionProvider) => {
|
||||
const selectProvider = (next: LLMProvider) => {
|
||||
setProvider(next);
|
||||
localStorage.setItem("selected-provider", next);
|
||||
setTimeout(() => textareaRef.current?.focus(), 100);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { SessionProvider } from '../../types/app';
|
||||
import type { LLMProvider } from '../../types/app';
|
||||
import ClaudeLogo from './ClaudeLogo';
|
||||
import CodexLogo from './CodexLogo';
|
||||
import CursorLogo from './CursorLogo';
|
||||
import GeminiLogo from './GeminiLogo';
|
||||
|
||||
type SessionProviderLogoProps = {
|
||||
provider?: SessionProvider | string | null;
|
||||
provider?: LLMProvider | string | null;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { Check, ChevronLeft, ChevronRight, Loader2 } from 'lucide-react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import type { LLMProvider } from '../../../types/app';
|
||||
import { authenticatedFetch } from '../../../utils/api';
|
||||
import { useProviderAuthStatus } from '../../provider-auth/hooks/useProviderAuthStatus';
|
||||
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 = {
|
||||
@@ -24,59 +22,14 @@ export default function Onboarding({ onComplete }: OnboardingProps) {
|
||||
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 [activeLoginProvider, setActiveLoginProvider] = useState<LLMProvider | null>(null);
|
||||
const {
|
||||
providerAuthStatus,
|
||||
checkProviderAuthStatus,
|
||||
refreshProviderAuthStatuses,
|
||||
} = useProviderAuthStatus();
|
||||
|
||||
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 previousActiveLoginProviderRef = useRef<LLMProvider | null | undefined>(undefined);
|
||||
|
||||
const loadGitConfig = useCallback(async () => {
|
||||
try {
|
||||
@@ -99,23 +52,24 @@ export default function Onboarding({ onComplete }: OnboardingProps) {
|
||||
|
||||
useEffect(() => {
|
||||
void loadGitConfig();
|
||||
void refreshAllProviderStatuses();
|
||||
}, [loadGitConfig, refreshAllProviderStatuses]);
|
||||
void refreshProviderAuthStatuses();
|
||||
}, [loadGitConfig, refreshProviderAuthStatuses]);
|
||||
|
||||
useEffect(() => {
|
||||
const previousProvider = previousActiveLoginProviderRef.current;
|
||||
previousActiveLoginProviderRef.current = activeLoginProvider;
|
||||
|
||||
const isInitialMount = previousProvider === undefined;
|
||||
const didCloseModal = previousProvider !== null && activeLoginProvider === null;
|
||||
const didCloseModal = previousProvider !== undefined
|
||||
&& previousProvider !== null
|
||||
&& activeLoginProvider === null;
|
||||
|
||||
// Refresh statuses once on mount and again after the login modal is closed.
|
||||
if (isInitialMount || didCloseModal) {
|
||||
void refreshAllProviderStatuses();
|
||||
// Refresh statuses after the login modal is closed.
|
||||
if (didCloseModal) {
|
||||
void refreshProviderAuthStatuses();
|
||||
}
|
||||
}, [activeLoginProvider, refreshAllProviderStatuses]);
|
||||
}, [activeLoginProvider, refreshProviderAuthStatuses]);
|
||||
|
||||
const handleProviderLoginOpen = (provider: CliProvider) => {
|
||||
const handleProviderLoginOpen = (provider: LLMProvider) => {
|
||||
setActiveLoginProvider(provider);
|
||||
};
|
||||
|
||||
@@ -209,7 +163,7 @@ export default function Onboarding({ onComplete }: OnboardingProps) {
|
||||
/>
|
||||
) : (
|
||||
<AgentConnectionsStep
|
||||
providerStatuses={providerStatuses}
|
||||
providerStatuses={providerAuthStatus}
|
||||
onOpenProviderLogin={handleProviderLoginOpen}
|
||||
/>
|
||||
)}
|
||||
@@ -279,7 +233,6 @@ export default function Onboarding({ onComplete }: OnboardingProps) {
|
||||
isOpen={Boolean(activeLoginProvider)}
|
||||
onClose={() => setActiveLoginProvider(null)}
|
||||
provider={activeLoginProvider}
|
||||
project={selectedProject}
|
||||
onComplete={handleLoginComplete}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Check } from 'lucide-react';
|
||||
import SessionProviderLogo from '../../../llm-logo-provider/SessionProviderLogo';
|
||||
import type { CliProvider, ProviderAuthStatus } from '../types';
|
||||
import type { LLMProvider } from '../../../../types/app';
|
||||
import type { ProviderAuthStatus } from '../../../provider-auth/types';
|
||||
|
||||
type AgentConnectionCardProps = {
|
||||
provider: CliProvider;
|
||||
provider: LLMProvider;
|
||||
title: string;
|
||||
status: ProviderAuthStatus;
|
||||
connectedClassName: string;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { CliProvider, ProviderStatusMap } from '../types';
|
||||
import type { LLMProvider } from '../../../../types/app';
|
||||
import type { ProviderAuthStatusMap } from '../../../provider-auth/types';
|
||||
import AgentConnectionCard from './AgentConnectionCard';
|
||||
|
||||
type AgentConnectionsStepProps = {
|
||||
providerStatuses: ProviderStatusMap;
|
||||
onOpenProviderLogin: (provider: CliProvider) => void;
|
||||
providerStatuses: ProviderAuthStatusMap;
|
||||
onOpenProviderLogin: (provider: LLMProvider) => void;
|
||||
};
|
||||
|
||||
const providerCards = [
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
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>;
|
||||
@@ -1,24 +1,5 @@
|
||||
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 };
|
||||
|
||||
109
src/components/provider-auth/hooks/useProviderAuthStatus.ts
Normal file
109
src/components/provider-auth/hooks/useProviderAuthStatus.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { authenticatedFetch } from '../../../utils/api';
|
||||
import type { LLMProvider } from '../../../types/app';
|
||||
import {
|
||||
CLI_AUTH_STATUS_ENDPOINTS,
|
||||
CLI_PROVIDERS,
|
||||
createInitialProviderAuthStatusMap,
|
||||
} from '../types';
|
||||
import type {
|
||||
ProviderAuthStatus,
|
||||
ProviderAuthStatusMap,
|
||||
} from '../types';
|
||||
|
||||
type ProviderAuthStatusPayload = {
|
||||
authenticated?: boolean;
|
||||
email?: string | null;
|
||||
method?: string | null;
|
||||
error?: string | null;
|
||||
};
|
||||
|
||||
const FALLBACK_STATUS_ERROR = 'Failed to check authentication status';
|
||||
const FALLBACK_UNKNOWN_ERROR = 'Unknown error';
|
||||
|
||||
const toErrorMessage = (error: unknown): string => (
|
||||
error instanceof Error ? error.message : FALLBACK_UNKNOWN_ERROR
|
||||
);
|
||||
|
||||
const toProviderAuthStatus = (
|
||||
payload: ProviderAuthStatusPayload,
|
||||
fallbackError: string | null = null,
|
||||
): ProviderAuthStatus => ({
|
||||
authenticated: Boolean(payload.authenticated),
|
||||
email: payload.email ?? null,
|
||||
method: payload.method ?? null,
|
||||
error: payload.error ?? fallbackError,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
type UseProviderAuthStatusOptions = {
|
||||
initialLoading?: boolean;
|
||||
};
|
||||
|
||||
export function useProviderAuthStatus(
|
||||
{ initialLoading = true }: UseProviderAuthStatusOptions = {},
|
||||
) {
|
||||
const [providerAuthStatus, setProviderAuthStatus] = useState<ProviderAuthStatusMap>(() => (
|
||||
createInitialProviderAuthStatusMap(initialLoading)
|
||||
));
|
||||
|
||||
const setProviderLoading = useCallback((provider: LLMProvider) => {
|
||||
setProviderAuthStatus((previous) => ({
|
||||
...previous,
|
||||
[provider]: {
|
||||
...previous[provider],
|
||||
loading: true,
|
||||
error: null,
|
||||
},
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const setProviderStatus = useCallback((provider: LLMProvider, status: ProviderAuthStatus) => {
|
||||
setProviderAuthStatus((previous) => ({
|
||||
...previous,
|
||||
[provider]: status,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const checkProviderAuthStatus = useCallback(async (provider: LLMProvider) => {
|
||||
setProviderLoading(provider);
|
||||
|
||||
try {
|
||||
const response = await authenticatedFetch(CLI_AUTH_STATUS_ENDPOINTS[provider]);
|
||||
|
||||
if (!response.ok) {
|
||||
setProviderStatus(provider, {
|
||||
authenticated: false,
|
||||
email: null,
|
||||
method: null,
|
||||
loading: false,
|
||||
error: FALLBACK_STATUS_ERROR,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = (await response.json()) as ProviderAuthStatusPayload;
|
||||
setProviderStatus(provider, toProviderAuthStatus(payload));
|
||||
} catch (caughtError) {
|
||||
console.error(`Error checking ${provider} auth status:`, caughtError);
|
||||
setProviderStatus(provider, {
|
||||
authenticated: false,
|
||||
email: null,
|
||||
method: null,
|
||||
loading: false,
|
||||
error: toErrorMessage(caughtError),
|
||||
});
|
||||
}
|
||||
}, [setProviderLoading, setProviderStatus]);
|
||||
|
||||
const refreshProviderAuthStatuses = useCallback(async (providers: LLMProvider[] = CLI_PROVIDERS) => {
|
||||
await Promise.all(providers.map((provider) => checkProviderAuthStatus(provider)));
|
||||
}, [checkProviderAuthStatus]);
|
||||
|
||||
return {
|
||||
providerAuthStatus,
|
||||
setProviderAuthStatus,
|
||||
checkProviderAuthStatus,
|
||||
refreshProviderAuthStatuses,
|
||||
};
|
||||
}
|
||||
@@ -1 +1,27 @@
|
||||
export type CliProvider = 'claude' | 'cursor' | 'codex' | 'gemini';
|
||||
import type { LLMProvider } from '../../types/app';
|
||||
|
||||
export type ProviderAuthStatus = {
|
||||
authenticated: boolean;
|
||||
email: string | null;
|
||||
method: string | null;
|
||||
error: string | null;
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
export type ProviderAuthStatusMap = Record<LLMProvider, ProviderAuthStatus>;
|
||||
|
||||
export const CLI_PROVIDERS: LLMProvider[] = ['claude', 'cursor', 'codex', 'gemini'];
|
||||
|
||||
export const CLI_AUTH_STATUS_ENDPOINTS: Record<LLMProvider, string> = {
|
||||
claude: '/api/cli/claude/status',
|
||||
cursor: '/api/cli/cursor/status',
|
||||
codex: '/api/cli/codex/status',
|
||||
gemini: '/api/cli/gemini/status',
|
||||
};
|
||||
|
||||
export const createInitialProviderAuthStatusMap = (loading = true): ProviderAuthStatusMap => ({
|
||||
claude: { authenticated: false, email: null, method: null, error: null, loading },
|
||||
cursor: { authenticated: false, email: null, method: null, error: null, loading },
|
||||
codex: { authenticated: false, email: null, method: null, error: null, loading },
|
||||
gemini: { authenticated: false, email: null, method: null, error: null, loading },
|
||||
});
|
||||
|
||||
@@ -1,21 +1,12 @@
|
||||
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';
|
||||
|
||||
type LoginModalProject = {
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
fullPath?: string;
|
||||
path?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
import { DEFAULT_PROJECT_FOR_EMPTY_SHELL, IS_PLATFORM } from '../../../constants/config';
|
||||
import type { LLMProvider } from '../../../types/app';
|
||||
|
||||
type ProviderLoginModalProps = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
provider?: CliProvider;
|
||||
project?: LoginModalProject | null;
|
||||
provider?: LLMProvider;
|
||||
onComplete?: (exitCode: number) => void;
|
||||
customCommand?: string;
|
||||
isAuthenticated?: boolean;
|
||||
@@ -26,7 +17,7 @@ const getProviderCommand = ({
|
||||
customCommand,
|
||||
isAuthenticated: _isAuthenticated,
|
||||
}: {
|
||||
provider: CliProvider;
|
||||
provider: LLMProvider;
|
||||
customCommand?: string;
|
||||
isAuthenticated: boolean;
|
||||
}) => {
|
||||
@@ -49,30 +40,17 @@ const getProviderCommand = ({
|
||||
return 'gemini status';
|
||||
};
|
||||
|
||||
const getProviderTitle = (provider: CliProvider) => {
|
||||
const getProviderTitle = (provider: LLMProvider) => {
|
||||
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 = null,
|
||||
onComplete,
|
||||
customCommand,
|
||||
isAuthenticated = false,
|
||||
@@ -83,7 +61,6 @@ export default function ProviderLoginModal({
|
||||
|
||||
const command = getProviderCommand({ provider, customCommand, isAuthenticated });
|
||||
const title = getProviderTitle(provider);
|
||||
const shellProject = normalizeProject(project);
|
||||
|
||||
const handleComplete = (exitCode: number) => {
|
||||
onComplete?.(exitCode);
|
||||
@@ -158,7 +135,7 @@ export default function ProviderLoginModal({
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<StandaloneShell project={shellProject} command={command} onComplete={handleComplete} minimal={true} />
|
||||
<StandaloneShell project={DEFAULT_PROJECT_FOR_EMPTY_SHELL} command={command} onComplete={handleComplete} minimal={true} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type {
|
||||
AgentCategory,
|
||||
AgentProvider,
|
||||
AuthStatus,
|
||||
ClaudeMcpFormState,
|
||||
CodexMcpFormState,
|
||||
CodeEditorSettingsState,
|
||||
@@ -34,13 +33,6 @@ export const DEFAULT_CODE_EDITOR_SETTINGS: CodeEditorSettingsState = {
|
||||
fontSize: '14',
|
||||
};
|
||||
|
||||
export const DEFAULT_AUTH_STATUS: AuthStatus = {
|
||||
authenticated: false,
|
||||
email: null,
|
||||
loading: true,
|
||||
error: null,
|
||||
};
|
||||
|
||||
export const DEFAULT_MCP_TEST_RESULT: McpTestResult = {
|
||||
success: false,
|
||||
message: '',
|
||||
@@ -88,9 +80,3 @@ export const DEFAULT_CURSOR_PERMISSIONS: CursorPermissionsState = {
|
||||
skipPermissions: false,
|
||||
};
|
||||
|
||||
export const AUTH_STATUS_ENDPOINTS: Record<AgentProvider, string> = {
|
||||
claude: '/api/cli/claude/status',
|
||||
cursor: '/api/cli/cursor/status',
|
||||
codex: '/api/cli/codex/status',
|
||||
gemini: '/api/cli/gemini/status',
|
||||
};
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTheme } from '../../../contexts/ThemeContext';
|
||||
import { authenticatedFetch } from '../../../utils/api';
|
||||
import { useProviderAuthStatus } from '../../provider-auth/hooks/useProviderAuthStatus';
|
||||
import {
|
||||
AUTH_STATUS_ENDPOINTS,
|
||||
DEFAULT_AUTH_STATUS,
|
||||
DEFAULT_CODE_EDITOR_SETTINGS,
|
||||
DEFAULT_CURSOR_PERMISSIONS,
|
||||
} from '../constants/constants';
|
||||
import type {
|
||||
AgentProvider,
|
||||
AuthStatus,
|
||||
ClaudeMcpFormState,
|
||||
ClaudePermissionsState,
|
||||
CodeEditorSettingsState,
|
||||
@@ -23,7 +21,6 @@ import type {
|
||||
NotificationPreferencesState,
|
||||
ProjectSortOrder,
|
||||
SettingsMainTab,
|
||||
SettingsProject,
|
||||
} from '../types/types';
|
||||
|
||||
type ThemeContextValue = {
|
||||
@@ -34,15 +31,6 @@ type ThemeContextValue = {
|
||||
type UseSettingsControllerArgs = {
|
||||
isOpen: boolean;
|
||||
initialTab: string;
|
||||
projects: SettingsProject[];
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
type StatusApiResponse = {
|
||||
authenticated?: boolean;
|
||||
email?: string | null;
|
||||
error?: string | null;
|
||||
method?: string;
|
||||
};
|
||||
|
||||
type JsonResult = {
|
||||
@@ -166,20 +154,6 @@ const mapCliServersToMcpServers = (servers: McpCliServer[] = []): McpServer[] =>
|
||||
}))
|
||||
);
|
||||
|
||||
const getDefaultProject = (projects: SettingsProject[]): SettingsProject => {
|
||||
if (projects.length > 0) {
|
||||
return projects[0];
|
||||
}
|
||||
|
||||
const cwd = typeof process !== 'undefined' && process.cwd ? process.cwd() : '';
|
||||
return {
|
||||
name: 'default',
|
||||
displayName: 'default',
|
||||
fullPath: cwd,
|
||||
path: cwd,
|
||||
};
|
||||
};
|
||||
|
||||
const toResponseJson = async <T>(response: Response): Promise<T> => response.json() as Promise<T>;
|
||||
|
||||
const createEmptyClaudePermissions = (): ClaudePermissionsState => ({
|
||||
@@ -204,7 +178,7 @@ const createDefaultNotificationPreferences = (): NotificationPreferencesState =>
|
||||
},
|
||||
});
|
||||
|
||||
export function useSettingsController({ isOpen, initialTab, projects, onClose }: UseSettingsControllerArgs) {
|
||||
export function useSettingsController({ isOpen, initialTab }: UseSettingsControllerArgs) {
|
||||
const { isDarkMode, toggleDarkMode } = useTheme() as ThemeContextValue;
|
||||
const closeTimerRef = useRef<number | null>(null);
|
||||
|
||||
@@ -242,64 +216,11 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
||||
|
||||
const [showLoginModal, setShowLoginModal] = useState(false);
|
||||
const [loginProvider, setLoginProvider] = useState<ActiveLoginProvider>('');
|
||||
const [selectedProject, setSelectedProject] = useState<SettingsProject | null>(null);
|
||||
|
||||
const [claudeAuthStatus, setClaudeAuthStatus] = useState<AuthStatus>(DEFAULT_AUTH_STATUS);
|
||||
const [cursorAuthStatus, setCursorAuthStatus] = useState<AuthStatus>(DEFAULT_AUTH_STATUS);
|
||||
const [codexAuthStatus, setCodexAuthStatus] = useState<AuthStatus>(DEFAULT_AUTH_STATUS);
|
||||
const [geminiAuthStatus, setGeminiAuthStatus] = useState<AuthStatus>(DEFAULT_AUTH_STATUS);
|
||||
|
||||
const setAuthStatusByProvider = useCallback((provider: AgentProvider, status: AuthStatus) => {
|
||||
if (provider === 'claude') {
|
||||
setClaudeAuthStatus(status);
|
||||
return;
|
||||
}
|
||||
|
||||
if (provider === 'cursor') {
|
||||
setCursorAuthStatus(status);
|
||||
return;
|
||||
}
|
||||
|
||||
if (provider === 'gemini') {
|
||||
setGeminiAuthStatus(status);
|
||||
return;
|
||||
}
|
||||
|
||||
setCodexAuthStatus(status);
|
||||
}, []);
|
||||
|
||||
const checkAuthStatus = useCallback(async (provider: AgentProvider) => {
|
||||
try {
|
||||
const response = await authenticatedFetch(AUTH_STATUS_ENDPOINTS[provider]);
|
||||
|
||||
if (!response.ok) {
|
||||
setAuthStatusByProvider(provider, {
|
||||
authenticated: false,
|
||||
email: null,
|
||||
loading: false,
|
||||
error: 'Failed to check authentication status',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await toResponseJson<StatusApiResponse>(response);
|
||||
setAuthStatusByProvider(provider, {
|
||||
authenticated: Boolean(data.authenticated),
|
||||
email: data.email || null,
|
||||
loading: false,
|
||||
error: data.error || null,
|
||||
method: data.method,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Error checking ${provider} auth status:`, error);
|
||||
setAuthStatusByProvider(provider, {
|
||||
authenticated: false,
|
||||
email: null,
|
||||
loading: false,
|
||||
error: getErrorMessage(error),
|
||||
});
|
||||
}
|
||||
}, [setAuthStatusByProvider]);
|
||||
const {
|
||||
providerAuthStatus,
|
||||
checkProviderAuthStatus,
|
||||
refreshProviderAuthStatuses,
|
||||
} = useProviderAuthStatus();
|
||||
|
||||
const fetchCursorMcpServers = useCallback(async () => {
|
||||
try {
|
||||
@@ -724,9 +645,8 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
||||
|
||||
const openLoginForProvider = useCallback((provider: AgentProvider) => {
|
||||
setLoginProvider(provider);
|
||||
setSelectedProject(getDefaultProject(projects));
|
||||
setShowLoginModal(true);
|
||||
}, [projects]);
|
||||
}, []);
|
||||
|
||||
const handleLoginComplete = useCallback((exitCode: number) => {
|
||||
if (exitCode !== 0 || !loginProvider) {
|
||||
@@ -734,8 +654,8 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
||||
}
|
||||
|
||||
setSaveStatus('success');
|
||||
void checkAuthStatus(loginProvider);
|
||||
}, [checkAuthStatus, loginProvider]);
|
||||
void checkProviderAuthStatus(loginProvider);
|
||||
}, [checkProviderAuthStatus, loginProvider]);
|
||||
|
||||
const saveSettings = useCallback(async () => {
|
||||
setSaveStatus(null);
|
||||
@@ -827,11 +747,8 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
||||
|
||||
setActiveTab(normalizeMainTab(initialTab));
|
||||
void loadSettings();
|
||||
void checkAuthStatus('claude');
|
||||
void checkAuthStatus('cursor');
|
||||
void checkAuthStatus('codex');
|
||||
void checkAuthStatus('gemini');
|
||||
}, [checkAuthStatus, initialTab, isOpen, loadSettings]);
|
||||
void refreshProviderAuthStatuses();
|
||||
}, [initialTab, isOpen, loadSettings, refreshProviderAuthStatuses]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('codeEditorTheme', codeEditorSettings.theme);
|
||||
@@ -935,17 +852,13 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
||||
closeCodexMcpForm,
|
||||
submitCodexMcpForm,
|
||||
handleCodexMcpDelete,
|
||||
claudeAuthStatus,
|
||||
cursorAuthStatus,
|
||||
codexAuthStatus,
|
||||
geminiAuthStatus,
|
||||
providerAuthStatus,
|
||||
geminiPermissionMode,
|
||||
setGeminiPermissionMode,
|
||||
openLoginForProvider,
|
||||
showLoginModal,
|
||||
setShowLoginModal,
|
||||
loginProvider,
|
||||
selectedProject,
|
||||
handleLoginComplete,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import type { LLMProvider } from '../../../types/app';
|
||||
import type { ProviderAuthStatus } from '../../provider-auth/types';
|
||||
|
||||
export type SettingsMainTab = 'agents' | 'appearance' | 'git' | 'api' | 'tasks' | 'notifications' | 'plugins' | 'about';
|
||||
export type AgentProvider = 'claude' | 'cursor' | 'codex' | 'gemini';
|
||||
export type AgentProvider = LLMProvider;
|
||||
export type AgentCategory = 'account' | 'permissions' | 'mcp';
|
||||
export type ProjectSortOrder = 'name' | 'date';
|
||||
export type SaveStatus = 'success' | 'error' | null;
|
||||
@@ -18,13 +20,7 @@ export type SettingsProject = {
|
||||
path?: string;
|
||||
};
|
||||
|
||||
export type AuthStatus = {
|
||||
authenticated: boolean;
|
||||
email: string | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
method?: string;
|
||||
};
|
||||
export type AuthStatus = ProviderAuthStatus;
|
||||
|
||||
export type KeyValueMap = Record<string, string>;
|
||||
|
||||
|
||||
@@ -56,23 +56,17 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set
|
||||
closeCodexMcpForm,
|
||||
submitCodexMcpForm,
|
||||
handleCodexMcpDelete,
|
||||
claudeAuthStatus,
|
||||
cursorAuthStatus,
|
||||
codexAuthStatus,
|
||||
geminiAuthStatus,
|
||||
providerAuthStatus,
|
||||
geminiPermissionMode,
|
||||
setGeminiPermissionMode,
|
||||
openLoginForProvider,
|
||||
showLoginModal,
|
||||
setShowLoginModal,
|
||||
loginProvider,
|
||||
selectedProject,
|
||||
handleLoginComplete,
|
||||
} = useSettingsController({
|
||||
isOpen,
|
||||
initialTab,
|
||||
projects,
|
||||
onClose,
|
||||
initialTab
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -105,13 +99,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set
|
||||
return null;
|
||||
}
|
||||
|
||||
const isAuthenticated = loginProvider === 'claude'
|
||||
? claudeAuthStatus.authenticated
|
||||
: loginProvider === 'cursor'
|
||||
? cursorAuthStatus.authenticated
|
||||
: loginProvider === 'codex'
|
||||
? codexAuthStatus.authenticated
|
||||
: false;
|
||||
const isAuthenticated = Boolean(loginProvider && providerAuthStatus[loginProvider].authenticated);
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop fixed inset-0 z-[9999] flex items-center justify-center bg-background/80 backdrop-blur-sm md:p-4">
|
||||
@@ -121,7 +109,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set
|
||||
<h2 className="text-base font-semibold text-foreground">{t('title')}</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
{saveStatus === 'success' && (
|
||||
<span className="text-xs text-muted-foreground animate-in fade-in">{t('saveStatus.success')}</span>
|
||||
<span className="animate-in fade-in text-xs text-muted-foreground">{t('saveStatus.success')}</span>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -158,14 +146,8 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set
|
||||
|
||||
{activeTab === 'agents' && (
|
||||
<AgentsSettingsTab
|
||||
claudeAuthStatus={claudeAuthStatus}
|
||||
cursorAuthStatus={cursorAuthStatus}
|
||||
codexAuthStatus={codexAuthStatus}
|
||||
geminiAuthStatus={geminiAuthStatus}
|
||||
onClaudeLogin={() => openLoginForProvider('claude')}
|
||||
onCursorLogin={() => openLoginForProvider('cursor')}
|
||||
onCodexLogin={() => openLoginForProvider('codex')}
|
||||
onGeminiLogin={() => openLoginForProvider('gemini')}
|
||||
providerAuthStatus={providerAuthStatus}
|
||||
onProviderLogin={openLoginForProvider}
|
||||
claudePermissions={claudePermissions}
|
||||
onClaudePermissionsChange={setClaudePermissions}
|
||||
cursorPermissions={cursorPermissions}
|
||||
@@ -219,7 +201,6 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set
|
||||
isOpen={showLoginModal}
|
||||
onClose={() => setShowLoginModal(false)}
|
||||
provider={loginProvider || 'claude'}
|
||||
project={selectedProject}
|
||||
onComplete={handleLoginComplete}
|
||||
isAuthenticated={isAuthenticated}
|
||||
/>
|
||||
|
||||
@@ -6,14 +6,8 @@ import AgentSelectorSection from './sections/AgentSelectorSection';
|
||||
import type { AgentContext, AgentsSettingsTabProps } from './types';
|
||||
|
||||
export default function AgentsSettingsTab({
|
||||
claudeAuthStatus,
|
||||
cursorAuthStatus,
|
||||
codexAuthStatus,
|
||||
geminiAuthStatus,
|
||||
onClaudeLogin,
|
||||
onCursorLogin,
|
||||
onCodexLogin,
|
||||
onGeminiLogin,
|
||||
providerAuthStatus,
|
||||
onProviderLogin,
|
||||
claudePermissions,
|
||||
onClaudePermissionsChange,
|
||||
cursorPermissions,
|
||||
@@ -41,30 +35,27 @@ export default function AgentsSettingsTab({
|
||||
|
||||
const agentContextById = useMemo<Record<AgentProvider, AgentContext>>(() => ({
|
||||
claude: {
|
||||
authStatus: claudeAuthStatus,
|
||||
onLogin: onClaudeLogin,
|
||||
authStatus: providerAuthStatus.claude,
|
||||
onLogin: () => onProviderLogin('claude'),
|
||||
},
|
||||
cursor: {
|
||||
authStatus: cursorAuthStatus,
|
||||
onLogin: onCursorLogin,
|
||||
authStatus: providerAuthStatus.cursor,
|
||||
onLogin: () => onProviderLogin('cursor'),
|
||||
},
|
||||
codex: {
|
||||
authStatus: codexAuthStatus,
|
||||
onLogin: onCodexLogin,
|
||||
authStatus: providerAuthStatus.codex,
|
||||
onLogin: () => onProviderLogin('codex'),
|
||||
},
|
||||
gemini: {
|
||||
authStatus: geminiAuthStatus,
|
||||
onLogin: onGeminiLogin,
|
||||
authStatus: providerAuthStatus.gemini,
|
||||
onLogin: () => onProviderLogin('gemini'),
|
||||
},
|
||||
}), [
|
||||
claudeAuthStatus,
|
||||
codexAuthStatus,
|
||||
cursorAuthStatus,
|
||||
geminiAuthStatus,
|
||||
onClaudeLogin,
|
||||
onCodexLogin,
|
||||
onCursorLogin,
|
||||
onGeminiLogin,
|
||||
onProviderLogin,
|
||||
providerAuthStatus.claude,
|
||||
providerAuthStatus.codex,
|
||||
providerAuthStatus.cursor,
|
||||
providerAuthStatus.gemini,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -17,16 +17,11 @@ export type AgentContext = {
|
||||
};
|
||||
|
||||
export type AgentContextByProvider = Record<AgentProvider, AgentContext>;
|
||||
export type ProviderAuthStatusByProvider = Record<AgentProvider, AuthStatus>;
|
||||
|
||||
export type AgentsSettingsTabProps = {
|
||||
claudeAuthStatus: AuthStatus;
|
||||
cursorAuthStatus: AuthStatus;
|
||||
codexAuthStatus: AuthStatus;
|
||||
geminiAuthStatus: AuthStatus;
|
||||
onClaudeLogin: () => void;
|
||||
onCursorLogin: () => void;
|
||||
onCodexLogin: () => void;
|
||||
onGeminiLogin: () => void;
|
||||
providerAuthStatus: ProviderAuthStatusByProvider;
|
||||
onProviderLogin: (provider: AgentProvider) => void;
|
||||
claudePermissions: ClaudePermissionsState;
|
||||
onClaudePermissionsChange: (value: ClaudePermissionsState) => void;
|
||||
cursorPermissions: CursorPermissionsState;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type React from 'react';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { api } from '../../../utils/api';
|
||||
import type { Project, ProjectSession, SessionProvider } from '../../../types/app';
|
||||
import type { Project, ProjectSession, LLMProvider } from '../../../types/app';
|
||||
import type {
|
||||
AdditionalSessionsByProject,
|
||||
DeleteProjectConfirmation,
|
||||
@@ -545,7 +545,7 @@ export function useSidebarController({
|
||||
}, [onRefresh]);
|
||||
|
||||
const updateSessionSummary = useCallback(
|
||||
async (_projectName: string, sessionId: string, summary: string, provider: SessionProvider) => {
|
||||
async (_projectName: string, sessionId: string, summary: string, provider: LLMProvider) => {
|
||||
const trimmed = summary.trim();
|
||||
if (!trimmed) {
|
||||
setEditingSession(null);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { LoadingProgress, Project, ProjectSession, SessionProvider } from '../../../types/app';
|
||||
import type { LoadingProgress, Project, ProjectSession, LLMProvider } from '../../../types/app';
|
||||
|
||||
export type ProjectSortOrder = 'name' | 'date';
|
||||
|
||||
export type SessionWithProvider = ProjectSession & {
|
||||
__provider: SessionProvider;
|
||||
__provider: LLMProvider;
|
||||
};
|
||||
|
||||
export type AdditionalSessionsByProject = Record<string, ProjectSession[]>;
|
||||
@@ -18,7 +18,7 @@ export type SessionDeleteConfirmation = {
|
||||
projectName: string;
|
||||
sessionId: string;
|
||||
sessionTitle: string;
|
||||
provider: SessionProvider;
|
||||
provider: LLMProvider;
|
||||
};
|
||||
|
||||
export type SidebarProps = {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useUiPreferences } from '../../../hooks/useUiPreferences';
|
||||
import { useSidebarController } from '../hooks/useSidebarController';
|
||||
import { useTaskMaster } from '../../../contexts/TaskMasterContext';
|
||||
import { useTasksSettings } from '../../../contexts/TasksSettingsContext';
|
||||
import type { Project, SessionProvider } from '../../../types/app';
|
||||
import type { Project, LLMProvider } from '../../../types/app';
|
||||
import type { MCPServerStatus, SidebarProps } from '../types/types';
|
||||
import SidebarCollapsed from './subcomponents/SidebarCollapsed';
|
||||
import SidebarContent from './subcomponents/SidebarContent';
|
||||
@@ -177,7 +177,7 @@ function Sidebar({
|
||||
setEditingSession(null);
|
||||
setEditingSessionName('');
|
||||
},
|
||||
onSaveEditingSession: (projectName: string, sessionId: string, summary: string, provider: SessionProvider) => {
|
||||
onSaveEditingSession: (projectName: string, sessionId: string, summary: string, provider: LLMProvider) => {
|
||||
void updateSessionSummary(projectName, sessionId, summary, provider);
|
||||
},
|
||||
t,
|
||||
@@ -235,7 +235,7 @@ function Sidebar({
|
||||
isSearching={isSearching}
|
||||
searchProgress={searchProgress}
|
||||
onConversationResultClick={(projectName: string, sessionId: string, provider: string, messageTimestamp?: string | null, messageSnippet?: string | null) => {
|
||||
const resolvedProvider = (provider || 'claude') as SessionProvider;
|
||||
const resolvedProvider = (provider || 'claude') as LLMProvider;
|
||||
const project = projects.find(p => p.name === projectName);
|
||||
const searchTarget = { __searchTargetTimestamp: messageTimestamp || null, __searchTargetSnippet: messageSnippet || null };
|
||||
const sessionObj = {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Check, ChevronDown, ChevronRight, Edit3, Folder, FolderOpen, Star, Tras
|
||||
import type { TFunction } from 'i18next';
|
||||
import { Button } from '../../../../shared/view/ui';
|
||||
import { cn } from '../../../../lib/utils';
|
||||
import type { Project, ProjectSession, SessionProvider } from '../../../../types/app';
|
||||
import type { Project, ProjectSession, LLMProvider } from '../../../../types/app';
|
||||
import type { MCPServerStatus, SessionWithProvider } from '../../types/types';
|
||||
import { getTaskIndicatorStatus } from '../../utils/utils';
|
||||
import TaskIndicator from './TaskIndicator';
|
||||
@@ -38,14 +38,14 @@ type SidebarProjectItemProps = {
|
||||
projectName: string,
|
||||
sessionId: string,
|
||||
sessionTitle: string,
|
||||
provider: SessionProvider,
|
||||
provider: LLMProvider,
|
||||
) => void;
|
||||
onLoadMoreSessions: (project: Project) => void;
|
||||
onNewSession: (project: Project) => void;
|
||||
onEditingSessionNameChange: (value: string) => void;
|
||||
onStartEditingSession: (sessionId: string, initialName: string) => void;
|
||||
onCancelEditingSession: () => void;
|
||||
onSaveEditingSession: (projectName: string, sessionId: string, summary: string, provider: SessionProvider) => void;
|
||||
onSaveEditingSession: (projectName: string, sessionId: string, summary: string, provider: LLMProvider) => void;
|
||||
t: TFunction;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect } from 'react';
|
||||
import type { TFunction } from 'i18next';
|
||||
import type { LoadingProgress, Project, ProjectSession, SessionProvider } from '../../../../types/app';
|
||||
import type { LoadingProgress, Project, ProjectSession, LLMProvider } from '../../../../types/app';
|
||||
import type {
|
||||
LoadingSessionsByProject,
|
||||
MCPServerStatus,
|
||||
@@ -42,14 +42,14 @@ export type SidebarProjectListProps = {
|
||||
projectName: string,
|
||||
sessionId: string,
|
||||
sessionTitle: string,
|
||||
provider: SessionProvider,
|
||||
provider: LLMProvider,
|
||||
) => void;
|
||||
onLoadMoreSessions: (project: Project) => void;
|
||||
onNewSession: (project: Project) => void;
|
||||
onEditingSessionNameChange: (value: string) => void;
|
||||
onStartEditingSession: (sessionId: string, initialName: string) => void;
|
||||
onCancelEditingSession: () => void;
|
||||
onSaveEditingSession: (projectName: string, sessionId: string, summary: string, provider: SessionProvider) => void;
|
||||
onSaveEditingSession: (projectName: string, sessionId: string, summary: string, provider: LLMProvider) => void;
|
||||
t: TFunction;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ChevronDown, Plus } from 'lucide-react';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { Button } from '../../../../shared/view/ui';
|
||||
import type { Project, ProjectSession, SessionProvider } from '../../../../types/app';
|
||||
import type { Project, ProjectSession, LLMProvider } from '../../../../types/app';
|
||||
import type { SessionWithProvider } from '../../types/types';
|
||||
import SidebarSessionItem from './SidebarSessionItem';
|
||||
|
||||
@@ -18,14 +18,14 @@ type SidebarProjectSessionsProps = {
|
||||
onEditingSessionNameChange: (value: string) => void;
|
||||
onStartEditingSession: (sessionId: string, initialName: string) => void;
|
||||
onCancelEditingSession: () => void;
|
||||
onSaveEditingSession: (projectName: string, sessionId: string, summary: string, provider: SessionProvider) => void;
|
||||
onSaveEditingSession: (projectName: string, sessionId: string, summary: string, provider: LLMProvider) => void;
|
||||
onProjectSelect: (project: Project) => void;
|
||||
onSessionSelect: (session: SessionWithProvider, projectName: string) => void;
|
||||
onDeleteSession: (
|
||||
projectName: string,
|
||||
sessionId: string,
|
||||
sessionTitle: string,
|
||||
provider: SessionProvider,
|
||||
provider: LLMProvider,
|
||||
) => void;
|
||||
onLoadMoreSessions: (project: Project) => void;
|
||||
onNewSession: (project: Project) => void;
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { TFunction } from 'i18next';
|
||||
import { Badge, Button } from '../../../../shared/view/ui';
|
||||
import { cn } from '../../../../lib/utils';
|
||||
import { formatTimeAgo } from '../../../../utils/dateUtils';
|
||||
import type { Project, ProjectSession, SessionProvider } from '../../../../types/app';
|
||||
import type { Project, ProjectSession, LLMProvider } from '../../../../types/app';
|
||||
import type { SessionWithProvider } from '../../types/types';
|
||||
import { createSessionViewModel } from '../../utils/utils';
|
||||
import SessionProviderLogo from '../../../llm-logo-provider/SessionProviderLogo';
|
||||
@@ -18,14 +18,14 @@ type SidebarSessionItemProps = {
|
||||
onEditingSessionNameChange: (value: string) => void;
|
||||
onStartEditingSession: (sessionId: string, initialName: string) => void;
|
||||
onCancelEditingSession: () => void;
|
||||
onSaveEditingSession: (projectName: string, sessionId: string, summary: string, provider: SessionProvider) => void;
|
||||
onSaveEditingSession: (projectName: string, sessionId: string, summary: string, provider: LLMProvider) => void;
|
||||
onProjectSelect: (project: Project) => void;
|
||||
onSessionSelect: (session: SessionWithProvider, projectName: string) => void;
|
||||
onDeleteSession: (
|
||||
projectName: string,
|
||||
sessionId: string,
|
||||
sessionTitle: string,
|
||||
provider: SessionProvider,
|
||||
provider: LLMProvider,
|
||||
) => void;
|
||||
t: TFunction;
|
||||
};
|
||||
|
||||
@@ -2,4 +2,16 @@
|
||||
* Environment Flag: Is Platform
|
||||
* Indicates if the app is running in Platform mode (hosted) or OSS mode (self-hosted)
|
||||
*/
|
||||
export const IS_PLATFORM = import.meta.env.VITE_IS_PLATFORM === 'true';
|
||||
export const IS_PLATFORM = import.meta.env.VITE_IS_PLATFORM === 'true';
|
||||
|
||||
/**
|
||||
* For empty shell instances where no project is provided,
|
||||
* we use a default project object to ensure the shell can still function.
|
||||
* This prevents errors related to missing project data.
|
||||
*/
|
||||
export const DEFAULT_PROJECT_FOR_EMPTY_SHELL = {
|
||||
name: 'default',
|
||||
displayName: 'default',
|
||||
fullPath: IS_PLATFORM ? '/workspace' : '',
|
||||
path: IS_PLATFORM ? '/workspace' : '',
|
||||
};
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import type { SessionProvider } from '../types/app';
|
||||
import type { LLMProvider } from '../types/app';
|
||||
import { authenticatedFetch } from '../utils/api';
|
||||
|
||||
// ─── NormalizedMessage (mirrors server/adapters/types.js) ────────────────────
|
||||
@@ -33,7 +33,7 @@ export interface NormalizedMessage {
|
||||
id: string;
|
||||
sessionId: string;
|
||||
timestamp: string;
|
||||
provider: SessionProvider;
|
||||
provider: LLMProvider;
|
||||
kind: MessageKind;
|
||||
|
||||
// kind-specific fields (flat for simplicity)
|
||||
@@ -169,7 +169,7 @@ export function useSessionStore() {
|
||||
const fetchFromServer = useCallback(async (
|
||||
sessionId: string,
|
||||
opts: {
|
||||
provider?: SessionProvider;
|
||||
provider?: LLMProvider;
|
||||
projectName?: string;
|
||||
projectPath?: string;
|
||||
limit?: number | null;
|
||||
@@ -228,7 +228,7 @@ export function useSessionStore() {
|
||||
const fetchMore = useCallback(async (
|
||||
sessionId: string,
|
||||
opts: {
|
||||
provider?: SessionProvider;
|
||||
provider?: LLMProvider;
|
||||
projectName?: string;
|
||||
projectPath?: string;
|
||||
limit?: number;
|
||||
@@ -303,7 +303,7 @@ export function useSessionStore() {
|
||||
const refreshFromServer = useCallback(async (
|
||||
sessionId: string,
|
||||
opts: {
|
||||
provider?: SessionProvider;
|
||||
provider?: LLMProvider;
|
||||
projectName?: string;
|
||||
projectPath?: string;
|
||||
} = {},
|
||||
@@ -357,7 +357,7 @@ export function useSessionStore() {
|
||||
* Update or create a streaming message (accumulated text so far).
|
||||
* Uses a well-known ID so subsequent calls replace the same message.
|
||||
*/
|
||||
const updateStreaming = useCallback((sessionId: string, accumulatedText: string, msgProvider: SessionProvider) => {
|
||||
const updateStreaming = useCallback((sessionId: string, accumulatedText: string, msgProvider: LLMProvider) => {
|
||||
const slot = getSlot(sessionId);
|
||||
const streamId = `__streaming_${sessionId}`;
|
||||
const msg: NormalizedMessage = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type SessionProvider = 'claude' | 'cursor' | 'codex' | 'gemini';
|
||||
export type LLMProvider = 'claude' | 'cursor' | 'codex' | 'gemini';
|
||||
|
||||
export type AppTab = 'chat' | 'files' | 'shell' | 'git' | 'tasks' | 'preview' | `plugin:${string}`;
|
||||
|
||||
@@ -12,7 +12,7 @@ export interface ProjectSession {
|
||||
updated_at?: string;
|
||||
lastActivity?: string;
|
||||
messageCount?: number;
|
||||
__provider?: SessionProvider;
|
||||
__provider?: LLMProvider;
|
||||
__projectName?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user