mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-04 11:45:38 +08:00
feat: implement platform-specific provider visibility for cursor agent
This commit is contained in:
@@ -1,7 +1,18 @@
|
|||||||
|
import os from 'node:os';
|
||||||
|
|
||||||
import { providerRegistry } from '@/modules/providers/provider.registry.js';
|
import { providerRegistry } from '@/modules/providers/provider.registry.js';
|
||||||
import type { LLMProvider, McpScope, ProviderMcpServer, UpsertProviderMcpServerInput } from '@/shared/types.js';
|
import type { LLMProvider, McpScope, ProviderMcpServer, UpsertProviderMcpServerInput } from '@/shared/types.js';
|
||||||
import { AppError } from '@/shared/utils.js';
|
import { AppError } from '@/shared/utils.js';
|
||||||
|
|
||||||
|
/** Cursor MCP is not supported on Windows hosts (no Cursor CLI integration). */
|
||||||
|
function includeProviderInGlobalMcp(providerId: LLMProvider): boolean {
|
||||||
|
if (providerId === 'cursor' && os.platform() === 'win32') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export const providerMcpService = {
|
export const providerMcpService = {
|
||||||
/**
|
/**
|
||||||
@@ -83,7 +94,7 @@ export const providerMcpService = {
|
|||||||
|
|
||||||
const scope = input.scope ?? 'project';
|
const scope = input.scope ?? 'project';
|
||||||
const results: Array<{ provider: LLMProvider; created: boolean; error?: string }> = [];
|
const results: Array<{ provider: LLMProvider; created: boolean; error?: string }> = [];
|
||||||
const providers = providerRegistry.listProviders();
|
const providers = providerRegistry.listProviders().filter((p) => includeProviderInGlobalMcp(p.id));
|
||||||
for (const provider of providers) {
|
for (const provider of providers) {
|
||||||
try {
|
try {
|
||||||
await provider.mcp.upsertServer({ ...input, scope });
|
await provider.mcp.upsertServer({ ...input, scope });
|
||||||
|
|||||||
@@ -255,7 +255,8 @@ test('providerMcpService global adder writes to all providers and rejects unsupp
|
|||||||
workspacePath,
|
workspacePath,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(globalResult.length, 4);
|
const expectCursorGlobal = process.platform !== 'win32';
|
||||||
|
assert.equal(globalResult.length, expectCursorGlobal ? 4 : 3);
|
||||||
assert.ok(globalResult.every((entry) => entry.created === true));
|
assert.ok(globalResult.every((entry) => entry.created === true));
|
||||||
|
|
||||||
const claudeProject = await readJson(path.join(workspacePath, '.mcp.json'));
|
const claudeProject = await readJson(path.join(workspacePath, '.mcp.json'));
|
||||||
@@ -267,8 +268,10 @@ test('providerMcpService global adder writes to all providers and rejects unsupp
|
|||||||
const geminiProject = await readJson(path.join(workspacePath, '.gemini', 'settings.json'));
|
const geminiProject = await readJson(path.join(workspacePath, '.gemini', 'settings.json'));
|
||||||
assert.ok((geminiProject.mcpServers as Record<string, unknown>)['global-http']);
|
assert.ok((geminiProject.mcpServers as Record<string, unknown>)['global-http']);
|
||||||
|
|
||||||
const cursorProject = await readJson(path.join(workspacePath, '.cursor', 'mcp.json'));
|
if (expectCursorGlobal) {
|
||||||
assert.ok((cursorProject.mcpServers as Record<string, unknown>)['global-http']);
|
const cursorProject = await readJson(path.join(workspacePath, '.cursor', 'mcp.json'));
|
||||||
|
assert.ok((cursorProject.mcpServers as Record<string, unknown>)['global-http']);
|
||||||
|
}
|
||||||
|
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
providerMcpService.addMcpServerToAllProviders({
|
providerMcpService.addMcpServerToAllProviders({
|
||||||
|
|||||||
@@ -273,4 +273,14 @@ router.post('/push/unsubscribe', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Host OS for UI (e.g. hide Cursor agent when the backend runs on Windows).
|
||||||
|
router.get('/server-env', async (req, res) => {
|
||||||
|
try {
|
||||||
|
res.json({ platform: process.platform });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading server environment:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to read server environment' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from "react";
|
import React, { useEffect, useMemo } from "react";
|
||||||
import { Check, ChevronDown } from "lucide-react";
|
import { Check, ChevronDown } from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useServerPlatform } from "../../../../hooks/useServerPlatform";
|
||||||
import SessionProviderLogo from "../../../llm-logo-provider/SessionProviderLogo";
|
import SessionProviderLogo from "../../../llm-logo-provider/SessionProviderLogo";
|
||||||
import {
|
import {
|
||||||
CLAUDE_MODELS,
|
CLAUDE_MODELS,
|
||||||
@@ -115,6 +116,23 @@ export default function ProviderSelectionEmptyState({
|
|||||||
setInput,
|
setInput,
|
||||||
}: ProviderSelectionEmptyStateProps) {
|
}: ProviderSelectionEmptyStateProps) {
|
||||||
const { t } = useTranslation("chat");
|
const { t } = useTranslation("chat");
|
||||||
|
const { isWindowsServer } = useServerPlatform();
|
||||||
|
|
||||||
|
const visibleProviders = useMemo(
|
||||||
|
() =>
|
||||||
|
isWindowsServer
|
||||||
|
? PROVIDERS.filter((p) => p.id !== "cursor")
|
||||||
|
: PROVIDERS,
|
||||||
|
[isWindowsServer],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isWindowsServer && provider === "cursor") {
|
||||||
|
setProvider("claude");
|
||||||
|
localStorage.setItem("selected-provider", "claude");
|
||||||
|
}
|
||||||
|
}, [isWindowsServer, provider, setProvider]);
|
||||||
|
|
||||||
const nextTaskPrompt = t("tasks.nextTaskPrompt", {
|
const nextTaskPrompt = t("tasks.nextTaskPrompt", {
|
||||||
defaultValue: "Start the next task",
|
defaultValue: "Start the next task",
|
||||||
});
|
});
|
||||||
@@ -166,8 +184,10 @@ export default function ProviderSelectionEmptyState({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Provider cards — horizontal row, equal width */}
|
{/* Provider cards — horizontal row, equal width */}
|
||||||
<div className="mb-6 grid grid-cols-2 gap-2 sm:grid-cols-4 sm:gap-2.5">
|
<div
|
||||||
{PROVIDERS.map((p) => {
|
className={`mb-6 grid grid-cols-2 gap-2 sm:gap-2.5 ${visibleProviders.length >= 4 ? "sm:grid-cols-4" : "sm:grid-cols-3"}`}
|
||||||
|
>
|
||||||
|
{visibleProviders.map((p) => {
|
||||||
const active = provider === p.id;
|
const active = provider === p.id;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { useServerPlatform } from '../../../../../hooks/useServerPlatform';
|
||||||
import type { AgentCategory, AgentProvider } from '../../../types/types';
|
import type { AgentCategory, AgentProvider } from '../../../types/types';
|
||||||
|
|
||||||
import type { AgentContext, AgentsSettingsTabProps } from './types';
|
import type { AgentContext, AgentsSettingsTabProps } from './types';
|
||||||
@@ -22,6 +23,22 @@ export default function AgentsSettingsTab({
|
|||||||
}: AgentsSettingsTabProps) {
|
}: AgentsSettingsTabProps) {
|
||||||
const [selectedAgent, setSelectedAgent] = useState<AgentProvider>('claude');
|
const [selectedAgent, setSelectedAgent] = useState<AgentProvider>('claude');
|
||||||
const [selectedCategory, setSelectedCategory] = useState<AgentCategory>('account');
|
const [selectedCategory, setSelectedCategory] = useState<AgentCategory>('account');
|
||||||
|
const { isWindowsServer } = useServerPlatform();
|
||||||
|
|
||||||
|
const visibleAgents = useMemo<AgentProvider[]>(() => {
|
||||||
|
const all: AgentProvider[] = ['claude', 'cursor', 'codex', 'gemini'];
|
||||||
|
if (isWindowsServer) {
|
||||||
|
return all.filter((id) => id !== 'cursor');
|
||||||
|
}
|
||||||
|
|
||||||
|
return all;
|
||||||
|
}, [isWindowsServer]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isWindowsServer && selectedAgent === 'cursor') {
|
||||||
|
setSelectedAgent('claude');
|
||||||
|
}
|
||||||
|
}, [isWindowsServer, selectedAgent]);
|
||||||
|
|
||||||
const agentContextById = useMemo<Record<AgentProvider, AgentContext>>(() => ({
|
const agentContextById = useMemo<Record<AgentProvider, AgentContext>>(() => ({
|
||||||
claude: {
|
claude: {
|
||||||
@@ -51,6 +68,7 @@ export default function AgentsSettingsTab({
|
|||||||
return (
|
return (
|
||||||
<div className="-mx-4 -mb-4 -mt-2 flex min-h-[300px] flex-col overflow-hidden md:-mx-6 md:-mb-6 md:-mt-2 md:min-h-[500px]">
|
<div className="-mx-4 -mb-4 -mt-2 flex min-h-[300px] flex-col overflow-hidden md:-mx-6 md:-mb-6 md:-mt-2 md:min-h-[500px]">
|
||||||
<AgentSelectorSection
|
<AgentSelectorSection
|
||||||
|
agents={visibleAgents}
|
||||||
selectedAgent={selectedAgent}
|
selectedAgent={selectedAgent}
|
||||||
onSelectAgent={setSelectedAgent}
|
onSelectAgent={setSelectedAgent}
|
||||||
agentContextById={agentContextById}
|
agentContextById={agentContextById}
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import SessionProviderLogo from '../../../../../llm-logo-provider/SessionProvide
|
|||||||
import type { AgentProvider } from '../../../../types/types';
|
import type { AgentProvider } from '../../../../types/types';
|
||||||
import type { AgentSelectorSectionProps } from '../types';
|
import type { AgentSelectorSectionProps } from '../types';
|
||||||
|
|
||||||
const AGENT_PROVIDERS: AgentProvider[] = ['claude', 'cursor', 'codex', 'gemini'];
|
|
||||||
|
|
||||||
const AGENT_NAMES: Record<AgentProvider, string> = {
|
const AGENT_NAMES: Record<AgentProvider, string> = {
|
||||||
claude: 'Claude',
|
claude: 'Claude',
|
||||||
cursor: 'Cursor',
|
cursor: 'Cursor',
|
||||||
@@ -13,6 +11,7 @@ const AGENT_NAMES: Record<AgentProvider, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function AgentSelectorSection({
|
export default function AgentSelectorSection({
|
||||||
|
agents,
|
||||||
selectedAgent,
|
selectedAgent,
|
||||||
onSelectAgent,
|
onSelectAgent,
|
||||||
agentContextById,
|
agentContextById,
|
||||||
@@ -20,7 +19,7 @@ export default function AgentSelectorSection({
|
|||||||
return (
|
return (
|
||||||
<div className="flex-shrink-0 border-b border-border px-3 py-2 md:px-4 md:py-3">
|
<div className="flex-shrink-0 border-b border-border px-3 py-2 md:px-4 md:py-3">
|
||||||
<PillBar className="w-full md:w-auto">
|
<PillBar className="w-full md:w-auto">
|
||||||
{AGENT_PROVIDERS.map((agent) => {
|
{agents.map((agent) => {
|
||||||
const dotColor =
|
const dotColor =
|
||||||
agent === 'claude' ? 'bg-blue-500' :
|
agent === 'claude' ? 'bg-blue-500' :
|
||||||
agent === 'cursor' ? 'bg-purple-500' :
|
agent === 'cursor' ? 'bg-purple-500' :
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export type AgentCategoryTabsSectionProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type AgentSelectorSectionProps = {
|
export type AgentSelectorSectionProps = {
|
||||||
|
agents: AgentProvider[];
|
||||||
selectedAgent: AgentProvider;
|
selectedAgent: AgentProvider;
|
||||||
onSelectAgent: (agent: AgentProvider) => void;
|
onSelectAgent: (agent: AgentProvider) => void;
|
||||||
agentContextById: AgentContextByProvider;
|
agentContextById: AgentContextByProvider;
|
||||||
|
|||||||
40
src/hooks/useServerPlatform.ts
Normal file
40
src/hooks/useServerPlatform.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { authenticatedFetch } from '../utils/api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Node `process.platform` from the API host (e.g. win32, darwin, linux).
|
||||||
|
* Null until loaded or if the request fails.
|
||||||
|
*/
|
||||||
|
export function useServerPlatform(): {
|
||||||
|
serverPlatform: string | null;
|
||||||
|
isWindowsServer: boolean;
|
||||||
|
} {
|
||||||
|
const [serverPlatform, setServerPlatform] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const response = await authenticatedFetch('/api/settings/server-env');
|
||||||
|
if (!response.ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const body = (await response.json()) as { platform?: string };
|
||||||
|
if (!cancelled && typeof body.platform === 'string') {
|
||||||
|
setServerPlatform(body.platform);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Keep null: treat as unknown host.
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
serverPlatform,
|
||||||
|
isWindowsServer: serverPlatform === 'win32',
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user