mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-27 22:25:29 +08:00
feat: remove project dependency from settings and mcp form modal
This commit is contained in:
@@ -11,8 +11,8 @@ import {
|
|||||||
createInitialProviderStatuses,
|
createInitialProviderStatuses,
|
||||||
gitEmailPattern,
|
gitEmailPattern,
|
||||||
readErrorMessageFromResponse,
|
readErrorMessageFromResponse,
|
||||||
selectedProject,
|
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
import { DEFAULT_PROJECT_FOR_EMPTY_SHELL } from '@/constants/config.js';
|
||||||
|
|
||||||
type OnboardingProps = {
|
type OnboardingProps = {
|
||||||
onComplete?: () => void | Promise<void>;
|
onComplete?: () => void | Promise<void>;
|
||||||
@@ -279,7 +279,7 @@ export default function Onboarding({ onComplete }: OnboardingProps) {
|
|||||||
isOpen={Boolean(activeLoginProvider)}
|
isOpen={Boolean(activeLoginProvider)}
|
||||||
onClose={() => setActiveLoginProvider(null)}
|
onClose={() => setActiveLoginProvider(null)}
|
||||||
provider={activeLoginProvider}
|
provider={activeLoginProvider}
|
||||||
project={selectedProject}
|
project={DEFAULT_PROJECT_FOR_EMPTY_SHELL}
|
||||||
onComplete={handleLoginComplete}
|
onComplete={handleLoginComplete}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,16 +1,9 @@
|
|||||||
import { IS_PLATFORM } from '../../../constants/config';
|
|
||||||
import type { CliProvider, ProviderStatusMap } from './types';
|
import type { CliProvider, ProviderStatusMap } from './types';
|
||||||
|
|
||||||
export const cliProviders: CliProvider[] = ['claude', 'cursor', 'codex', 'gemini'];
|
export const cliProviders: CliProvider[] = ['claude', 'cursor', 'codex', 'gemini'];
|
||||||
|
|
||||||
export const gitEmailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
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 => ({
|
export const createInitialProviderStatuses = (): ProviderStatusMap => ({
|
||||||
claude: { authenticated: false, email: null, loading: true, error: null },
|
claude: { authenticated: false, email: null, loading: true, error: null },
|
||||||
|
|||||||
@@ -6,13 +6,25 @@ import { useState } from 'react';
|
|||||||
*/
|
*/
|
||||||
export const useSidebarModals = () => {
|
export const useSidebarModals = () => {
|
||||||
const [showNewProject, setShowNewProject] = useState(false);
|
const [showNewProject, setShowNewProject] = useState(false);
|
||||||
|
const [showSettingsModal, setShowSettingsModal] = useState(false);
|
||||||
|
const [showVersionModal, setShowVersionModal] = useState(false);
|
||||||
|
|
||||||
const openNewProject = () => setShowNewProject(true);
|
const openNewProject = () => setShowNewProject(true);
|
||||||
const closeNewProject = () => setShowNewProject(false);
|
const closeNewProject = () => setShowNewProject(false);
|
||||||
|
const openSettingsModal = () => setShowSettingsModal(true);
|
||||||
|
const closeSettingsModal = () => setShowSettingsModal(false);
|
||||||
|
const openVersionModal = () => setShowVersionModal(true);
|
||||||
|
const closeVersionModal = () => setShowVersionModal(false);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
showNewProject,
|
showNewProject,
|
||||||
openNewProject,
|
openNewProject,
|
||||||
closeNewProject,
|
closeNewProject,
|
||||||
|
showSettingsModal,
|
||||||
|
openSettingsModal,
|
||||||
|
closeSettingsModal,
|
||||||
|
showVersionModal,
|
||||||
|
openVersionModal,
|
||||||
|
closeVersionModal,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { PanelRightOpen } from 'lucide-react';
|
import { PanelRightOpen } from 'lucide-react';
|
||||||
|
import SidebarFooter from './SidebarFooter';
|
||||||
import { useSidebarSettings } from '@/components/refactored/sidebar/hooks/useSidebarSettings';
|
import { useSidebarSettings } from '@/components/refactored/sidebar/hooks/useSidebarSettings';
|
||||||
import { useSidebarModals } from '@/components/refactored/sidebar/hooks/useSidebarModals';
|
import { useSidebarModals } from '@/components/refactored/sidebar/hooks/useSidebarModals';
|
||||||
import { useWorkspaces } from '@/components/refactored/sidebar/hooks/useWorkspaces';
|
import { useWorkspaces } from '@/components/refactored/sidebar/hooks/useWorkspaces';
|
||||||
@@ -8,6 +9,8 @@ import { SidebarWorkspaceList } from '@/components/refactored/sidebar/view/Sideb
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Button } from '@/shared/view/ui';
|
import { Button } from '@/shared/view/ui';
|
||||||
import ProjectCreationWizard from '@/components/project-creation-wizard';
|
import ProjectCreationWizard from '@/components/project-creation-wizard';
|
||||||
|
import VersionUpgradeModal from '@/components/version-upgrade/view';
|
||||||
|
import Settings from '@/components/settings/view/Settings';
|
||||||
|
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
const { isCollapsed, toggleCollapse, setCollapsed } = useSidebarSettings();
|
const { isCollapsed, toggleCollapse, setCollapsed } = useSidebarSettings();
|
||||||
@@ -50,7 +53,18 @@ export function Sidebar() {
|
|||||||
cancelSessionDelete,
|
cancelSessionDelete,
|
||||||
confirmSessionDelete,
|
confirmSessionDelete,
|
||||||
} = useWorkspaces();
|
} = useWorkspaces();
|
||||||
const { showNewProject, openNewProject, closeNewProject } = useSidebarModals();
|
|
||||||
|
const {
|
||||||
|
showNewProject,
|
||||||
|
openNewProject,
|
||||||
|
closeNewProject,
|
||||||
|
showSettingsModal,
|
||||||
|
openSettingsModal,
|
||||||
|
closeSettingsModal,
|
||||||
|
showVersionModal,
|
||||||
|
openVersionModal,
|
||||||
|
closeVersionModal,
|
||||||
|
} = useSidebarModals();
|
||||||
|
|
||||||
const handleSessionDeleteRequest = (workspacePath: string, sessionId: string) => {
|
const handleSessionDeleteRequest = (workspacePath: string, sessionId: string) => {
|
||||||
const workspace = workspaces.find(
|
const workspace = workspaces.find(
|
||||||
@@ -77,7 +91,7 @@ export function Sidebar() {
|
|||||||
|
|
||||||
<aside
|
<aside
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col bg-background/80 backdrop-blur-sm transition-all duration-300 border-r border-border h-full",
|
"flex h-full min-h-0 flex-col overflow-hidden border-r border-border bg-background/80 backdrop-blur-sm transition-all duration-300",
|
||||||
"fixed inset-y-0 left-0 z-50 md:relative md:z-0", // Make it fixed drawer on mobile, relative on desktop
|
"fixed inset-y-0 left-0 z-50 md:relative md:z-0", // Make it fixed drawer on mobile, relative on desktop
|
||||||
isCollapsed
|
isCollapsed
|
||||||
? "-translate-x-full md:translate-x-0 md:w-0 md:opacity-0 md:overflow-hidden md:border-none" // Hide fully on mobile if collapsed
|
? "-translate-x-full md:translate-x-0 md:w-0 md:opacity-0 md:overflow-hidden md:border-none" // Hide fully on mobile if collapsed
|
||||||
@@ -96,34 +110,40 @@ export function Sidebar() {
|
|||||||
onSearchFilterChange={setSearchFilter}
|
onSearchFilterChange={setSearchFilter}
|
||||||
/>
|
/>
|
||||||
{!isCollapsed && (
|
{!isCollapsed && (
|
||||||
<div className="flex-1 overflow-y-auto overscroll-contain">
|
<div className="flex min-h-0 flex-1 flex-col">
|
||||||
<SidebarWorkspaceList
|
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain">
|
||||||
workspacesCount={workspaces.length}
|
<SidebarWorkspaceList
|
||||||
searchFilter={searchFilter}
|
workspacesCount={workspaces.length}
|
||||||
starredWorkspaces={starredWorkspaces}
|
searchFilter={searchFilter}
|
||||||
unstarredWorkspaces={unstarredWorkspaces}
|
starredWorkspaces={starredWorkspaces}
|
||||||
expandedWorkspaces={expandedWorkspaces}
|
unstarredWorkspaces={unstarredWorkspaces}
|
||||||
selectedSessionId={selectedSessionId}
|
expandedWorkspaces={expandedWorkspaces}
|
||||||
editingWorkspacePath={editingWorkspacePath}
|
selectedSessionId={selectedSessionId}
|
||||||
editingWorkspaceName={editingWorkspaceName}
|
editingWorkspacePath={editingWorkspacePath}
|
||||||
isSavingWorkspaceName={isSavingWorkspaceName}
|
editingWorkspaceName={editingWorkspaceName}
|
||||||
editingSessionId={editingSessionId}
|
isSavingWorkspaceName={isSavingWorkspaceName}
|
||||||
editingSessionName={editingSessionName}
|
editingSessionId={editingSessionId}
|
||||||
isSavingSessionName={isSavingSessionName}
|
editingSessionName={editingSessionName}
|
||||||
onEditingWorkspaceNameChange={setEditingWorkspaceName}
|
isSavingSessionName={isSavingSessionName}
|
||||||
onEditingSessionNameChange={setEditingSessionName}
|
onEditingWorkspaceNameChange={setEditingWorkspaceName}
|
||||||
onToggleWorkspace={toggleWorkspace}
|
onEditingSessionNameChange={setEditingSessionName}
|
||||||
onToggleWorkspaceStar={toggleWorkspaceStar}
|
onToggleWorkspace={toggleWorkspace}
|
||||||
onStartWorkspaceRename={startWorkspaceRename}
|
onToggleWorkspaceStar={toggleWorkspaceStar}
|
||||||
onCancelWorkspaceRename={cancelWorkspaceRename}
|
onStartWorkspaceRename={startWorkspaceRename}
|
||||||
onSaveWorkspaceRename={saveWorkspaceRename}
|
onCancelWorkspaceRename={cancelWorkspaceRename}
|
||||||
onStartSessionRename={startSessionRename}
|
onSaveWorkspaceRename={saveWorkspaceRename}
|
||||||
onCancelSessionRename={cancelSessionRename}
|
onStartSessionRename={startSessionRename}
|
||||||
onSaveSessionRename={saveSessionRename}
|
onCancelSessionRename={cancelSessionRename}
|
||||||
onDeleteWorkspace={requestWorkspaceDelete}
|
onSaveSessionRename={saveSessionRename}
|
||||||
onSessionSelect={openSession}
|
onDeleteWorkspace={requestWorkspaceDelete}
|
||||||
onSessionDelete={handleSessionDeleteRequest}
|
onSessionSelect={openSession}
|
||||||
onNewSession={openNewSession}
|
onSessionDelete={handleSessionDeleteRequest}
|
||||||
|
onNewSession={openNewSession}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<SidebarFooter
|
||||||
|
onOpenSettings={openSettingsModal}
|
||||||
|
onOpenVersionModal={openVersionModal}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -161,6 +181,12 @@ export function Sidebar() {
|
|||||||
onCancelSessionDelete={cancelSessionDelete}
|
onCancelSessionDelete={cancelSessionDelete}
|
||||||
onConfirmSessionDelete={confirmSessionDelete}
|
onConfirmSessionDelete={confirmSessionDelete}
|
||||||
/>
|
/>
|
||||||
|
<VersionUpgradeModal isOpen={showVersionModal} onClose={closeVersionModal} />
|
||||||
|
<Settings
|
||||||
|
isOpen={showSettingsModal}
|
||||||
|
onClose={closeSettingsModal}
|
||||||
|
initialTab="agents"
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
123
src/components/refactored/sidebar/view/SidebarFooter.tsx
Normal file
123
src/components/refactored/sidebar/view/SidebarFooter.tsx
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import { ArrowUpCircle, Settings } from 'lucide-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useVersionCheck } from '@/hooks/useVersionCheck';
|
||||||
|
|
||||||
|
const DISCORD_INVITE_URL = 'https://discord.gg/buxwujPNRE';
|
||||||
|
|
||||||
|
function DiscordIcon({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg className={className} fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type SidebarFooterProps = {
|
||||||
|
onOpenSettings: () => void;
|
||||||
|
onOpenVersionModal: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SidebarFooter({ onOpenSettings, onOpenVersionModal }: SidebarFooterProps) {
|
||||||
|
const { t } = useTranslation('sidebar');
|
||||||
|
const { updateAvailable, releaseInfo, latestVersion } = useVersionCheck('siteboon', 'claudecodeui');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-shrink-0" style={{ paddingBottom: 'env(safe-area-inset-bottom, 0)' }}>
|
||||||
|
{updateAvailable && (
|
||||||
|
<>
|
||||||
|
<div className="nav-divider" />
|
||||||
|
|
||||||
|
<div className="hidden px-2 py-1.5 md:block">
|
||||||
|
<button
|
||||||
|
className="group flex w-full items-center gap-2.5 rounded-lg px-2.5 py-2 text-left transition-colors hover:bg-blue-50/80 dark:hover:bg-blue-900/15"
|
||||||
|
onClick={onOpenVersionModal}
|
||||||
|
>
|
||||||
|
<div className="relative flex-shrink-0">
|
||||||
|
<ArrowUpCircle className="h-4 w-4 text-blue-500 dark:text-blue-400" />
|
||||||
|
<span className="absolute -right-0.5 -top-0.5 h-1.5 w-1.5 animate-pulse rounded-full bg-blue-500" />
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<span className="block truncate text-sm font-medium text-blue-600 dark:text-blue-300">
|
||||||
|
{releaseInfo?.title || (latestVersion ? `v${latestVersion}` : '')}
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] text-blue-500/70 dark:text-blue-400/60">
|
||||||
|
{t('version.updateAvailable')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-3 py-2 md:hidden">
|
||||||
|
<button
|
||||||
|
className="flex h-11 w-full items-center gap-3 rounded-xl border border-blue-200/60 bg-blue-50/80 px-3.5 transition-all active:scale-[0.98] dark:border-blue-700/40 dark:bg-blue-900/15"
|
||||||
|
onClick={onOpenVersionModal}
|
||||||
|
>
|
||||||
|
<div className="relative flex-shrink-0">
|
||||||
|
<ArrowUpCircle className="w-4.5 h-4.5 text-blue-500 dark:text-blue-400" />
|
||||||
|
<span className="absolute -right-0.5 -top-0.5 h-1.5 w-1.5 animate-pulse rounded-full bg-blue-500" />
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1 text-left">
|
||||||
|
<span className="block truncate text-sm font-medium text-blue-600 dark:text-blue-300">
|
||||||
|
{releaseInfo?.title || (latestVersion ? `v${latestVersion}` : '')}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-blue-500/70 dark:text-blue-400/60">
|
||||||
|
{t('version.updateAvailable')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="nav-divider" />
|
||||||
|
|
||||||
|
<div className="hidden px-2 pt-1.5 md:block">
|
||||||
|
<a
|
||||||
|
href={DISCORD_INVITE_URL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex w-full items-center gap-2 rounded-lg px-2.5 py-1.5 text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground"
|
||||||
|
>
|
||||||
|
<DiscordIcon className="h-3.5 w-3.5" />
|
||||||
|
<span className="text-sm">{t('actions.joinCommunity')}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden px-2 py-1.5 md:block">
|
||||||
|
<button
|
||||||
|
className="flex w-full items-center gap-2 rounded-lg px-2.5 py-1.5 text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground"
|
||||||
|
onClick={onOpenSettings}
|
||||||
|
>
|
||||||
|
<Settings className="h-3.5 w-3.5" />
|
||||||
|
<span className="text-sm">{t('actions.settings')}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-3 pt-3 md:hidden">
|
||||||
|
<a
|
||||||
|
href={DISCORD_INVITE_URL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex h-12 w-full items-center gap-3.5 rounded-xl bg-muted/40 px-4 transition-all hover:bg-muted/60 active:scale-[0.98]"
|
||||||
|
>
|
||||||
|
<div className="flex h-8 w-8 items-center justify-center rounded-xl bg-background/80">
|
||||||
|
<DiscordIcon className="w-4.5 h-4.5 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<span className="text-base font-medium text-foreground">{t('actions.joinCommunity')}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-3 pb-20 pt-2 md:hidden">
|
||||||
|
<button
|
||||||
|
className="flex h-12 w-full items-center gap-3.5 rounded-xl bg-muted/40 px-4 transition-all hover:bg-muted/60 active:scale-[0.98]"
|
||||||
|
onClick={onOpenSettings}
|
||||||
|
>
|
||||||
|
<div className="flex h-8 w-8 items-center justify-center rounded-xl bg-background/80">
|
||||||
|
<Settings className="w-4.5 h-4.5 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<span className="text-base font-medium text-foreground">{t('actions.settings')}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -20,7 +20,6 @@ import type {
|
|||||||
McpTestResult,
|
McpTestResult,
|
||||||
NotificationPreferencesState,
|
NotificationPreferencesState,
|
||||||
SettingsMainTab,
|
SettingsMainTab,
|
||||||
SettingsProject,
|
|
||||||
} from '../types/types';
|
} from '../types/types';
|
||||||
|
|
||||||
type ThemeContextValue = {
|
type ThemeContextValue = {
|
||||||
@@ -31,7 +30,6 @@ type ThemeContextValue = {
|
|||||||
type UseSettingsControllerArgs = {
|
type UseSettingsControllerArgs = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
initialTab: string;
|
initialTab: string;
|
||||||
projects: SettingsProject[];
|
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -154,19 +152,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 toResponseJson = async <T>(response: Response): Promise<T> => response.json() as Promise<T>;
|
||||||
|
|
||||||
@@ -192,7 +177,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 { isDarkMode, toggleDarkMode } = useTheme() as ThemeContextValue;
|
||||||
const closeTimerRef = useRef<number | null>(null);
|
const closeTimerRef = useRef<number | null>(null);
|
||||||
|
|
||||||
@@ -225,7 +210,6 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
|||||||
|
|
||||||
const [showLoginModal, setShowLoginModal] = useState(false);
|
const [showLoginModal, setShowLoginModal] = useState(false);
|
||||||
const [loginProvider, setLoginProvider] = useState<ActiveLoginProvider>('');
|
const [loginProvider, setLoginProvider] = useState<ActiveLoginProvider>('');
|
||||||
const [selectedProject, setSelectedProject] = useState<SettingsProject | null>(null);
|
|
||||||
|
|
||||||
const [claudeAuthStatus, setClaudeAuthStatus] = useState<AuthStatus>(DEFAULT_AUTH_STATUS);
|
const [claudeAuthStatus, setClaudeAuthStatus] = useState<AuthStatus>(DEFAULT_AUTH_STATUS);
|
||||||
const [cursorAuthStatus, setCursorAuthStatus] = useState<AuthStatus>(DEFAULT_AUTH_STATUS);
|
const [cursorAuthStatus, setCursorAuthStatus] = useState<AuthStatus>(DEFAULT_AUTH_STATUS);
|
||||||
@@ -705,9 +689,8 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
|||||||
|
|
||||||
const openLoginForProvider = useCallback((provider: AgentProvider) => {
|
const openLoginForProvider = useCallback((provider: AgentProvider) => {
|
||||||
setLoginProvider(provider);
|
setLoginProvider(provider);
|
||||||
setSelectedProject(getDefaultProject(projects));
|
|
||||||
setShowLoginModal(true);
|
setShowLoginModal(true);
|
||||||
}, [projects]);
|
}, []);
|
||||||
|
|
||||||
const handleLoginComplete = useCallback((exitCode: number) => {
|
const handleLoginComplete = useCallback((exitCode: number) => {
|
||||||
if (exitCode !== 0 || !loginProvider) {
|
if (exitCode !== 0 || !loginProvider) {
|
||||||
@@ -904,7 +887,6 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
|||||||
showLoginModal,
|
showLoginModal,
|
||||||
setShowLoginModal,
|
setShowLoginModal,
|
||||||
loginProvider,
|
loginProvider,
|
||||||
selectedProject,
|
|
||||||
handleLoginComplete,
|
handleLoginComplete,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,11 +123,5 @@ export type CursorPermissionsState = {
|
|||||||
skipPermissions: boolean;
|
skipPermissions: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SettingsProps = {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
projects?: SettingsProject[];
|
|
||||||
initialTab?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SetState<T> = Dispatch<SetStateAction<T>>;
|
export type SetState<T> = Dispatch<SetStateAction<T>>;
|
||||||
|
|||||||
@@ -14,9 +14,15 @@ import TasksSettingsTab from '../view/tabs/tasks-settings/TasksSettingsTab';
|
|||||||
import PluginSettingsTab from '../../plugins/view/PluginSettingsTab';
|
import PluginSettingsTab from '../../plugins/view/PluginSettingsTab';
|
||||||
import { useSettingsController } from '../hooks/useSettingsController';
|
import { useSettingsController } from '../hooks/useSettingsController';
|
||||||
import { useWebPush } from '../../../hooks/useWebPush';
|
import { useWebPush } from '../../../hooks/useWebPush';
|
||||||
import type { SettingsProps } from '../types/types';
|
import { DEFAULT_PROJECT_FOR_EMPTY_SHELL } from '@/constants/config.js';
|
||||||
|
|
||||||
function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: SettingsProps) {
|
type SettingsProps = {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
initialTab?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Settings({ isOpen, onClose, initialTab = 'agents' }: SettingsProps) {
|
||||||
const { t } = useTranslation('settings');
|
const { t } = useTranslation('settings');
|
||||||
const {
|
const {
|
||||||
activeTab,
|
activeTab,
|
||||||
@@ -61,12 +67,10 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set
|
|||||||
showLoginModal,
|
showLoginModal,
|
||||||
setShowLoginModal,
|
setShowLoginModal,
|
||||||
loginProvider,
|
loginProvider,
|
||||||
selectedProject,
|
|
||||||
handleLoginComplete,
|
handleLoginComplete,
|
||||||
} = useSettingsController({
|
} = useSettingsController({
|
||||||
isOpen,
|
isOpen,
|
||||||
initialTab,
|
initialTab,
|
||||||
projects,
|
|
||||||
onClose,
|
onClose,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -116,7 +120,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set
|
|||||||
<h2 className="text-base font-semibold text-foreground">{t('title')}</h2>
|
<h2 className="text-base font-semibold text-foreground">{t('title')}</h2>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{saveStatus === 'success' && (
|
{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
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -201,7 +205,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set
|
|||||||
isOpen={showLoginModal}
|
isOpen={showLoginModal}
|
||||||
onClose={() => setShowLoginModal(false)}
|
onClose={() => setShowLoginModal(false)}
|
||||||
provider={loginProvider || 'claude'}
|
provider={loginProvider || 'claude'}
|
||||||
project={selectedProject}
|
project={DEFAULT_PROJECT_FOR_EMPTY_SHELL}
|
||||||
onComplete={handleLoginComplete}
|
onComplete={handleLoginComplete}
|
||||||
isAuthenticated={isAuthenticated}
|
isAuthenticated={isAuthenticated}
|
||||||
/>
|
/>
|
||||||
@@ -209,7 +213,6 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set
|
|||||||
<ClaudeMcpFormModal
|
<ClaudeMcpFormModal
|
||||||
isOpen={showMcpForm}
|
isOpen={showMcpForm}
|
||||||
editingServer={editingMcpServer}
|
editingServer={editingMcpServer}
|
||||||
projects={projects}
|
|
||||||
onClose={closeMcpForm}
|
onClose={closeMcpForm}
|
||||||
onSubmit={submitMcpForm}
|
onSubmit={submitMcpForm}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,17 +3,23 @@ import { useEffect, useMemo, useState } from 'react';
|
|||||||
import type { FormEvent } from 'react';
|
import type { FormEvent } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button, Input } from '../../../../shared/view/ui';
|
import { Button, Input } from '../../../../shared/view/ui';
|
||||||
|
import { authenticatedFetch } from '../../../../utils/api';
|
||||||
import { DEFAULT_CLAUDE_MCP_FORM } from '../../constants/constants';
|
import { DEFAULT_CLAUDE_MCP_FORM } from '../../constants/constants';
|
||||||
import type { ClaudeMcpFormState, McpServer, McpScope, McpTransportType, SettingsProject } from '../../types/types';
|
import type { ClaudeMcpFormState, McpServer, McpScope, McpTransportType } from '../../types/types';
|
||||||
|
|
||||||
type ClaudeMcpFormModalProps = {
|
type ClaudeMcpFormModalProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
editingServer: McpServer | null;
|
editingServer: McpServer | null;
|
||||||
projects: SettingsProject[];
|
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSubmit: (formData: ClaudeMcpFormState, editingServer: McpServer | null) => Promise<void>;
|
onSubmit: (formData: ClaudeMcpFormState, editingServer: McpServer | null) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ProjectApiRecord = {
|
||||||
|
workspaceOriginalPath?: string;
|
||||||
|
workspaceCustomName?: string | null;
|
||||||
|
workspaceDisplayName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
const getSafeTransportType = (value: unknown): McpTransportType => {
|
const getSafeTransportType = (value: unknown): McpTransportType => {
|
||||||
if (value === 'sse' || value === 'http') {
|
if (value === 'sse' || value === 'http') {
|
||||||
return value;
|
return value;
|
||||||
@@ -49,7 +55,6 @@ const createFormStateFromServer = (server: McpServer): ClaudeMcpFormState => ({
|
|||||||
export default function ClaudeMcpFormModal({
|
export default function ClaudeMcpFormModal({
|
||||||
isOpen,
|
isOpen,
|
||||||
editingServer,
|
editingServer,
|
||||||
projects,
|
|
||||||
onClose,
|
onClose,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
}: ClaudeMcpFormModalProps) {
|
}: ClaudeMcpFormModalProps) {
|
||||||
@@ -57,6 +62,9 @@ export default function ClaudeMcpFormModal({
|
|||||||
const [formData, setFormData] = useState<ClaudeMcpFormState>(DEFAULT_CLAUDE_MCP_FORM);
|
const [formData, setFormData] = useState<ClaudeMcpFormState>(DEFAULT_CLAUDE_MCP_FORM);
|
||||||
const [jsonValidationError, setJsonValidationError] = useState('');
|
const [jsonValidationError, setJsonValidationError] = useState('');
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [projects, setProjects] = useState<ProjectApiRecord[]>([]);
|
||||||
|
const [projectsLoading, setProjectsLoading] = useState(false);
|
||||||
|
const [projectsError, setProjectsError] = useState<string | null>(null);
|
||||||
|
|
||||||
const isEditing = Boolean(editingServer);
|
const isEditing = Boolean(editingServer);
|
||||||
|
|
||||||
@@ -74,6 +82,51 @@ export default function ClaudeMcpFormModal({
|
|||||||
setFormData(DEFAULT_CLAUDE_MCP_FORM);
|
setFormData(DEFAULT_CLAUDE_MCP_FORM);
|
||||||
}, [editingServer, isOpen]);
|
}, [editingServer, isOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
const fetchProjects = async () => {
|
||||||
|
setProjectsLoading(true);
|
||||||
|
setProjectsError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await authenticatedFetch('/api/sidebar/get-workspaces-sessions');
|
||||||
|
const data = (await response.json()) as unknown;
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch projects');
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawProjects = Array.isArray((data as { workspaces?: unknown }).workspaces)
|
||||||
|
? (data as { workspaces: ProjectApiRecord[] }).workspaces
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (!cancelled) {
|
||||||
|
setProjects(rawProjects);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (!cancelled) {
|
||||||
|
setProjects([]);
|
||||||
|
setProjectsError(getErrorMessage(error));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (!cancelled) {
|
||||||
|
setProjectsLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void fetchProjects();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
const canSubmit = useMemo(() => {
|
const canSubmit = useMemo(() => {
|
||||||
if (!formData.name.trim()) {
|
if (!formData.name.trim()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -147,7 +200,7 @@ export default function ClaudeMcpFormModal({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-[110] flex items-center justify-center bg-black/50 p-4">
|
<div className="fixed inset-0 z-[10050] flex items-center justify-center bg-black/50 p-4">
|
||||||
<div className="max-h-[90vh] w-full max-w-2xl overflow-y-auto rounded-lg border border-border bg-background">
|
<div className="max-h-[90vh] w-full max-w-2xl overflow-y-auto rounded-lg border border-border bg-background">
|
||||||
<div className="flex items-center justify-between border-b border-border p-4">
|
<div className="flex items-center justify-between border-b border-border p-4">
|
||||||
<h3 className="text-lg font-medium text-foreground">
|
<h3 className="text-lg font-medium text-foreground">
|
||||||
@@ -260,13 +313,19 @@ export default function ClaudeMcpFormModal({
|
|||||||
className="w-full rounded-lg border border-gray-300 bg-gray-50 px-3 py-2 text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
|
className="w-full rounded-lg border border-gray-300 bg-gray-50 px-3 py-2 text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
<option value="">{t('mcpForm.fields.selectProject')}...</option>
|
<option value="">{projectsLoading ? 'Loading projects...' : `${t('mcpForm.fields.selectProject')}...`}</option>
|
||||||
{projects.map((project) => (
|
{projects.map((project) => (
|
||||||
<option key={project.name} value={project.path || project.fullPath}>
|
<option key={project.workspaceOriginalPath} value={project.workspaceOriginalPath}>
|
||||||
{project.displayName || project.name}
|
{project.workspaceDisplayName}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
{projectsError && (
|
||||||
|
<p className="mt-1 text-xs text-red-500">{projectsError}</p>
|
||||||
|
)}
|
||||||
|
{!projectsLoading && !projectsError && projects.length === 0 && (
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">No projects found.</p>
|
||||||
|
)}
|
||||||
{formData.projectPath && (
|
{formData.projectPath && (
|
||||||
<p className="mt-1 text-xs text-muted-foreground">
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
{t('mcpForm.projectPath', { path: formData.projectPath })}
|
{t('mcpForm.projectPath', { path: formData.projectPath })}
|
||||||
|
|||||||
@@ -3,3 +3,16 @@
|
|||||||
* Indicates if the app is running in Platform mode (hosted) or OSS mode (self-hosted)
|
* 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' : '',
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user