mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
feat(cli-settings): support multi-provider settings for Claude, Codex, and Gemini
Decouple CLI settings architecture from Claude-only to support multiple
providers. Each provider has independent settings UI and backend handling.
- Add CliProvider type discriminator ('claude' | 'codex' | 'gemini')
- Add CodexCliSettings (profile, authJson, configToml) and GeminiCliSettings types
- Update EndpointSettings with provider field (defaults 'claude' for backward compat)
- Refactor CliSettingsModal with provider selector and provider-specific forms
- Remove includeCoAuthoredBy field across all layers
- Extend CliConfigModal to show Config Profile for all tools (not just claude)
- Add provider-aware argument injection in cli-session-manager (--settings/--profile/env)
- Rename addClaudeCustomEndpoint to addCustomEndpoint (old name kept as deprecated alias)
- Replace providerBasedCount/directCount with per-provider counts in useCliSettings hook
- Update CliSettingsList with provider badges and per-provider stat cards
- Add Codex and Gemini test cases for validateSettings and createDefaultSettings
This commit is contained in:
@@ -63,23 +63,18 @@ function CliSettingsCard({
|
|||||||
}: CliSettingsCardProps) {
|
}: CliSettingsCardProps) {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
// Determine mode based on settings
|
// Display provider badge
|
||||||
const isProviderBased = Boolean(
|
const getProviderBadge = () => {
|
||||||
cliSettings.settings.env.ANTHROPIC_BASE_URL &&
|
const provider = cliSettings.provider || 'claude';
|
||||||
!cliSettings.settings.env.ANTHROPIC_BASE_URL.includes('api.anthropic.com')
|
const variants: Record<string, { variant: 'secondary' | 'outline' | 'default'; label: string }> = {
|
||||||
);
|
claude: { variant: 'secondary', label: 'Claude' },
|
||||||
|
codex: { variant: 'outline', label: 'Codex' },
|
||||||
const getModeBadge = () => {
|
gemini: { variant: 'default', label: 'Gemini' },
|
||||||
if (isProviderBased) {
|
};
|
||||||
|
const config = variants[provider] || variants.claude;
|
||||||
return (
|
return (
|
||||||
<Badge variant="secondary" className="text-xs">
|
<Badge variant={config.variant} className="text-xs">
|
||||||
{formatMessage({ id: 'apiSettings.cliSettings.providerBased' })}
|
{config.label}
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Badge variant="outline" className="text-xs">
|
|
||||||
{formatMessage({ id: 'apiSettings.cliSettings.direct' })}
|
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -91,6 +86,12 @@ function CliSettingsCard({
|
|||||||
return <Badge variant="success">{formatMessage({ id: 'apiSettings.common.enabled' })}</Badge>;
|
return <Badge variant="success">{formatMessage({ id: 'apiSettings.common.enabled' })}</Badge>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get provider-appropriate endpoint URL for display
|
||||||
|
const endpointUrl = (() => {
|
||||||
|
const env = cliSettings.settings.env;
|
||||||
|
return env.ANTHROPIC_BASE_URL || env.OPENAI_BASE_URL || '';
|
||||||
|
})();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="p-4">
|
<Card className="p-4">
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4">
|
||||||
@@ -99,7 +100,7 @@ function CliSettingsCard({
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<h3 className="text-lg font-semibold text-foreground truncate">{cliSettings.name}</h3>
|
<h3 className="text-lg font-semibold text-foreground truncate">{cliSettings.name}</h3>
|
||||||
{getStatusBadge()}
|
{getStatusBadge()}
|
||||||
{getModeBadge()}
|
{getProviderBadge()}
|
||||||
</div>
|
</div>
|
||||||
{cliSettings.description && (
|
{cliSettings.description && (
|
||||||
<p className="text-sm text-muted-foreground mt-1">{cliSettings.description}</p>
|
<p className="text-sm text-muted-foreground mt-1">{cliSettings.description}</p>
|
||||||
@@ -107,17 +108,12 @@ function CliSettingsCard({
|
|||||||
<div className="flex items-center gap-4 mt-2 text-sm text-muted-foreground">
|
<div className="flex items-center gap-4 mt-2 text-sm text-muted-foreground">
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<Settings className="w-3 h-3" />
|
<Settings className="w-3 h-3" />
|
||||||
{cliSettings.settings.model || 'sonnet'}
|
{cliSettings.settings.model || 'default'}
|
||||||
</span>
|
</span>
|
||||||
{cliSettings.settings.env.ANTHROPIC_BASE_URL && (
|
{endpointUrl && (
|
||||||
<span className="flex items-center gap-1 truncate max-w-[200px]" title={cliSettings.settings.env.ANTHROPIC_BASE_URL}>
|
<span className="flex items-center gap-1 truncate max-w-[200px]" title={endpointUrl}>
|
||||||
<LinkIcon className="w-3 h-3 flex-shrink-0" />
|
<LinkIcon className="w-3 h-3 flex-shrink-0" />
|
||||||
{cliSettings.settings.env.ANTHROPIC_BASE_URL}
|
{endpointUrl}
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{cliSettings.settings.includeCoAuthoredBy !== undefined && (
|
|
||||||
<span>
|
|
||||||
{formatMessage({ id: 'apiSettings.cliSettings.coAuthoredBy' })}: {formatMessage({ id: cliSettings.settings.includeCoAuthoredBy ? 'common.yes' : 'common.no' })}
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -168,8 +164,7 @@ export function CliSettingsList({
|
|||||||
cliSettings,
|
cliSettings,
|
||||||
totalCount,
|
totalCount,
|
||||||
enabledCount,
|
enabledCount,
|
||||||
providerBasedCount,
|
providerCounts,
|
||||||
directCount,
|
|
||||||
isLoading,
|
isLoading,
|
||||||
refetch,
|
refetch,
|
||||||
} = useCliSettings();
|
} = useCliSettings();
|
||||||
@@ -219,7 +214,7 @@ export function CliSettingsList({
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Stats Cards */}
|
{/* Stats Cards */}
|
||||||
<div className="grid grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 sm:grid-cols-5 gap-4">
|
||||||
<Card className="p-4">
|
<Card className="p-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Settings className="w-5 h-5 text-primary" />
|
<Settings className="w-5 h-5 text-primary" />
|
||||||
@@ -240,21 +235,21 @@ export function CliSettingsList({
|
|||||||
</Card>
|
</Card>
|
||||||
<Card className="p-4">
|
<Card className="p-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<LinkIcon className="w-5 h-5 text-blue-500" />
|
<span className="text-2xl font-bold">{providerCounts.claude || 0}</span>
|
||||||
<span className="text-2xl font-bold">{providerBasedCount}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
<p className="text-sm text-muted-foreground mt-1">Claude</p>
|
||||||
{formatMessage({ id: 'apiSettings.cliSettings.providerBased' })}
|
|
||||||
</p>
|
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="p-4">
|
<Card className="p-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Settings className="w-5 h-5 text-orange-500" />
|
<span className="text-2xl font-bold">{providerCounts.codex || 0}</span>
|
||||||
<span className="text-2xl font-bold">{directCount}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
<p className="text-sm text-muted-foreground mt-1">Codex</p>
|
||||||
{formatMessage({ id: 'apiSettings.cliSettings.direct' })}
|
</Card>
|
||||||
</p>
|
<Card className="p-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-2xl font-bold">{providerCounts.gemini || 0}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">Gemini</p>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// ========================================
|
// ========================================
|
||||||
// CLI Settings Modal Component
|
// CLI Settings Modal Component
|
||||||
// ========================================
|
// ========================================
|
||||||
// Add/Edit CLI settings modal with provider-based and direct modes
|
// Add/Edit CLI settings modal with multi-provider support (Claude/Codex/Gemini)
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
@@ -23,7 +23,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
|||||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/Tabs';
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/Tabs';
|
||||||
import { useCreateCliSettings, useUpdateCliSettings, useProviders } from '@/hooks/useApiSettings';
|
import { useCreateCliSettings, useUpdateCliSettings, useProviders } from '@/hooks/useApiSettings';
|
||||||
import { useNotifications } from '@/hooks/useNotifications';
|
import { useNotifications } from '@/hooks/useNotifications';
|
||||||
import type { CliSettingsEndpoint } from '@/lib/api';
|
import type { CliSettingsEndpoint, CliProvider } from '@/lib/api';
|
||||||
|
|
||||||
// ========== Types ==========
|
// ========== Types ==========
|
||||||
|
|
||||||
@@ -31,34 +31,39 @@ export interface CliSettingsModalProps {
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
cliSettings?: CliSettingsEndpoint | null;
|
cliSettings?: CliSettingsEndpoint | null;
|
||||||
|
/** Pre-selected provider when creating new */
|
||||||
|
defaultProvider?: CliProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ModeType = 'provider-based' | 'direct';
|
type ModeType = 'provider-based' | 'direct';
|
||||||
|
|
||||||
// ========== Helper Functions ==========
|
// ========== Helper Functions ==========
|
||||||
|
|
||||||
function parseConfigJson(
|
function parseConfigText(
|
||||||
configJson: string
|
text: string,
|
||||||
): { ok: true; value: Record<string, unknown> } | { ok: false; errorKey: string } {
|
format: 'json' | 'toml' = 'json'
|
||||||
const trimmed = configJson.trim();
|
): { ok: true; value: string } | { ok: false; errorKey: string } {
|
||||||
|
const trimmed = text.trim();
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
return { ok: true, value: {} };
|
return { ok: true, value: '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (format === 'json') {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(trimmed) as unknown;
|
JSON.parse(trimmed);
|
||||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
return { ok: true, value: trimmed };
|
||||||
return { ok: false, errorKey: 'configMustBeObject' };
|
|
||||||
}
|
|
||||||
return { ok: true, value: parsed as Record<string, unknown> };
|
|
||||||
} catch {
|
} catch {
|
||||||
return { ok: false, errorKey: 'invalidJson' };
|
return { ok: false, errorKey: 'invalidJson' };
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TOML: basic format check (no full parser needed)
|
||||||
|
return { ok: true, value: trimmed };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Main Component ==========
|
// ========== Main Component ==========
|
||||||
|
|
||||||
export function CliSettingsModal({ open, onClose, cliSettings }: CliSettingsModalProps) {
|
export function CliSettingsModal({ open, onClose, cliSettings, defaultProvider }: CliSettingsModalProps) {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { error } = useNotifications();
|
const { error } = useNotifications();
|
||||||
const isEditing = !!cliSettings;
|
const isEditing = !!cliSettings;
|
||||||
@@ -67,83 +72,105 @@ export function CliSettingsModal({ open, onClose, cliSettings }: CliSettingsModa
|
|||||||
const { createCliSettings, isCreating } = useCreateCliSettings();
|
const { createCliSettings, isCreating } = useCreateCliSettings();
|
||||||
const { updateCliSettings, isUpdating } = useUpdateCliSettings();
|
const { updateCliSettings, isUpdating } = useUpdateCliSettings();
|
||||||
|
|
||||||
// Get providers for provider-based mode
|
// Get providers for provider-based mode (Claude)
|
||||||
const { providers } = useProviders();
|
const { providers } = useProviders();
|
||||||
|
|
||||||
// Form state
|
// Common form state
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [description, setDescription] = useState('');
|
const [description, setDescription] = useState('');
|
||||||
const [enabled, setEnabled] = useState(true);
|
const [enabled, setEnabled] = useState(true);
|
||||||
|
const [cliProvider, setCliProvider] = useState<CliProvider>('claude');
|
||||||
|
|
||||||
|
// Claude: mode tabs
|
||||||
const [mode, setMode] = useState<ModeType>('direct');
|
const [mode, setMode] = useState<ModeType>('direct');
|
||||||
|
|
||||||
// Provider-based mode state
|
|
||||||
const [providerId, setProviderId] = useState('');
|
const [providerId, setProviderId] = useState('');
|
||||||
const [model, setModel] = useState('sonnet');
|
|
||||||
const [includeCoAuthoredBy, setIncludeCoAuthoredBy] = useState(false);
|
|
||||||
const [settingsFile, setSettingsFile] = useState('');
|
|
||||||
|
|
||||||
// Direct mode state
|
|
||||||
const [authToken, setAuthToken] = useState('');
|
const [authToken, setAuthToken] = useState('');
|
||||||
const [baseUrl, setBaseUrl] = useState('');
|
const [baseUrl, setBaseUrl] = useState('');
|
||||||
const [showToken, setShowToken] = useState(false);
|
const [showToken, setShowToken] = useState(false);
|
||||||
|
const [settingsFile, setSettingsFile] = useState('');
|
||||||
|
|
||||||
// Available models state
|
// Codex specific
|
||||||
|
const [codexApiKey, setCodexApiKey] = useState('');
|
||||||
|
const [codexBaseUrl, setCodexBaseUrl] = useState('');
|
||||||
|
const [codexProfile, setCodexProfile] = useState('');
|
||||||
|
const [showCodexKey, setShowCodexKey] = useState(false);
|
||||||
|
const [authJson, setAuthJson] = useState('');
|
||||||
|
const [configToml, setConfigToml] = useState('');
|
||||||
|
const [writeCommonConfig, setWriteCommonConfig] = useState(false);
|
||||||
|
|
||||||
|
// Gemini specific
|
||||||
|
const [geminiApiKey, setGeminiApiKey] = useState('');
|
||||||
|
const [showGeminiKey, setShowGeminiKey] = useState(false);
|
||||||
|
|
||||||
|
// Shared
|
||||||
|
const [model, setModel] = useState('');
|
||||||
const [availableModels, setAvailableModels] = useState<string[]>([]);
|
const [availableModels, setAvailableModels] = useState<string[]>([]);
|
||||||
const [modelInput, setModelInput] = useState('');
|
const [modelInput, setModelInput] = useState('');
|
||||||
|
|
||||||
// Tags state
|
|
||||||
const [tags, setTags] = useState<string[]>([]);
|
const [tags, setTags] = useState<string[]>([]);
|
||||||
const [tagInput, setTagInput] = useState('');
|
const [tagInput, setTagInput] = useState('');
|
||||||
|
|
||||||
// JSON config state
|
|
||||||
const [configJson, setConfigJson] = useState('{}');
|
const [configJson, setConfigJson] = useState('{}');
|
||||||
const [showJsonInput, setShowJsonInput] = useState(false);
|
const [showJsonInput, setShowJsonInput] = useState(false);
|
||||||
|
|
||||||
// Validation errors
|
|
||||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
// Initialize form from cliSettings
|
// Initialize form
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (cliSettings) {
|
if (cliSettings) {
|
||||||
setName(cliSettings.name);
|
setName(cliSettings.name);
|
||||||
setDescription(cliSettings.description || '');
|
setDescription(cliSettings.description || '');
|
||||||
setEnabled(cliSettings.enabled);
|
setEnabled(cliSettings.enabled);
|
||||||
setModel(cliSettings.settings.model || 'sonnet');
|
setCliProvider(cliSettings.provider || 'claude');
|
||||||
setIncludeCoAuthoredBy(cliSettings.settings.includeCoAuthoredBy || false);
|
setModel(cliSettings.settings.model || '');
|
||||||
setSettingsFile(cliSettings.settings.settingsFile || '');
|
|
||||||
setAvailableModels(cliSettings.settings.availableModels || []);
|
setAvailableModels(cliSettings.settings.availableModels || []);
|
||||||
setTags(cliSettings.settings.tags || []);
|
setTags(cliSettings.settings.tags || []);
|
||||||
|
|
||||||
// Determine mode based on settings
|
const provider = cliSettings.provider || 'claude';
|
||||||
|
if (provider === 'claude') {
|
||||||
|
setSettingsFile((cliSettings.settings as any).settingsFile || '');
|
||||||
|
const env = cliSettings.settings.env;
|
||||||
const hasCustomBaseUrl = Boolean(
|
const hasCustomBaseUrl = Boolean(
|
||||||
cliSettings.settings.env.ANTHROPIC_BASE_URL &&
|
env.ANTHROPIC_BASE_URL && !env.ANTHROPIC_BASE_URL.includes('api.anthropic.com')
|
||||||
!cliSettings.settings.env.ANTHROPIC_BASE_URL.includes('api.anthropic.com')
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (hasCustomBaseUrl) {
|
if (hasCustomBaseUrl) {
|
||||||
setMode('direct');
|
setMode('direct');
|
||||||
setBaseUrl(cliSettings.settings.env.ANTHROPIC_BASE_URL || '');
|
setBaseUrl(env.ANTHROPIC_BASE_URL || '');
|
||||||
setAuthToken(cliSettings.settings.env.ANTHROPIC_AUTH_TOKEN || '');
|
setAuthToken(env.ANTHROPIC_AUTH_TOKEN || '');
|
||||||
} else {
|
} else {
|
||||||
setMode('provider-based');
|
setMode('provider-based');
|
||||||
// Try to find matching provider
|
const matchingProvider = providers.find((p) => p.apiBase === env.ANTHROPIC_BASE_URL);
|
||||||
const matchingProvider = providers.find((p) => p.apiBase === cliSettings.settings.env.ANTHROPIC_BASE_URL);
|
if (matchingProvider) setProviderId(matchingProvider.id);
|
||||||
if (matchingProvider) {
|
|
||||||
setProviderId(matchingProvider.id);
|
|
||||||
}
|
}
|
||||||
|
} else if (provider === 'codex') {
|
||||||
|
const s = cliSettings.settings as any;
|
||||||
|
setCodexApiKey(s.env.OPENAI_API_KEY || '');
|
||||||
|
setCodexBaseUrl(s.env.OPENAI_BASE_URL || '');
|
||||||
|
setCodexProfile(s.profile || '');
|
||||||
|
setAuthJson(s.authJson || '');
|
||||||
|
setConfigToml(s.configToml || '');
|
||||||
|
} else if (provider === 'gemini') {
|
||||||
|
setGeminiApiKey(cliSettings.settings.env.GEMINI_API_KEY || cliSettings.settings.env.GOOGLE_API_KEY || '');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Reset form for new CLI settings
|
// Reset for new
|
||||||
|
const p = defaultProvider || 'claude';
|
||||||
setName('');
|
setName('');
|
||||||
setDescription('');
|
setDescription('');
|
||||||
setEnabled(true);
|
setEnabled(true);
|
||||||
|
setCliProvider(p);
|
||||||
setMode('direct');
|
setMode('direct');
|
||||||
setProviderId('');
|
setProviderId('');
|
||||||
setModel('sonnet');
|
setModel(p === 'claude' ? 'sonnet' : '');
|
||||||
setIncludeCoAuthoredBy(false);
|
|
||||||
setSettingsFile('');
|
setSettingsFile('');
|
||||||
setAuthToken('');
|
setAuthToken('');
|
||||||
setBaseUrl('');
|
setBaseUrl('');
|
||||||
|
setCodexApiKey('');
|
||||||
|
setCodexBaseUrl('');
|
||||||
|
setCodexProfile('');
|
||||||
|
setShowCodexKey(false);
|
||||||
|
setAuthJson('');
|
||||||
|
setConfigToml('');
|
||||||
|
setWriteCommonConfig(false);
|
||||||
|
setGeminiApiKey('');
|
||||||
|
setShowGeminiKey(false);
|
||||||
setAvailableModels([]);
|
setAvailableModels([]);
|
||||||
setModelInput('');
|
setModelInput('');
|
||||||
setTags([]);
|
setTags([]);
|
||||||
@@ -152,7 +179,7 @@ export function CliSettingsModal({ open, onClose, cliSettings }: CliSettingsModa
|
|||||||
setShowJsonInput(false);
|
setShowJsonInput(false);
|
||||||
setErrors({});
|
setErrors({});
|
||||||
}
|
}
|
||||||
}, [cliSettings, open, providers]);
|
}, [cliSettings, open, providers, defaultProvider]);
|
||||||
|
|
||||||
// Validate form
|
// Validate form
|
||||||
const validateForm = (): boolean => {
|
const validateForm = (): boolean => {
|
||||||
@@ -161,33 +188,35 @@ export function CliSettingsModal({ open, onClose, cliSettings }: CliSettingsModa
|
|||||||
if (!name.trim()) {
|
if (!name.trim()) {
|
||||||
newErrors.name = formatMessage({ id: 'apiSettings.validation.nameRequired' });
|
newErrors.name = formatMessage({ id: 'apiSettings.validation.nameRequired' });
|
||||||
} else {
|
} else {
|
||||||
// Validate name format: must start with letter, followed by letters/numbers/hyphens/underscores
|
|
||||||
const namePattern = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
|
const namePattern = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
|
||||||
if (!namePattern.test(name.trim())) {
|
if (!namePattern.test(name.trim())) {
|
||||||
newErrors.name = formatMessage({ id: 'apiSettings.cliSettings.nameFormatHint' });
|
newErrors.name = formatMessage({ id: 'apiSettings.cliSettings.nameFormatHint' });
|
||||||
}
|
}
|
||||||
// Validate name length
|
|
||||||
if (name.trim().length > 32) {
|
if (name.trim().length > 32) {
|
||||||
newErrors.name = formatMessage({ id: 'apiSettings.cliSettings.nameTooLong' }, { max: 32 });
|
newErrors.name = formatMessage({ id: 'apiSettings.cliSettings.nameTooLong' }, { max: 32 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode === 'provider-based') {
|
if (cliProvider === 'claude') {
|
||||||
if (!providerId) {
|
if (mode === 'provider-based' && !providerId) {
|
||||||
newErrors.providerId = formatMessage({ id: 'apiSettings.cliSettings.validation.providerRequired' });
|
newErrors.providerId = formatMessage({ id: 'apiSettings.cliSettings.validation.providerRequired' });
|
||||||
}
|
}
|
||||||
} else {
|
if (mode === 'direct' && !authToken.trim() && !baseUrl.trim()) {
|
||||||
// Direct mode
|
|
||||||
if (!authToken.trim() && !baseUrl.trim()) {
|
|
||||||
newErrors.direct = formatMessage({ id: 'apiSettings.cliSettings.validation.authOrBaseUrlRequired' });
|
newErrors.direct = formatMessage({ id: 'apiSettings.cliSettings.validation.authOrBaseUrlRequired' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate JSON config if shown
|
if (authJson.trim()) {
|
||||||
|
const parsed = parseConfigText(authJson, 'json');
|
||||||
|
if (!parsed.ok) {
|
||||||
|
newErrors.authJson = formatMessage({ id: `apiSettings.cliSettings.${parsed.errorKey}` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (showJsonInput) {
|
if (showJsonInput) {
|
||||||
const parsedConfig = parseConfigJson(configJson);
|
const parsed = parseConfigText(configJson, 'json');
|
||||||
if (!parsedConfig.ok) {
|
if (!parsed.ok) {
|
||||||
newErrors.configJson = formatMessage({ id: `apiSettings.cliSettings.${parsedConfig.errorKey}` });
|
newErrors.configJson = formatMessage({ id: `apiSettings.cliSettings.${parsed.errorKey}` });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,38 +229,59 @@ export function CliSettingsModal({ open, onClose, cliSettings }: CliSettingsModa
|
|||||||
if (!validateForm()) return;
|
if (!validateForm()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Build settings object based on mode
|
let settings: any;
|
||||||
const env: Record<string, string> = {
|
|
||||||
DISABLE_AUTOUPDATER: '1',
|
|
||||||
};
|
|
||||||
|
|
||||||
|
if (cliProvider === 'claude') {
|
||||||
|
const env: Record<string, string> = { DISABLE_AUTOUPDATER: '1' };
|
||||||
if (mode === 'provider-based') {
|
if (mode === 'provider-based') {
|
||||||
// Provider-based mode: get settings from selected provider
|
|
||||||
const provider = providers.find((p) => p.id === providerId);
|
const provider = providers.find((p) => p.id === providerId);
|
||||||
if (provider) {
|
if (provider?.apiBase) env.ANTHROPIC_BASE_URL = provider.apiBase;
|
||||||
if (provider.apiBase) {
|
if (provider?.apiKey) env.ANTHROPIC_AUTH_TOKEN = provider.apiKey;
|
||||||
env.ANTHROPIC_BASE_URL = provider.apiBase;
|
|
||||||
}
|
|
||||||
if (provider.apiKey) {
|
|
||||||
env.ANTHROPIC_AUTH_TOKEN = provider.apiKey;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Direct mode: use manual input
|
if (authToken.trim()) env.ANTHROPIC_AUTH_TOKEN = authToken.trim();
|
||||||
if (authToken.trim()) {
|
if (baseUrl.trim()) env.ANTHROPIC_BASE_URL = baseUrl.trim();
|
||||||
env.ANTHROPIC_AUTH_TOKEN = authToken.trim();
|
|
||||||
}
|
|
||||||
if (baseUrl.trim()) {
|
|
||||||
env.ANTHROPIC_BASE_URL = baseUrl.trim();
|
|
||||||
}
|
}
|
||||||
|
settings = {
|
||||||
|
env,
|
||||||
|
model: model || 'sonnet',
|
||||||
|
settingsFile: settingsFile.trim() || undefined,
|
||||||
|
availableModels,
|
||||||
|
tags,
|
||||||
|
};
|
||||||
|
} else if (cliProvider === 'codex') {
|
||||||
|
const env: Record<string, string> = {};
|
||||||
|
if (codexApiKey.trim()) env.OPENAI_API_KEY = codexApiKey.trim();
|
||||||
|
if (codexBaseUrl.trim()) env.OPENAI_BASE_URL = codexBaseUrl.trim();
|
||||||
|
settings = {
|
||||||
|
env,
|
||||||
|
model: model || undefined,
|
||||||
|
profile: codexProfile.trim() || undefined,
|
||||||
|
authJson: authJson.trim() || undefined,
|
||||||
|
configToml: configToml.trim() || undefined,
|
||||||
|
availableModels,
|
||||||
|
tags,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// gemini
|
||||||
|
const env: Record<string, string> = {};
|
||||||
|
if (geminiApiKey.trim()) env.GEMINI_API_KEY = geminiApiKey.trim();
|
||||||
|
settings = {
|
||||||
|
env,
|
||||||
|
model: model || undefined,
|
||||||
|
availableModels,
|
||||||
|
tags,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse and merge JSON config if shown
|
// Merge JSON config if shown
|
||||||
let extraSettings: Record<string, unknown> = {};
|
if (showJsonInput && configJson.trim()) {
|
||||||
if (showJsonInput) {
|
try {
|
||||||
const parsedConfig = parseConfigJson(configJson);
|
const extra = JSON.parse(configJson);
|
||||||
if (parsedConfig.ok) {
|
if (extra && typeof extra === 'object' && !Array.isArray(extra)) {
|
||||||
extraSettings = parsedConfig.value;
|
Object.assign(settings, extra);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// skip invalid json
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,16 +289,9 @@ export function CliSettingsModal({ open, onClose, cliSettings }: CliSettingsModa
|
|||||||
id: cliSettings?.id,
|
id: cliSettings?.id,
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
description: description.trim() || undefined,
|
description: description.trim() || undefined,
|
||||||
|
provider: cliProvider,
|
||||||
enabled,
|
enabled,
|
||||||
settings: {
|
settings,
|
||||||
env,
|
|
||||||
model,
|
|
||||||
includeCoAuthoredBy,
|
|
||||||
settingsFile: settingsFile.trim() || undefined,
|
|
||||||
availableModels,
|
|
||||||
tags,
|
|
||||||
...extraSettings,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isEditing && cliSettings) {
|
if (isEditing && cliSettings) {
|
||||||
@@ -263,40 +306,34 @@ export function CliSettingsModal({ open, onClose, cliSettings }: CliSettingsModa
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle add model
|
// Tag/Model helpers
|
||||||
const handleAddModel = () => {
|
const handleAddModel = () => {
|
||||||
const newModel = modelInput.trim();
|
const v = modelInput.trim();
|
||||||
if (newModel && !availableModels.includes(newModel)) {
|
if (v && !availableModels.includes(v)) {
|
||||||
setAvailableModels([...availableModels, newModel]);
|
setAvailableModels([...availableModels, v]);
|
||||||
setModelInput('');
|
setModelInput('');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const handleRemoveModel = (m: string) => setAvailableModels(availableModels.filter((x) => x !== m));
|
||||||
// Handle remove model
|
|
||||||
const handleRemoveModel = (modelToRemove: string) => {
|
|
||||||
setAvailableModels(availableModels.filter((m) => m !== modelToRemove));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle add tag
|
|
||||||
const handleAddTag = () => {
|
const handleAddTag = () => {
|
||||||
const newTag = tagInput.trim();
|
const v = tagInput.trim();
|
||||||
if (newTag && !tags.includes(newTag)) {
|
if (v && !tags.includes(v)) {
|
||||||
setTags([...tags, newTag]);
|
setTags([...tags, v]);
|
||||||
setTagInput('');
|
setTagInput('');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const handleRemoveTag = (t: string) => setTags(tags.filter((x) => x !== t));
|
||||||
|
|
||||||
// Handle remove tag
|
|
||||||
const handleRemoveTag = (tagToRemove: string) => {
|
|
||||||
setTags(tags.filter((t) => t !== tagToRemove));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Predefined tags
|
|
||||||
const predefinedTags = ['分析', 'Debug', 'implementation', 'refactoring', 'testing'];
|
const predefinedTags = ['分析', 'Debug', 'implementation', 'refactoring', 'testing'];
|
||||||
|
|
||||||
// Get selected provider info
|
|
||||||
const selectedProvider = providers.find((p) => p.id === providerId);
|
const selectedProvider = providers.find((p) => p.id === providerId);
|
||||||
|
|
||||||
|
// Title by provider
|
||||||
|
const providerLabel: Record<CliProvider, string> = {
|
||||||
|
claude: 'Claude',
|
||||||
|
codex: 'Codex',
|
||||||
|
gemini: 'Gemini',
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onClose}>
|
<Dialog open={open} onOpenChange={onClose}>
|
||||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||||
@@ -305,6 +342,7 @@ export function CliSettingsModal({ open, onClose, cliSettings }: CliSettingsModa
|
|||||||
{isEditing
|
{isEditing
|
||||||
? formatMessage({ id: 'apiSettings.cliSettings.actions.edit' })
|
? formatMessage({ id: 'apiSettings.cliSettings.actions.edit' })
|
||||||
: formatMessage({ id: 'apiSettings.cliSettings.actions.add' })}
|
: formatMessage({ id: 'apiSettings.cliSettings.actions.add' })}
|
||||||
|
{' - '}{providerLabel[cliProvider]}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{formatMessage({ id: 'apiSettings.cliSettings.modalDescription' })}
|
{formatMessage({ id: 'apiSettings.cliSettings.modalDescription' })}
|
||||||
@@ -314,6 +352,23 @@ export function CliSettingsModal({ open, onClose, cliSettings }: CliSettingsModa
|
|||||||
<div className="space-y-6 py-4">
|
<div className="space-y-6 py-4">
|
||||||
{/* Common Fields */}
|
{/* Common Fields */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
{/* Provider Selector (only when creating new) */}
|
||||||
|
{!isEditing && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>CLI Provider</Label>
|
||||||
|
<Select value={cliProvider} onValueChange={(v) => setCliProvider(v as CliProvider)}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="claude">Claude</SelectItem>
|
||||||
|
<SelectItem value="codex">Codex</SelectItem>
|
||||||
|
<SelectItem value="gemini">Gemini</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="name">
|
<Label htmlFor="name">
|
||||||
{formatMessage({ id: 'apiSettings.common.name' })}
|
{formatMessage({ id: 'apiSettings.common.name' })}
|
||||||
@@ -347,18 +402,16 @@ export function CliSettingsModal({ open, onClose, cliSettings }: CliSettingsModa
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Switch
|
<Switch id="enabled" checked={enabled} onCheckedChange={setEnabled} />
|
||||||
id="enabled"
|
|
||||||
checked={enabled}
|
|
||||||
onCheckedChange={setEnabled}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="enabled" className="cursor-pointer">
|
<Label htmlFor="enabled" className="cursor-pointer">
|
||||||
{formatMessage({ id: 'apiSettings.common.enableThis' })}
|
{formatMessage({ id: 'apiSettings.common.enableThis' })}
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mode Tabs */}
|
{/* ========== Claude Settings ========== */}
|
||||||
|
{cliProvider === 'claude' && (
|
||||||
|
<>
|
||||||
<Tabs value={mode} onValueChange={(v) => setMode(v as ModeType)} className="w-full">
|
<Tabs value={mode} onValueChange={(v) => setMode(v as ModeType)} className="w-full">
|
||||||
<TabsList className="grid w-full grid-cols-2">
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
<TabsTrigger value="provider-based">
|
<TabsTrigger value="provider-based">
|
||||||
@@ -369,7 +422,6 @@ export function CliSettingsModal({ open, onClose, cliSettings }: CliSettingsModa
|
|||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
{/* Provider-based Mode */}
|
|
||||||
<TabsContent value="provider-based" className="space-y-4 mt-4">
|
<TabsContent value="provider-based" className="space-y-4 mt-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="providerId">
|
<Label htmlFor="providerId">
|
||||||
@@ -381,10 +433,8 @@ export function CliSettingsModal({ open, onClose, cliSettings }: CliSettingsModa
|
|||||||
<SelectValue placeholder={formatMessage({ id: 'apiSettings.cliSettings.selectProvider' })} />
|
<SelectValue placeholder={formatMessage({ id: 'apiSettings.cliSettings.selectProvider' })} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{providers.map((provider) => (
|
{providers.map((p) => (
|
||||||
<SelectItem key={provider.id} value={provider.id}>
|
<SelectItem key={p.id} value={p.id}>{p.name}</SelectItem>
|
||||||
{provider.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
@@ -406,28 +456,14 @@ export function CliSettingsModal({ open, onClose, cliSettings }: CliSettingsModa
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="model-pb">
|
<Label htmlFor="model-pb">{formatMessage({ id: 'apiSettings.cliSettings.model' })}</Label>
|
||||||
{formatMessage({ id: 'apiSettings.cliSettings.model' })}
|
<Input id="model-pb" value={model} onChange={(e) => setModel(e.target.value)} placeholder="sonnet" />
|
||||||
</Label>
|
|
||||||
<Select value={model} onValueChange={setModel}>
|
|
||||||
<SelectTrigger id="model-pb">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="opus">Opus</SelectItem>
|
|
||||||
<SelectItem value="sonnet">Sonnet</SelectItem>
|
|
||||||
<SelectItem value="haiku">Haiku</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
{/* Direct Mode */}
|
|
||||||
<TabsContent value="direct" className="space-y-4 mt-4">
|
<TabsContent value="direct" className="space-y-4 mt-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="authToken">
|
<Label htmlFor="authToken">{formatMessage({ id: 'apiSettings.cliSettings.authToken' })}</Label>
|
||||||
{formatMessage({ id: 'apiSettings.cliSettings.authToken' })}
|
|
||||||
</Label>
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
id="authToken"
|
id="authToken"
|
||||||
@@ -438,9 +474,7 @@ export function CliSettingsModal({ open, onClose, cliSettings }: CliSettingsModa
|
|||||||
className={errors.direct ? 'border-destructive pr-10' : 'pr-10'}
|
className={errors.direct ? 'border-destructive pr-10' : 'pr-10'}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button" variant="ghost" size="icon"
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="absolute right-0 top-0 h-full px-2"
|
className="absolute right-0 top-0 h-full px-2"
|
||||||
onClick={() => setShowToken(!showToken)}
|
onClick={() => setShowToken(!showToken)}
|
||||||
>
|
>
|
||||||
@@ -450,9 +484,7 @@ export function CliSettingsModal({ open, onClose, cliSettings }: CliSettingsModa
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="baseUrl">
|
<Label htmlFor="baseUrl">{formatMessage({ id: 'apiSettings.cliSettings.baseUrl' })}</Label>
|
||||||
{formatMessage({ id: 'apiSettings.cliSettings.baseUrl' })}
|
|
||||||
</Label>
|
|
||||||
<Input
|
<Input
|
||||||
id="baseUrl"
|
id="baseUrl"
|
||||||
value={baseUrl}
|
value={baseUrl}
|
||||||
@@ -461,44 +493,18 @@ export function CliSettingsModal({ open, onClose, cliSettings }: CliSettingsModa
|
|||||||
className={errors.direct ? 'border-destructive' : ''}
|
className={errors.direct ? 'border-destructive' : ''}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{errors.direct && <p className="text-sm text-destructive">{errors.direct}</p>}
|
{errors.direct && <p className="text-sm text-destructive">{errors.direct}</p>}
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="model-direct">
|
<Label htmlFor="model-direct">{formatMessage({ id: 'apiSettings.cliSettings.model' })}</Label>
|
||||||
{formatMessage({ id: 'apiSettings.cliSettings.model' })}
|
<Input id="model-direct" value={model} onChange={(e) => setModel(e.target.value)} placeholder="sonnet" />
|
||||||
</Label>
|
|
||||||
<Select value={model} onValueChange={setModel}>
|
|
||||||
<SelectTrigger id="model-direct">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="opus">Opus</SelectItem>
|
|
||||||
<SelectItem value="sonnet">Sonnet</SelectItem>
|
|
||||||
<SelectItem value="haiku">Haiku</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
{/* Additional Settings (both modes) */}
|
{/* Claude: Settings File */}
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Switch
|
|
||||||
id="coAuthored"
|
|
||||||
checked={includeCoAuthoredBy}
|
|
||||||
onCheckedChange={setIncludeCoAuthoredBy}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="coAuthored" className="cursor-pointer">
|
|
||||||
{formatMessage({ id: 'apiSettings.cliSettings.includeCoAuthoredBy' })}
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="settingsFile">
|
<Label htmlFor="settingsFile">{formatMessage({ id: 'apiSettings.cliSettings.settingsFile' })}</Label>
|
||||||
{formatMessage({ id: 'apiSettings.cliSettings.settingsFile' })}
|
|
||||||
</Label>
|
|
||||||
<Input
|
<Input
|
||||||
id="settingsFile"
|
id="settingsFile"
|
||||||
value={settingsFile}
|
value={settingsFile}
|
||||||
@@ -509,26 +515,179 @@ export function CliSettingsModal({ open, onClose, cliSettings }: CliSettingsModa
|
|||||||
{formatMessage({ id: 'apiSettings.cliSettings.settingsFileHint' })}
|
{formatMessage({ id: 'apiSettings.cliSettings.settingsFileHint' })}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Available Models Section */}
|
{/* ========== Codex Settings ========== */}
|
||||||
|
{cliProvider === 'codex' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* API Key */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="availableModels">
|
<Label htmlFor="codex-apikey">API Key</Label>
|
||||||
{formatMessage({ id: 'apiSettings.cliSettings.availableModels' })}
|
<p className="text-xs text-muted-foreground">
|
||||||
</Label>
|
只需要填这里,下方 auth.json 会自动填充
|
||||||
|
</p>
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
id="codex-apikey"
|
||||||
|
type={showCodexKey ? 'text' : 'password'}
|
||||||
|
value={codexApiKey}
|
||||||
|
onChange={(e) => setCodexApiKey(e.target.value)}
|
||||||
|
placeholder="sk-..."
|
||||||
|
className="pr-10"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button" variant="ghost" size="icon"
|
||||||
|
className="absolute right-0 top-0 h-full px-2"
|
||||||
|
onClick={() => setShowCodexKey(!showCodexKey)}
|
||||||
|
>
|
||||||
|
{showCodexKey ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* API Endpoint URL */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="codex-baseurl">API 请求地址</Label>
|
||||||
|
<Input
|
||||||
|
id="codex-baseurl"
|
||||||
|
value={codexBaseUrl}
|
||||||
|
onChange={(e) => setCodexBaseUrl(e.target.value)}
|
||||||
|
placeholder="https://your-api-endpoint.com/v1"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
填写兼容 OpenAI Response 格式的服务端点地址
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Model Name */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="codex-model">{formatMessage({ id: 'apiSettings.cliSettings.model' })}</Label>
|
||||||
|
<Input
|
||||||
|
id="codex-model"
|
||||||
|
value={model}
|
||||||
|
onChange={(e) => setModel(e.target.value)}
|
||||||
|
placeholder="gpt-5.2"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
指定使用的模型,将自动更新到 config.toml 中
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Profile */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="codex-profile">Profile</Label>
|
||||||
|
<Input
|
||||||
|
id="codex-profile"
|
||||||
|
value={codexProfile}
|
||||||
|
onChange={(e) => setCodexProfile(e.target.value)}
|
||||||
|
placeholder="default"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Codex profile 名称,通过 --profile 参数传递
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* auth.json Editor */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="codex-authjson">auth.json (JSON) *</Label>
|
||||||
|
<Textarea
|
||||||
|
id="codex-authjson"
|
||||||
|
value={authJson}
|
||||||
|
onChange={(e) => {
|
||||||
|
setAuthJson(e.target.value);
|
||||||
|
if (errors.authJson) {
|
||||||
|
setErrors((prev) => { const n = { ...prev }; delete n.authJson; return n; });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder='{"OPENAI_API_KEY": "..."}'
|
||||||
|
className={`font-mono text-sm ${errors.authJson ? 'border-destructive' : ''}`}
|
||||||
|
rows={8}
|
||||||
|
/>
|
||||||
|
{errors.authJson && <p className="text-xs text-destructive">{errors.authJson}</p>}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-xs text-muted-foreground">Codex auth.json 配置内容</p>
|
||||||
|
<Button
|
||||||
|
type="button" variant="ghost" size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
try {
|
||||||
|
const formatted = JSON.stringify(JSON.parse(authJson), null, 2);
|
||||||
|
setAuthJson(formatted);
|
||||||
|
} catch { /* skip */ }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
格式化
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* config.toml Editor */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label htmlFor="codex-configtoml">config.toml (TOML)</Label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Switch id="writeCommon" checked={writeCommonConfig} onCheckedChange={setWriteCommonConfig} />
|
||||||
|
<Label htmlFor="writeCommon" className="text-xs cursor-pointer">写入通用配置</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Textarea
|
||||||
|
id="codex-configtoml"
|
||||||
|
value={configToml}
|
||||||
|
onChange={(e) => setConfigToml(e.target.value)}
|
||||||
|
placeholder={'model = "gpt-5.2"\nmodel_reasoning_effort = "xhigh"'}
|
||||||
|
className="font-mono text-sm"
|
||||||
|
rows={6}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ========== Gemini Settings ========== */}
|
||||||
|
{cliProvider === 'gemini' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="gemini-apikey">API Key</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
id="gemini-apikey"
|
||||||
|
type={showGeminiKey ? 'text' : 'password'}
|
||||||
|
value={geminiApiKey}
|
||||||
|
onChange={(e) => setGeminiApiKey(e.target.value)}
|
||||||
|
placeholder="AIza..."
|
||||||
|
className="pr-10"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button" variant="ghost" size="icon"
|
||||||
|
className="absolute right-0 top-0 h-full px-2"
|
||||||
|
onClick={() => setShowGeminiKey(!showGeminiKey)}
|
||||||
|
>
|
||||||
|
{showGeminiKey ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="gemini-model">{formatMessage({ id: 'apiSettings.cliSettings.model' })}</Label>
|
||||||
|
<Input
|
||||||
|
id="gemini-model"
|
||||||
|
value={model}
|
||||||
|
onChange={(e) => setModel(e.target.value)}
|
||||||
|
placeholder="gemini-2.5-flash"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ========== Shared Settings (all providers) ========== */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Available Models */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="availableModels">{formatMessage({ id: 'apiSettings.cliSettings.availableModels' })}</Label>
|
||||||
<div className="flex flex-wrap gap-2 p-3 bg-muted/30 rounded-lg min-h-[60px]">
|
<div className="flex flex-wrap gap-2 p-3 bg-muted/30 rounded-lg min-h-[60px]">
|
||||||
{availableModels.map((model) => (
|
{availableModels.map((m) => (
|
||||||
<span
|
<span key={m} className="inline-flex items-center gap-1 px-2 py-1 bg-primary/10 text-primary rounded-md text-sm">
|
||||||
key={model}
|
{m}
|
||||||
className="inline-flex items-center gap-1 px-2 py-1 bg-primary/10 text-primary rounded-md text-sm"
|
<button type="button" onClick={() => handleRemoveModel(m)} className="hover:text-destructive transition-colors">×</button>
|
||||||
>
|
|
||||||
{model}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleRemoveModel(model)}
|
|
||||||
className="hover:text-destructive transition-colors"
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
<div className="flex gap-2 flex-1">
|
<div className="flex gap-2 flex-1">
|
||||||
@@ -536,21 +695,11 @@ export function CliSettingsModal({ open, onClose, cliSettings }: CliSettingsModa
|
|||||||
id="availableModels"
|
id="availableModels"
|
||||||
value={modelInput}
|
value={modelInput}
|
||||||
onChange={(e) => setModelInput(e.target.value)}
|
onChange={(e) => setModelInput(e.target.value)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); handleAddModel(); } }}
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
handleAddModel();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder={formatMessage({ id: 'apiSettings.cliSettings.availableModelsPlaceholder' })}
|
placeholder={formatMessage({ id: 'apiSettings.cliSettings.availableModelsPlaceholder' })}
|
||||||
className="flex-1 min-w-[120px]"
|
className="flex-1 min-w-[120px]"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button type="button" size="sm" onClick={handleAddModel} variant="outline">
|
||||||
type="button"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleAddModel}
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
<Check className="w-4 h-4" />
|
<Check className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -560,20 +709,15 @@ export function CliSettingsModal({ open, onClose, cliSettings }: CliSettingsModa
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tags Section */}
|
{/* Tags */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="tags">
|
<Label htmlFor="tags">{formatMessage({ id: 'apiSettings.cliSettings.tags' })}</Label>
|
||||||
{formatMessage({ id: 'apiSettings.cliSettings.tags' })}
|
|
||||||
</Label>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{formatMessage({ id: 'apiSettings.cliSettings.tagsDescription' })}
|
{formatMessage({ id: 'apiSettings.cliSettings.tagsDescription' })}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-wrap gap-2 p-3 bg-muted/30 rounded-lg min-h-[60px]">
|
<div className="flex flex-wrap gap-2 p-3 bg-muted/30 rounded-lg min-h-[60px]">
|
||||||
{tags.map((tag) => (
|
{tags.map((tag) => (
|
||||||
<span
|
<span key={tag} className="inline-flex items-center gap-1 px-2 py-1 bg-primary/10 text-primary rounded-md text-sm">
|
||||||
key={tag}
|
|
||||||
className="inline-flex items-center gap-1 px-2 py-1 bg-primary/10 text-primary rounded-md text-sm"
|
|
||||||
>
|
|
||||||
{tag}
|
{tag}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -590,58 +734,41 @@ export function CliSettingsModal({ open, onClose, cliSettings }: CliSettingsModa
|
|||||||
id="tags"
|
id="tags"
|
||||||
value={tagInput}
|
value={tagInput}
|
||||||
onChange={(e) => setTagInput(e.target.value)}
|
onChange={(e) => setTagInput(e.target.value)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); handleAddTag(); } }}
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
handleAddTag();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder={formatMessage({ id: 'apiSettings.cliSettings.tagInputPlaceholder' })}
|
placeholder={formatMessage({ id: 'apiSettings.cliSettings.tagInputPlaceholder' })}
|
||||||
className="flex-1 min-w-[120px]"
|
className="flex-1 min-w-[120px]"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button type="button" size="sm" onClick={handleAddTag} variant="outline">
|
||||||
type="button"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleAddTag}
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
<Plus className="w-4 h-4" />
|
<Plus className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Predefined Tags */}
|
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{formatMessage({ id: 'apiSettings.cliSettings.predefinedTags' })}:
|
{formatMessage({ id: 'apiSettings.cliSettings.predefinedTags' })}:
|
||||||
</span>
|
</span>
|
||||||
{predefinedTags.map((predefinedTag) => (
|
{predefinedTags.map((pt) => (
|
||||||
<button
|
<button
|
||||||
key={predefinedTag}
|
key={pt}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => { if (!tags.includes(pt)) setTags([...tags, pt]); }}
|
||||||
if (!tags.includes(predefinedTag)) {
|
disabled={tags.includes(pt)}
|
||||||
setTags([...tags, predefinedTag]);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={tags.includes(predefinedTag)}
|
|
||||||
className="text-xs px-2 py-0.5 rounded border border-border hover:bg-muted disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
className="text-xs px-2 py-0.5 rounded border border-border hover:bg-muted disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
>
|
>
|
||||||
{predefinedTag}
|
{pt}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* JSON Config Section */}
|
{/* JSON Config (all providers) */}
|
||||||
<div className="space-y-2 pt-4 border-t border-border">
|
<div className="space-y-2 pt-4 border-t border-border">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Label htmlFor="configJson" className="cursor-pointer">
|
<Label htmlFor="configJson" className="cursor-pointer">
|
||||||
{formatMessage({ id: 'apiSettings.cliSettings.configJson' })}
|
{formatMessage({ id: 'apiSettings.cliSettings.configJson' })}
|
||||||
</Label>
|
</Label>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button" variant="ghost" size="sm"
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setShowJsonInput(!showJsonInput)}
|
onClick={() => setShowJsonInput(!showJsonInput)}
|
||||||
>
|
>
|
||||||
{showJsonInput
|
{showJsonInput
|
||||||
@@ -657,20 +784,14 @@ export function CliSettingsModal({ open, onClose, cliSettings }: CliSettingsModa
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setConfigJson(e.target.value);
|
setConfigJson(e.target.value);
|
||||||
if (errors.configJson) {
|
if (errors.configJson) {
|
||||||
setErrors((prev) => {
|
setErrors((prev) => { const n = { ...prev }; delete n.configJson; return n; });
|
||||||
const newErrors = { ...prev };
|
|
||||||
delete newErrors.configJson;
|
|
||||||
return newErrors;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder={formatMessage({ id: 'apiSettings.cliSettings.configJsonPlaceholder' })}
|
placeholder={formatMessage({ id: 'apiSettings.cliSettings.configJsonPlaceholder' })}
|
||||||
className={errors.configJson ? 'font-mono border-destructive' : 'font-mono'}
|
className={errors.configJson ? 'font-mono border-destructive' : 'font-mono'}
|
||||||
rows={8}
|
rows={8}
|
||||||
/>
|
/>
|
||||||
{errors.configJson && (
|
{errors.configJson && <p className="text-xs text-destructive">{errors.configJson}</p>}
|
||||||
<p className="text-xs text-destructive">{errors.configJson}</p>
|
|
||||||
)}
|
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{formatMessage({ id: 'apiSettings.cliSettings.configJsonHint' })}
|
{formatMessage({ id: 'apiSettings.cliSettings.configJsonHint' })}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -26,8 +26,10 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/Select';
|
} from '@/components/ui/Select';
|
||||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/RadioGroup';
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/RadioGroup';
|
||||||
|
import { useConfigStore, selectCliTools } from '@/stores/configStore';
|
||||||
|
import { useCliSettings } from '@/hooks/useApiSettings';
|
||||||
|
|
||||||
export type CliTool = 'claude' | 'gemini' | 'qwen' | 'codex' | 'opencode';
|
export type CliTool = string;
|
||||||
export type LaunchMode = 'default' | 'yolo';
|
export type LaunchMode = 'default' | 'yolo';
|
||||||
export type ShellKind = 'bash' | 'pwsh' | 'cmd';
|
export type ShellKind = 'bash' | 'pwsh' | 'cmd';
|
||||||
|
|
||||||
@@ -39,6 +41,8 @@ export interface CliSessionConfig {
|
|||||||
workingDir: string;
|
workingDir: string;
|
||||||
/** Session tag for grouping (auto-generated if not provided) */
|
/** Session tag for grouping (auto-generated if not provided) */
|
||||||
tag: string;
|
tag: string;
|
||||||
|
/** CLI Settings endpoint ID for custom API configuration */
|
||||||
|
settingsEndpointId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CliConfigModalProps {
|
export interface CliConfigModalProps {
|
||||||
@@ -48,23 +52,13 @@ export interface CliConfigModalProps {
|
|||||||
onCreateSession: (config: CliSessionConfig) => Promise<void>;
|
onCreateSession: (config: CliSessionConfig) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CLI_TOOLS: CliTool[] = ['claude', 'gemini', 'qwen', 'codex', 'opencode'];
|
|
||||||
|
|
||||||
const MODEL_OPTIONS: Record<CliTool, string[]> = {
|
|
||||||
claude: ['sonnet', 'haiku'],
|
|
||||||
gemini: ['gemini-2.5-pro', 'gemini-2.5-flash'],
|
|
||||||
qwen: ['coder-model'],
|
|
||||||
codex: ['gpt-5.2'],
|
|
||||||
opencode: ['opencode/glm-4.7-free'],
|
|
||||||
};
|
|
||||||
|
|
||||||
const AUTO_MODEL_VALUE = '__auto__';
|
const AUTO_MODEL_VALUE = '__auto__';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a tag name: {tool}-{HHmmss}
|
* Generate a tag name: {tool}-{HHmmss}
|
||||||
* Example: gemini-143052
|
* Example: gemini-143052
|
||||||
*/
|
*/
|
||||||
function generateTag(tool: CliTool): string {
|
function generateTag(tool: string): string {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const time = now.toTimeString().slice(0, 8).replace(/:/g, '');
|
const time = now.toTimeString().slice(0, 8).replace(/:/g, '');
|
||||||
return `${tool}-${time}`;
|
return `${tool}-${time}`;
|
||||||
@@ -78,8 +72,18 @@ export function CliConfigModal({
|
|||||||
}: CliConfigModalProps) {
|
}: CliConfigModalProps) {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
|
// Dynamic tool data from configStore
|
||||||
|
const cliTools = useConfigStore(selectCliTools);
|
||||||
|
const enabledTools = React.useMemo(
|
||||||
|
() =>
|
||||||
|
Object.entries(cliTools)
|
||||||
|
.filter(([, config]) => config.enabled)
|
||||||
|
.map(([key]) => key),
|
||||||
|
[cliTools]
|
||||||
|
);
|
||||||
|
|
||||||
const [tool, setTool] = React.useState<CliTool>('gemini');
|
const [tool, setTool] = React.useState<CliTool>('gemini');
|
||||||
const [model, setModel] = React.useState<string | undefined>(MODEL_OPTIONS.gemini[0]);
|
const [model, setModel] = React.useState<string | undefined>(undefined);
|
||||||
const [launchMode, setLaunchMode] = React.useState<LaunchMode>('yolo');
|
const [launchMode, setLaunchMode] = React.useState<LaunchMode>('yolo');
|
||||||
// Default to 'cmd' on Windows for better compatibility with npm CLI tools (.cmd files)
|
// Default to 'cmd' on Windows for better compatibility with npm CLI tools (.cmd files)
|
||||||
const [preferredShell, setPreferredShell] = React.useState<ShellKind>(
|
const [preferredShell, setPreferredShell] = React.useState<ShellKind>(
|
||||||
@@ -91,7 +95,46 @@ export function CliConfigModal({
|
|||||||
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
|
|
||||||
const modelOptions = React.useMemo(() => MODEL_OPTIONS[tool] ?? [], [tool]);
|
// CLI Settings integration (for all tools)
|
||||||
|
const { cliSettings } = useCliSettings({ enabled: true });
|
||||||
|
|
||||||
|
// Map tool names to provider types for filtering
|
||||||
|
const toolProviderMap: Record<string, string> = {
|
||||||
|
claude: 'claude',
|
||||||
|
codex: 'codex',
|
||||||
|
gemini: 'gemini',
|
||||||
|
};
|
||||||
|
const currentProvider = toolProviderMap[tool] || tool;
|
||||||
|
|
||||||
|
const enabledCliSettings = React.useMemo(
|
||||||
|
() => (cliSettings || []).filter((s) => s.enabled && (s.provider || 'claude') === currentProvider),
|
||||||
|
[cliSettings, currentProvider]
|
||||||
|
);
|
||||||
|
const [settingsEndpointId, setSettingsEndpointId] = React.useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
// Reset settingsEndpointId when tool changes
|
||||||
|
React.useEffect(() => {
|
||||||
|
setSettingsEndpointId(undefined);
|
||||||
|
}, [tool]);
|
||||||
|
|
||||||
|
// Derive model options from configStore + CLI Settings profile override
|
||||||
|
const modelOptions = React.useMemo(() => {
|
||||||
|
// If a CLI Settings profile is selected and has availableModels, use those
|
||||||
|
if (settingsEndpointId) {
|
||||||
|
const endpoint = enabledCliSettings.find((s) => s.id === settingsEndpointId);
|
||||||
|
if (endpoint?.settings.availableModels?.length) {
|
||||||
|
return endpoint.settings.availableModels;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const toolConfig = cliTools[tool];
|
||||||
|
if (!toolConfig) return [];
|
||||||
|
if (toolConfig.availableModels?.length) return toolConfig.availableModels;
|
||||||
|
const models = [toolConfig.primaryModel];
|
||||||
|
if (toolConfig.secondaryModel && toolConfig.secondaryModel !== toolConfig.primaryModel) {
|
||||||
|
models.push(toolConfig.secondaryModel);
|
||||||
|
}
|
||||||
|
return models;
|
||||||
|
}, [cliTools, tool, settingsEndpointId, enabledCliSettings]);
|
||||||
|
|
||||||
// Generate new tag when modal opens or tool changes
|
// Generate new tag when modal opens or tool changes
|
||||||
const regenerateTag = React.useCallback(() => {
|
const regenerateTag = React.useCallback(() => {
|
||||||
@@ -104,6 +147,7 @@ export function CliConfigModal({
|
|||||||
const nextWorkingDir = defaultWorkingDir ?? '';
|
const nextWorkingDir = defaultWorkingDir ?? '';
|
||||||
setWorkingDir(nextWorkingDir);
|
setWorkingDir(nextWorkingDir);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
setSettingsEndpointId(undefined);
|
||||||
regenerateTag();
|
regenerateTag();
|
||||||
}, [isOpen, defaultWorkingDir, regenerateTag]);
|
}, [isOpen, defaultWorkingDir, regenerateTag]);
|
||||||
|
|
||||||
@@ -113,12 +157,23 @@ export function CliConfigModal({
|
|||||||
const suffix = tag.split('-').pop() || '';
|
const suffix = tag.split('-').pop() || '';
|
||||||
setTag(`${tool}-${suffix}`);
|
setTag(`${tool}-${suffix}`);
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- only re-run when tool changes, reading tag intentionally stale
|
||||||
}, [tool]);
|
}, [tool]);
|
||||||
|
|
||||||
|
// Sync initial model when tool/modelOptions change
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (modelOptions.length > 0 && (!model || !modelOptions.includes(model))) {
|
||||||
|
setModel(modelOptions[0]);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- only re-run when modelOptions changes
|
||||||
|
}, [modelOptions]);
|
||||||
|
|
||||||
const handleToolChange = (nextTool: string) => {
|
const handleToolChange = (nextTool: string) => {
|
||||||
const next = nextTool as CliTool;
|
setTool(nextTool as CliTool);
|
||||||
setTool(next);
|
const nextConfig = cliTools[nextTool];
|
||||||
const nextModels = MODEL_OPTIONS[next] ?? [];
|
const nextModels = nextConfig?.availableModels?.length
|
||||||
|
? nextConfig.availableModels
|
||||||
|
: [nextConfig?.primaryModel, nextConfig?.secondaryModel].filter(Boolean) as string[];
|
||||||
if (!model || !nextModels.includes(model)) {
|
if (!model || !nextModels.includes(model)) {
|
||||||
setModel(nextModels[0]);
|
setModel(nextModels[0]);
|
||||||
}
|
}
|
||||||
@@ -148,6 +203,7 @@ export function CliConfigModal({
|
|||||||
preferredShell,
|
preferredShell,
|
||||||
workingDir: dir,
|
workingDir: dir,
|
||||||
tag: finalTag,
|
tag: finalTag,
|
||||||
|
settingsEndpointId,
|
||||||
});
|
});
|
||||||
onClose();
|
onClose();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -209,7 +265,7 @@ export function CliConfigModal({
|
|||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{CLI_TOOLS.map((t) => (
|
{enabledTools.map((t) => (
|
||||||
<SelectItem key={t} value={t}>
|
<SelectItem key={t} value={t}>
|
||||||
{t}
|
{t}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@@ -245,6 +301,45 @@ export function CliConfigModal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Config Profile (all tools with settings) */}
|
||||||
|
{enabledCliSettings.length > 0 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="cli-config-profile">
|
||||||
|
{formatMessage({ id: 'terminalDashboard.cliConfig.configProfile', defaultMessage: 'Config Profile' })}
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={settingsEndpointId ?? '__default__'}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
const id = v === '__default__' ? undefined : v;
|
||||||
|
setSettingsEndpointId(id);
|
||||||
|
// If profile has availableModels, use those for model dropdown
|
||||||
|
if (id) {
|
||||||
|
const endpoint = enabledCliSettings.find((s) => s.id === id);
|
||||||
|
if (endpoint?.settings.availableModels?.length) {
|
||||||
|
setModel(endpoint.settings.availableModels[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="cli-config-profile">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="__default__">
|
||||||
|
{formatMessage({ id: 'terminalDashboard.cliConfig.defaultProfile', defaultMessage: 'Default' })}
|
||||||
|
</SelectItem>
|
||||||
|
{enabledCliSettings.map((s) => (
|
||||||
|
<SelectItem key={s.id} value={s.id}>{s.name}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{formatMessage({ id: 'terminalDashboard.cliConfig.configProfileHint', defaultMessage: 'Select a CLI Settings profile for custom API configuration.' })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Mode */}
|
{/* Mode */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>{formatMessage({ id: 'terminalDashboard.cliConfig.mode' })}</Label>
|
<Label>{formatMessage({ id: 'terminalDashboard.cliConfig.mode' })}</Label>
|
||||||
|
|||||||
@@ -699,8 +699,8 @@ export interface UseCliSettingsReturn {
|
|||||||
cliSettings: CliSettingsEndpoint[];
|
cliSettings: CliSettingsEndpoint[];
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
enabledCount: number;
|
enabledCount: number;
|
||||||
providerBasedCount: number;
|
/** Count per provider type */
|
||||||
directCount: number;
|
providerCounts: Record<string, number>;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isFetching: boolean;
|
isFetching: boolean;
|
||||||
error: Error | null;
|
error: Error | null;
|
||||||
@@ -723,13 +723,12 @@ export function useCliSettings(options: UseCliSettingsOptions = {}): UseCliSetti
|
|||||||
const cliSettings = query.data?.endpoints ?? [];
|
const cliSettings = query.data?.endpoints ?? [];
|
||||||
const enabledCliSettings = cliSettings.filter((s) => s.enabled);
|
const enabledCliSettings = cliSettings.filter((s) => s.enabled);
|
||||||
|
|
||||||
// Determine mode based on whether settings have providerId in description or env vars
|
// Count settings per provider type
|
||||||
const providerBasedCount = cliSettings.filter((s) => {
|
const providerCounts = cliSettings.reduce<Record<string, number>>((acc, s) => {
|
||||||
// Provider-based: has ANTHROPIC_BASE_URL set to provider's apiBase
|
const provider = s.provider || 'claude';
|
||||||
return s.settings.env.ANTHROPIC_BASE_URL && !s.settings.env.ANTHROPIC_BASE_URL.includes('api.anthropic.com');
|
acc[provider] = (acc[provider] || 0) + 1;
|
||||||
}).length;
|
return acc;
|
||||||
|
}, {});
|
||||||
const directCount = cliSettings.length - providerBasedCount;
|
|
||||||
|
|
||||||
const refetch = async () => {
|
const refetch = async () => {
|
||||||
await query.refetch();
|
await query.refetch();
|
||||||
@@ -743,8 +742,7 @@ export function useCliSettings(options: UseCliSettingsOptions = {}): UseCliSetti
|
|||||||
cliSettings,
|
cliSettings,
|
||||||
totalCount: cliSettings.length,
|
totalCount: cliSettings.length,
|
||||||
enabledCount: enabledCliSettings.length,
|
enabledCount: enabledCliSettings.length,
|
||||||
providerBasedCount,
|
providerCounts,
|
||||||
directCount,
|
|
||||||
isLoading: query.isLoading,
|
isLoading: query.isLoading,
|
||||||
isFetching: query.isFetching,
|
isFetching: query.isFetching,
|
||||||
error: query.error,
|
error: query.error,
|
||||||
|
|||||||
@@ -6020,23 +6020,50 @@ export async function uninstallCcwLitellm(): Promise<{ success: boolean; message
|
|||||||
* CLI Settings (Claude CLI endpoint configuration)
|
* CLI Settings (Claude CLI endpoint configuration)
|
||||||
* Maps to backend EndpointSettings from /api/cli/settings
|
* Maps to backend EndpointSettings from /api/cli/settings
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* CLI Provider type
|
||||||
|
*/
|
||||||
|
export type CliProvider = 'claude' | 'codex' | 'gemini';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base settings fields shared across all providers
|
||||||
|
*/
|
||||||
|
export interface CliSettingsBase {
|
||||||
|
env: Record<string, string | undefined>;
|
||||||
|
model?: string;
|
||||||
|
tags?: string[];
|
||||||
|
availableModels?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claude-specific settings
|
||||||
|
*/
|
||||||
|
export interface ClaudeCliSettingsApi extends CliSettingsBase {
|
||||||
|
settingsFile?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Codex-specific settings
|
||||||
|
*/
|
||||||
|
export interface CodexCliSettingsApi extends CliSettingsBase {
|
||||||
|
profile?: string;
|
||||||
|
authJson?: string;
|
||||||
|
configToml?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gemini-specific settings
|
||||||
|
*/
|
||||||
|
export interface GeminiCliSettingsApi extends CliSettingsBase {
|
||||||
|
}
|
||||||
|
|
||||||
export interface CliSettingsEndpoint {
|
export interface CliSettingsEndpoint {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
settings: {
|
/** CLI provider type (defaults to 'claude' for backward compat) */
|
||||||
env: {
|
provider: CliProvider;
|
||||||
ANTHROPIC_AUTH_TOKEN?: string;
|
settings: ClaudeCliSettingsApi | CodexCliSettingsApi | GeminiCliSettingsApi;
|
||||||
ANTHROPIC_BASE_URL?: string;
|
|
||||||
DISABLE_AUTOUPDATER?: string;
|
|
||||||
[key: string]: string | undefined;
|
|
||||||
};
|
|
||||||
model?: string;
|
|
||||||
includeCoAuthoredBy?: boolean;
|
|
||||||
settingsFile?: string;
|
|
||||||
availableModels?: string[];
|
|
||||||
tags?: string[];
|
|
||||||
};
|
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
@@ -6057,19 +6084,9 @@ export interface SaveCliSettingsRequest {
|
|||||||
id?: string;
|
id?: string;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
settings: {
|
/** CLI provider type */
|
||||||
env: {
|
provider?: CliProvider;
|
||||||
ANTHROPIC_AUTH_TOKEN?: string;
|
settings: ClaudeCliSettingsApi | CodexCliSettingsApi | GeminiCliSettingsApi;
|
||||||
ANTHROPIC_BASE_URL?: string;
|
|
||||||
DISABLE_AUTOUPDATER?: string;
|
|
||||||
[key: string]: string | undefined;
|
|
||||||
};
|
|
||||||
model?: string;
|
|
||||||
includeCoAuthoredBy?: boolean;
|
|
||||||
settingsFile?: string;
|
|
||||||
availableModels?: string[];
|
|
||||||
tags?: string[];
|
|
||||||
};
|
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6565,6 +6582,8 @@ export interface CreateCliSessionInput {
|
|||||||
resumeKey?: string;
|
resumeKey?: string;
|
||||||
/** Launch mode for native CLI sessions (default or yolo). */
|
/** Launch mode for native CLI sessions (default or yolo). */
|
||||||
launchMode?: 'default' | 'yolo';
|
launchMode?: 'default' | 'yolo';
|
||||||
|
/** Settings endpoint ID for injecting env vars and settings into CLI process. */
|
||||||
|
settingsEndpointId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function withPath(url: string, projectPath?: string): string {
|
function withPath(url: string, projectPath?: string): string {
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import { join } from 'path';
|
|||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import { getCCWHome, ensureStorageDir } from './storage-paths.js';
|
import { getCCWHome, ensureStorageDir } from './storage-paths.js';
|
||||||
import {
|
import {
|
||||||
ClaudeCliSettings,
|
CliSettings,
|
||||||
|
CliProvider,
|
||||||
EndpointSettings,
|
EndpointSettings,
|
||||||
SettingsListResponse,
|
SettingsListResponse,
|
||||||
SettingsOperationResult,
|
SettingsOperationResult,
|
||||||
@@ -19,8 +20,8 @@ import {
|
|||||||
createDefaultSettings
|
createDefaultSettings
|
||||||
} from '../types/cli-settings.js';
|
} from '../types/cli-settings.js';
|
||||||
import {
|
import {
|
||||||
addClaudeCustomEndpoint,
|
addCustomEndpoint,
|
||||||
removeClaudeCustomEndpoint
|
removeCustomEndpoint
|
||||||
} from '../tools/claude-cli-tools.js';
|
} from '../tools/claude-cli-tools.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -108,6 +109,7 @@ export function saveEndpointSettings(request: SaveEndpointRequest): SettingsOper
|
|||||||
id: endpointId,
|
id: endpointId,
|
||||||
name: request.name,
|
name: request.name,
|
||||||
description: request.description,
|
description: request.description,
|
||||||
|
provider: request.provider || 'claude',
|
||||||
enabled: request.enabled ?? true,
|
enabled: request.enabled ?? true,
|
||||||
createdAt: existing?.createdAt || now,
|
createdAt: existing?.createdAt || now,
|
||||||
updatedAt: now
|
updatedAt: now
|
||||||
@@ -129,13 +131,13 @@ export function saveEndpointSettings(request: SaveEndpointRequest): SettingsOper
|
|||||||
// Merge user-provided tags with cli-wrapper tag for proper type registration
|
// Merge user-provided tags with cli-wrapper tag for proper type registration
|
||||||
const userTags = request.settings.tags || [];
|
const userTags = request.settings.tags || [];
|
||||||
const tags = [...new Set([...userTags, 'cli-wrapper'])]; // Dedupe and ensure cli-wrapper tag
|
const tags = [...new Set([...userTags, 'cli-wrapper'])]; // Dedupe and ensure cli-wrapper tag
|
||||||
addClaudeCustomEndpoint(projectDir, {
|
addCustomEndpoint(projectDir, {
|
||||||
id: endpointId,
|
id: endpointId,
|
||||||
name: request.name,
|
name: request.name,
|
||||||
enabled: request.enabled ?? true,
|
enabled: request.enabled ?? true,
|
||||||
tags,
|
tags,
|
||||||
availableModels: request.settings.availableModels,
|
availableModels: request.settings.availableModels,
|
||||||
settingsFile: request.settings.settingsFile
|
settingsFile: 'settingsFile' in request.settings ? (request.settings as any).settingsFile : undefined
|
||||||
});
|
});
|
||||||
console.log(`[CliSettings] Synced endpoint ${endpointId} to cli-tools.json tools (cli-wrapper)`);
|
console.log(`[CliSettings] Synced endpoint ${endpointId} to cli-tools.json tools (cli-wrapper)`);
|
||||||
} catch (syncError) {
|
} catch (syncError) {
|
||||||
@@ -182,13 +184,15 @@ export function loadEndpointSettings(endpointId: string): EndpointSettings | nul
|
|||||||
|
|
||||||
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
||||||
|
|
||||||
if (!validateSettings(settings)) {
|
const provider = (metadata as any).provider || 'claude';
|
||||||
|
if (!validateSettings(settings, provider)) {
|
||||||
console.error(`[CliSettings] Invalid settings format for ${endpointId}`);
|
console.error(`[CliSettings] Invalid settings format for ${endpointId}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...metadata,
|
...metadata,
|
||||||
|
provider,
|
||||||
settings
|
settings
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -225,7 +229,7 @@ export function deleteEndpointSettings(endpointId: string): SettingsOperationRes
|
|||||||
// Step 3: Remove from cli-tools.json tools (api-endpoint type)
|
// Step 3: Remove from cli-tools.json tools (api-endpoint type)
|
||||||
try {
|
try {
|
||||||
const projectDir = os.homedir();
|
const projectDir = os.homedir();
|
||||||
removeClaudeCustomEndpoint(projectDir, endpointId);
|
removeCustomEndpoint(projectDir, endpointId);
|
||||||
console.log(`[CliSettings] Removed endpoint ${endpointId} from cli-tools.json tools`);
|
console.log(`[CliSettings] Removed endpoint ${endpointId} from cli-tools.json tools`);
|
||||||
} catch (syncError) {
|
} catch (syncError) {
|
||||||
console.warn(`[CliSettings] Failed to remove from cli-tools.json: ${syncError}`);
|
console.warn(`[CliSettings] Failed to remove from cli-tools.json: ${syncError}`);
|
||||||
@@ -262,10 +266,12 @@ export function listAllSettings(): SettingsListResponse {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
||||||
|
const provider: CliProvider = (metadata as any).provider || 'claude';
|
||||||
|
|
||||||
if (validateSettings(settings)) {
|
if (validateSettings(settings, provider)) {
|
||||||
endpoints.push({
|
endpoints.push({
|
||||||
...metadata,
|
...metadata,
|
||||||
|
provider,
|
||||||
settings
|
settings
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -315,13 +321,13 @@ export function toggleEndpointEnabled(endpointId: string, enabled: boolean): Set
|
|||||||
const endpoint = loadEndpointSettings(endpointId);
|
const endpoint = loadEndpointSettings(endpointId);
|
||||||
const userTags = endpoint?.settings.tags || [];
|
const userTags = endpoint?.settings.tags || [];
|
||||||
const tags = [...new Set([...userTags, 'cli-wrapper'])]; // Dedupe and ensure cli-wrapper tag
|
const tags = [...new Set([...userTags, 'cli-wrapper'])]; // Dedupe and ensure cli-wrapper tag
|
||||||
addClaudeCustomEndpoint(projectDir, {
|
addCustomEndpoint(projectDir, {
|
||||||
id: endpointId,
|
id: endpointId,
|
||||||
name: metadata.name,
|
name: metadata.name,
|
||||||
enabled: enabled,
|
enabled: enabled,
|
||||||
tags,
|
tags,
|
||||||
availableModels: endpoint?.settings.availableModels,
|
availableModels: endpoint?.settings.availableModels,
|
||||||
settingsFile: endpoint?.settings.settingsFile
|
settingsFile: endpoint?.settings && 'settingsFile' in endpoint.settings ? (endpoint.settings as any).settingsFile : undefined
|
||||||
});
|
});
|
||||||
console.log(`[CliSettings] Synced endpoint ${endpointId} enabled=${enabled} to cli-tools.json tools`);
|
console.log(`[CliSettings] Synced endpoint ${endpointId} enabled=${enabled} to cli-tools.json tools`);
|
||||||
} catch (syncError) {
|
} catch (syncError) {
|
||||||
@@ -368,9 +374,8 @@ export function createSettingsFromProvider(provider: {
|
|||||||
name?: string;
|
name?: string;
|
||||||
}, options?: {
|
}, options?: {
|
||||||
model?: string;
|
model?: string;
|
||||||
includeCoAuthoredBy?: boolean;
|
}): CliSettings {
|
||||||
}): ClaudeCliSettings {
|
const settings = createDefaultSettings('claude');
|
||||||
const settings = createDefaultSettings();
|
|
||||||
|
|
||||||
// Map provider credentials to env
|
// Map provider credentials to env
|
||||||
if (provider.apiKey) {
|
if (provider.apiKey) {
|
||||||
@@ -384,9 +389,6 @@ export function createSettingsFromProvider(provider: {
|
|||||||
if (options?.model) {
|
if (options?.model) {
|
||||||
settings.model = options.model;
|
settings.model = options.model;
|
||||||
}
|
}
|
||||||
if (options?.includeCoAuthoredBy !== undefined) {
|
|
||||||
settings.includeCoAuthoredBy = options.includeCoAuthoredBy;
|
|
||||||
}
|
|
||||||
|
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ export async function handleCliSettingsRoutes(ctx: RouteContext): Promise<boolea
|
|||||||
if (!request.settings || !request.settings.env) {
|
if (!request.settings || !request.settings.env) {
|
||||||
return { error: 'settings.env is required', status: 400 };
|
return { error: 'settings.env is required', status: 400 };
|
||||||
}
|
}
|
||||||
// Deep validation of settings object
|
// Deep validation of settings object (provider-aware)
|
||||||
if (!validateSettings(request.settings)) {
|
if (!validateSettings(request.settings, request.provider)) {
|
||||||
return { error: 'Invalid settings object format', status: 400 };
|
return { error: 'Invalid settings object format', status: 400 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import { getCliSessionPolicy } from './cli-session-policy.js';
|
|||||||
import { appendCliSessionAudit } from './cli-session-audit.js';
|
import { appendCliSessionAudit } from './cli-session-audit.js';
|
||||||
import { getLaunchConfig } from './cli-launch-registry.js';
|
import { getLaunchConfig } from './cli-launch-registry.js';
|
||||||
import { assembleInstruction, type InstructionType } from './cli-instruction-assembler.js';
|
import { assembleInstruction, type InstructionType } from './cli-instruction-assembler.js';
|
||||||
|
import { loadEndpointSettings } from '../../config/cli-settings-manager.js';
|
||||||
|
import { getToolConfig } from '../../tools/claude-cli-tools.js';
|
||||||
|
|
||||||
export interface CliSession {
|
export interface CliSession {
|
||||||
sessionKey: string;
|
sessionKey: string;
|
||||||
@@ -41,6 +43,8 @@ export interface CreateCliSessionOptions {
|
|||||||
resumeKey?: string;
|
resumeKey?: string;
|
||||||
/** Launch mode for native CLI sessions. */
|
/** Launch mode for native CLI sessions. */
|
||||||
launchMode?: 'default' | 'yolo';
|
launchMode?: 'default' | 'yolo';
|
||||||
|
/** Settings endpoint ID for injecting env vars and settings into CLI process. */
|
||||||
|
settingsEndpointId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExecuteInCliSessionOptions {
|
export interface ExecuteInCliSessionOptions {
|
||||||
@@ -221,15 +225,56 @@ export class CliSessionManager {
|
|||||||
let args: string[];
|
let args: string[];
|
||||||
let cliTool: string | undefined;
|
let cliTool: string | undefined;
|
||||||
|
|
||||||
|
// Load settings endpoint env vars and extra args if specified
|
||||||
|
let endpointEnv: Record<string, string> = {};
|
||||||
|
let endpointExtraArgs: string[] = [];
|
||||||
|
|
||||||
|
if (options.settingsEndpointId) {
|
||||||
|
try {
|
||||||
|
const endpoint = loadEndpointSettings(options.settingsEndpointId);
|
||||||
|
if (endpoint) {
|
||||||
|
// Merge env vars (skip undefined/empty values)
|
||||||
|
for (const [key, value] of Object.entries(endpoint.settings.env)) {
|
||||||
|
if (value !== undefined && value !== '') {
|
||||||
|
endpointEnv[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Provider-specific argument injection
|
||||||
|
const provider = endpoint.provider || 'claude';
|
||||||
|
if (provider === 'claude' && 'settingsFile' in endpoint.settings && endpoint.settings.settingsFile) {
|
||||||
|
endpointExtraArgs.push('--settings', endpoint.settings.settingsFile);
|
||||||
|
} else if (provider === 'codex' && 'profile' in endpoint.settings && endpoint.settings.profile) {
|
||||||
|
endpointExtraArgs.push('--profile', endpoint.settings.profile);
|
||||||
|
}
|
||||||
|
// Gemini: env vars only, no extra CLI flags
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[CliSessionManager] Failed to load settings endpoint:', options.settingsEndpointId, err);
|
||||||
|
}
|
||||||
|
} else if (options.tool) {
|
||||||
|
// Fallback: read settingsFile from cli-tools.json for the tool
|
||||||
|
try {
|
||||||
|
const toolConfig = getToolConfig(this.projectRoot, options.tool);
|
||||||
|
if (options.tool === 'claude' && toolConfig.settingsFile) {
|
||||||
|
endpointExtraArgs.push('--settings', toolConfig.settingsFile);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Non-fatal: continue without settings file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (options.tool) {
|
if (options.tool) {
|
||||||
// Native CLI interactive session: spawn the CLI process directly
|
// Native CLI interactive session: spawn the CLI process directly
|
||||||
const launchMode = options.launchMode ?? 'default';
|
const launchMode = options.launchMode ?? 'default';
|
||||||
const config = getLaunchConfig(options.tool, launchMode);
|
const config = getLaunchConfig(options.tool, launchMode);
|
||||||
cliTool = options.tool;
|
cliTool = options.tool;
|
||||||
|
|
||||||
|
// Append endpoint-specific extra args (e.g., --settings for claude)
|
||||||
|
const allArgs = [...config.args, ...endpointExtraArgs];
|
||||||
|
|
||||||
// Build the full command string with arguments
|
// Build the full command string with arguments
|
||||||
const fullCommand = config.args.length > 0
|
const fullCommand = allArgs.length > 0
|
||||||
? `${config.command} ${config.args.join(' ')}`
|
? `${config.command} ${allArgs.join(' ')}`
|
||||||
: config.command;
|
: config.command;
|
||||||
|
|
||||||
// On Windows, CLI tools installed via npm are typically .cmd files.
|
// On Windows, CLI tools installed via npm are typically .cmd files.
|
||||||
@@ -275,7 +320,7 @@ export class CliSessionManager {
|
|||||||
// Unix: direct spawn works for most CLI tools
|
// Unix: direct spawn works for most CLI tools
|
||||||
shellKind = 'git-bash';
|
shellKind = 'git-bash';
|
||||||
file = config.command;
|
file = config.command;
|
||||||
args = config.args;
|
args = allArgs;
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@@ -289,6 +334,12 @@ export class CliSessionManager {
|
|||||||
args = picked.args;
|
args = picked.args;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Merge endpoint env vars with process.env (endpoint overrides process.env)
|
||||||
|
const spawnEnv: Record<string, string> = {
|
||||||
|
...(process.env as Record<string, string>),
|
||||||
|
...endpointEnv,
|
||||||
|
};
|
||||||
|
|
||||||
let pty: nodePty.IPty;
|
let pty: nodePty.IPty;
|
||||||
try {
|
try {
|
||||||
pty = nodePty.spawn(file, args, {
|
pty = nodePty.spawn(file, args, {
|
||||||
@@ -296,7 +347,7 @@ export class CliSessionManager {
|
|||||||
cols: options.cols ?? 120,
|
cols: options.cols ?? 120,
|
||||||
rows: options.rows ?? 30,
|
rows: options.rows ?? 30,
|
||||||
cwd: workingDir,
|
cwd: workingDir,
|
||||||
env: process.env as Record<string, string>
|
env: spawnEnv
|
||||||
});
|
});
|
||||||
} catch (spawnError: unknown) {
|
} catch (spawnError: unknown) {
|
||||||
const errorMsg = spawnError instanceof Error ? spawnError.message : String(spawnError);
|
const errorMsg = spawnError instanceof Error ? spawnError.message : String(spawnError);
|
||||||
|
|||||||
@@ -859,12 +859,12 @@ export function removeClaudeApiEndpoint(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Use addClaudeApiEndpoint instead
|
* Add a custom CLI settings endpoint tool to cli-tools.json
|
||||||
* Adds tool to config based on tags:
|
* Adds tool based on tags:
|
||||||
* - cli-wrapper tag -> type: 'cli-wrapper'
|
* - cli-wrapper tag -> type: 'cli-wrapper'
|
||||||
* - others -> type: 'api-endpoint'
|
* - others -> type: 'api-endpoint'
|
||||||
*/
|
*/
|
||||||
export function addClaudeCustomEndpoint(
|
export function addCustomEndpoint(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
endpoint: { id: string; name: string; enabled: boolean; tags?: string[]; availableModels?: string[]; settingsFile?: string }
|
endpoint: { id: string; name: string; enabled: boolean; tags?: string[]; availableModels?: string[]; settingsFile?: string }
|
||||||
): ClaudeCliToolsConfig {
|
): ClaudeCliToolsConfig {
|
||||||
@@ -895,10 +895,13 @@ export function addClaudeCustomEndpoint(
|
|||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use addCustomEndpoint instead */
|
||||||
|
export const addClaudeCustomEndpoint = addCustomEndpoint;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove endpoint tool (cli-wrapper or api-endpoint)
|
* Remove endpoint tool (cli-wrapper or api-endpoint)
|
||||||
*/
|
*/
|
||||||
export function removeClaudeCustomEndpoint(
|
export function removeCustomEndpoint(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
endpointId: string
|
endpointId: string
|
||||||
): ClaudeCliToolsConfig {
|
): ClaudeCliToolsConfig {
|
||||||
@@ -918,6 +921,9 @@ export function removeClaudeCustomEndpoint(
|
|||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use removeCustomEndpoint instead */
|
||||||
|
export const removeClaudeCustomEndpoint = removeCustomEndpoint;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get config source info
|
* Get config source info
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* CLI Settings Type Definitions
|
* CLI Settings Type Definitions
|
||||||
* Supports Claude CLI --settings parameter format
|
* Supports multi-provider CLI settings: Claude, Codex, Gemini
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CLI Provider type discriminator
|
||||||
|
*/
|
||||||
|
export type CliProvider = 'claude' | 'codex' | 'gemini';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Claude CLI Settings 文件格式
|
* Claude CLI Settings 文件格式
|
||||||
* 对应 `claude --settings <file-or-json>` 参数
|
* 对应 `claude --settings <file-or-json>` 参数
|
||||||
@@ -21,8 +26,6 @@ export interface ClaudeCliSettings {
|
|||||||
};
|
};
|
||||||
/** 模型选择 */
|
/** 模型选择 */
|
||||||
model?: 'opus' | 'sonnet' | 'haiku' | string;
|
model?: 'opus' | 'sonnet' | 'haiku' | string;
|
||||||
/** 是否包含 co-authored-by */
|
|
||||||
includeCoAuthoredBy?: boolean;
|
|
||||||
/** CLI工具标签 (用于标签路由) */
|
/** CLI工具标签 (用于标签路由) */
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
/** 可用模型列表 (显示在下拉菜单中) */
|
/** 可用模型列表 (显示在下拉菜单中) */
|
||||||
@@ -31,6 +34,60 @@ export interface ClaudeCliSettings {
|
|||||||
settingsFile?: string;
|
settingsFile?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Codex CLI Settings
|
||||||
|
* Codex 使用 --profile 传递配置, auth.json / config.toml 管理凭证和设置
|
||||||
|
*/
|
||||||
|
export interface CodexCliSettings {
|
||||||
|
/** 环境变量配置 */
|
||||||
|
env: {
|
||||||
|
/** OpenAI API Key */
|
||||||
|
OPENAI_API_KEY?: string;
|
||||||
|
/** OpenAI API Base URL */
|
||||||
|
OPENAI_BASE_URL?: string;
|
||||||
|
/** 其他自定义环境变量 */
|
||||||
|
[key: string]: string | undefined;
|
||||||
|
};
|
||||||
|
/** Codex profile 名称 (传递为 --profile <name>) */
|
||||||
|
profile?: string;
|
||||||
|
/** 模型选择 */
|
||||||
|
model?: string;
|
||||||
|
/** auth.json 内容 (JSON 字符串) */
|
||||||
|
authJson?: string;
|
||||||
|
/** config.toml 内容 (TOML 字符串) */
|
||||||
|
configToml?: string;
|
||||||
|
/** CLI工具标签 */
|
||||||
|
tags?: string[];
|
||||||
|
/** 可用模型列表 */
|
||||||
|
availableModels?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gemini CLI Settings
|
||||||
|
*/
|
||||||
|
export interface GeminiCliSettings {
|
||||||
|
/** 环境变量配置 */
|
||||||
|
env: {
|
||||||
|
/** Gemini API Key */
|
||||||
|
GEMINI_API_KEY?: string;
|
||||||
|
/** Google API Key (alternative) */
|
||||||
|
GOOGLE_API_KEY?: string;
|
||||||
|
/** 其他自定义环境变量 */
|
||||||
|
[key: string]: string | undefined;
|
||||||
|
};
|
||||||
|
/** 模型选择 */
|
||||||
|
model?: string;
|
||||||
|
/** CLI工具标签 */
|
||||||
|
tags?: string[];
|
||||||
|
/** 可用模型列表 */
|
||||||
|
availableModels?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Union type for all provider settings
|
||||||
|
*/
|
||||||
|
export type CliSettings = ClaudeCliSettings | CodexCliSettings | GeminiCliSettings;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 端点 Settings 配置(带元数据)
|
* 端点 Settings 配置(带元数据)
|
||||||
*/
|
*/
|
||||||
@@ -41,8 +98,10 @@ export interface EndpointSettings {
|
|||||||
name: string;
|
name: string;
|
||||||
/** 端点描述 */
|
/** 端点描述 */
|
||||||
description?: string;
|
description?: string;
|
||||||
/** Claude CLI Settings */
|
/** CLI provider 类型 (默认 'claude' 兼容旧数据) */
|
||||||
settings: ClaudeCliSettings;
|
provider: CliProvider;
|
||||||
|
/** CLI Settings (provider-specific) */
|
||||||
|
settings: CliSettings;
|
||||||
/** 是否启用 */
|
/** 是否启用 */
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
/** 创建时间 */
|
/** 创建时间 */
|
||||||
@@ -76,7 +135,9 @@ export interface SaveEndpointRequest {
|
|||||||
id?: string;
|
id?: string;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
settings: ClaudeCliSettings;
|
/** CLI provider 类型 */
|
||||||
|
provider?: CliProvider;
|
||||||
|
settings: CliSettings;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,22 +165,39 @@ export function mapProviderToClaudeEnv(provider: {
|
|||||||
/**
|
/**
|
||||||
* 创建默认 Settings
|
* 创建默认 Settings
|
||||||
*/
|
*/
|
||||||
export function createDefaultSettings(): ClaudeCliSettings {
|
export function createDefaultSettings(provider: CliProvider = 'claude'): CliSettings {
|
||||||
|
switch (provider) {
|
||||||
|
case 'codex':
|
||||||
|
return {
|
||||||
|
env: {},
|
||||||
|
model: '',
|
||||||
|
tags: [],
|
||||||
|
availableModels: []
|
||||||
|
} satisfies CodexCliSettings;
|
||||||
|
case 'gemini':
|
||||||
|
return {
|
||||||
|
env: {},
|
||||||
|
model: '',
|
||||||
|
tags: [],
|
||||||
|
availableModels: []
|
||||||
|
} satisfies GeminiCliSettings;
|
||||||
|
case 'claude':
|
||||||
|
default:
|
||||||
return {
|
return {
|
||||||
env: {
|
env: {
|
||||||
DISABLE_AUTOUPDATER: '1'
|
DISABLE_AUTOUPDATER: '1'
|
||||||
},
|
},
|
||||||
model: 'sonnet',
|
model: 'sonnet',
|
||||||
includeCoAuthoredBy: false,
|
|
||||||
tags: [],
|
tags: [],
|
||||||
availableModels: []
|
availableModels: []
|
||||||
};
|
} satisfies ClaudeCliSettings;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证 Settings 格式
|
* 验证 Settings 格式 (provider-aware)
|
||||||
*/
|
*/
|
||||||
export function validateSettings(settings: unknown): settings is ClaudeCliSettings {
|
export function validateSettings(settings: unknown, provider?: CliProvider): settings is CliSettings {
|
||||||
if (!settings || typeof settings !== 'object') {
|
if (!settings || typeof settings !== 'object') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -136,7 +214,6 @@ export function validateSettings(settings: unknown): settings is ClaudeCliSettin
|
|||||||
for (const key in envObj) {
|
for (const key in envObj) {
|
||||||
if (Object.prototype.hasOwnProperty.call(envObj, key)) {
|
if (Object.prototype.hasOwnProperty.call(envObj, key)) {
|
||||||
const value = envObj[key];
|
const value = envObj[key];
|
||||||
// 允许 undefined 或 string,其他类型(包括 null)都拒绝
|
|
||||||
if (value !== undefined && typeof value !== 'string') {
|
if (value !== undefined && typeof value !== 'string') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -148,11 +225,6 @@ export function validateSettings(settings: unknown): settings is ClaudeCliSettin
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// includeCoAuthoredBy 可选,但如果存在必须是布尔值
|
|
||||||
if (s.includeCoAuthoredBy !== undefined && typeof s.includeCoAuthoredBy !== 'boolean') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// tags 可选,但如果存在必须是数组
|
// tags 可选,但如果存在必须是数组
|
||||||
if (s.tags !== undefined && !Array.isArray(s.tags)) {
|
if (s.tags !== undefined && !Array.isArray(s.tags)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -163,10 +235,26 @@ export function validateSettings(settings: unknown): settings is ClaudeCliSettin
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Provider-specific validation
|
||||||
|
if (provider === 'codex') {
|
||||||
|
// profile 可选,但如果存在必须是字符串
|
||||||
|
if (s.profile !== undefined && typeof s.profile !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// authJson 可选,但如果存在必须是字符串
|
||||||
|
if (s.authJson !== undefined && typeof s.authJson !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// configToml 可选,但如果存在必须是字符串
|
||||||
|
if (s.configToml !== undefined && typeof s.configToml !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (provider === 'claude' || !provider) {
|
||||||
// settingsFile 可选,但如果存在必须是字符串
|
// settingsFile 可选,但如果存在必须是字符串
|
||||||
if (s.settingsFile !== undefined && typeof s.settingsFile !== 'string') {
|
if (s.settingsFile !== undefined && typeof s.settingsFile !== 'string') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,13 +17,15 @@ import {
|
|||||||
} from '../../dist/types/cli-settings.js';
|
} from '../../dist/types/cli-settings.js';
|
||||||
|
|
||||||
// Type for testing (interfaces are erased in JS)
|
// Type for testing (interfaces are erased in JS)
|
||||||
type ClaudeCliSettings = {
|
type CliSettings = {
|
||||||
env: Record<string, string | undefined>;
|
env: Record<string, string | undefined>;
|
||||||
model?: string;
|
model?: string;
|
||||||
includeCoAuthoredBy?: boolean;
|
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
availableModels?: string[];
|
availableModels?: string[];
|
||||||
settingsFile?: string;
|
settingsFile?: string;
|
||||||
|
profile?: string;
|
||||||
|
authJson?: string;
|
||||||
|
configToml?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('cli-settings.ts', () => {
|
describe('cli-settings.ts', () => {
|
||||||
@@ -37,7 +39,6 @@ describe('cli-settings.ts', () => {
|
|||||||
DISABLE_AUTOUPDATER: '1',
|
DISABLE_AUTOUPDATER: '1',
|
||||||
},
|
},
|
||||||
model: 'sonnet',
|
model: 'sonnet',
|
||||||
includeCoAuthoredBy: true,
|
|
||||||
tags: ['分析', 'Debug'],
|
tags: ['分析', 'Debug'],
|
||||||
availableModels: ['opus', 'sonnet', 'haiku'],
|
availableModels: ['opus', 'sonnet', 'haiku'],
|
||||||
settingsFile: '/path/to/settings.json',
|
settingsFile: '/path/to/settings.json',
|
||||||
@@ -241,23 +242,72 @@ describe('cli-settings.ts', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should validate includeCoAuthoredBy field', () => {
|
describe('should validate codex-specific fields', () => {
|
||||||
it('should accept boolean includeCoAuthoredBy', () => {
|
it('should accept valid codex settings with profile', () => {
|
||||||
const settings = {
|
const settings = {
|
||||||
env: {},
|
env: { OPENAI_API_KEY: 'sk-test' },
|
||||||
includeCoAuthoredBy: true,
|
model: 'gpt-5.2',
|
||||||
|
profile: 'default',
|
||||||
};
|
};
|
||||||
|
|
||||||
assert.strictEqual(validateSettings(settings), true);
|
assert.strictEqual(validateSettings(settings, 'codex'), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject non-boolean includeCoAuthoredBy', () => {
|
it('should reject non-string profile for codex', () => {
|
||||||
const settings = {
|
const settings = {
|
||||||
env: {},
|
env: {},
|
||||||
includeCoAuthoredBy: 'true',
|
profile: 123,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert.strictEqual(validateSettings(settings), false);
|
assert.strictEqual(validateSettings(settings, 'codex'), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept codex settings with authJson and configToml', () => {
|
||||||
|
const settings = {
|
||||||
|
env: { OPENAI_API_KEY: 'sk-test' },
|
||||||
|
authJson: '{"OPENAI_API_KEY": "sk-test"}',
|
||||||
|
configToml: 'model = "gpt-5.2"',
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.strictEqual(validateSettings(settings, 'codex'), true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should validate gemini-specific fields', () => {
|
||||||
|
it('should accept valid gemini settings', () => {
|
||||||
|
const settings = {
|
||||||
|
env: { GEMINI_API_KEY: 'AIza-test' },
|
||||||
|
model: 'gemini-2.5-flash',
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.strictEqual(validateSettings(settings, 'gemini'), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept gemini settings with GOOGLE_API_KEY', () => {
|
||||||
|
const settings = {
|
||||||
|
env: { GOOGLE_API_KEY: 'AIza-test' },
|
||||||
|
model: 'gemini-2.5-pro',
|
||||||
|
tags: ['分析'],
|
||||||
|
availableModels: ['gemini-2.5-flash', 'gemini-2.5-pro'],
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.strictEqual(validateSettings(settings, 'gemini'), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept gemini settings with empty env', () => {
|
||||||
|
const settings = {
|
||||||
|
env: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.strictEqual(validateSettings(settings, 'gemini'), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject gemini settings with non-string env value', () => {
|
||||||
|
const settings = {
|
||||||
|
env: { GEMINI_API_KEY: 12345 },
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.strictEqual(validateSettings(settings, 'gemini'), false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -358,7 +408,6 @@ describe('cli-settings.ts', () => {
|
|||||||
CUSTOM_VAR: 'custom-value',
|
CUSTOM_VAR: 'custom-value',
|
||||||
},
|
},
|
||||||
model: 'custom-model',
|
model: 'custom-model',
|
||||||
includeCoAuthoredBy: false,
|
|
||||||
tags: [],
|
tags: [],
|
||||||
availableModels: [],
|
availableModels: [],
|
||||||
settingsFile: '/path/to/settings.json',
|
settingsFile: '/path/to/settings.json',
|
||||||
@@ -436,53 +485,59 @@ describe('cli-settings.ts', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('createDefaultSettings', () => {
|
describe('createDefaultSettings', () => {
|
||||||
it('should create valid default settings', () => {
|
it('should create valid default claude settings', () => {
|
||||||
const settings = createDefaultSettings();
|
const settings = createDefaultSettings();
|
||||||
|
|
||||||
assert.strictEqual(validateSettings(settings), true);
|
assert.strictEqual(validateSettings(settings), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should include all default fields', () => {
|
it('should create valid default codex settings', () => {
|
||||||
|
const settings = createDefaultSettings('codex');
|
||||||
|
|
||||||
|
assert.strictEqual(validateSettings(settings, 'codex'), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create valid default gemini settings', () => {
|
||||||
|
const settings = createDefaultSettings('gemini');
|
||||||
|
|
||||||
|
assert.strictEqual(validateSettings(settings, 'gemini'), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include all default fields for claude', () => {
|
||||||
const settings = createDefaultSettings();
|
const settings = createDefaultSettings();
|
||||||
|
|
||||||
assert.ok('env' in settings);
|
assert.ok('env' in settings);
|
||||||
assert.ok('model' in settings);
|
assert.ok('model' in settings);
|
||||||
assert.ok('includeCoAuthoredBy' in settings);
|
|
||||||
assert.ok('tags' in settings);
|
assert.ok('tags' in settings);
|
||||||
assert.ok('availableModels' in settings);
|
assert.ok('availableModels' in settings);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have correct default values', () => {
|
it('should have correct default values for claude', () => {
|
||||||
const settings = createDefaultSettings();
|
const settings = createDefaultSettings();
|
||||||
|
|
||||||
assert.deepStrictEqual(settings.env, {
|
assert.deepStrictEqual(settings.env, {
|
||||||
DISABLE_AUTOUPDATER: '1',
|
DISABLE_AUTOUPDATER: '1',
|
||||||
});
|
});
|
||||||
assert.strictEqual(settings.model, 'sonnet');
|
assert.strictEqual(settings.model, 'sonnet');
|
||||||
assert.strictEqual(settings.includeCoAuthoredBy, false);
|
|
||||||
assert.deepStrictEqual(settings.tags, []);
|
assert.deepStrictEqual(settings.tags, []);
|
||||||
assert.deepStrictEqual(settings.availableModels, []);
|
assert.deepStrictEqual(settings.availableModels, []);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('TypeScript type safety', () => {
|
describe('TypeScript type safety', () => {
|
||||||
it('should enforce ClaudeCliSettings interface structure', () => {
|
it('should enforce CliSettings interface structure', () => {
|
||||||
// This test verifies TypeScript compilation catches type errors
|
|
||||||
const validSettings = {
|
const validSettings = {
|
||||||
env: {
|
env: {
|
||||||
ANTHROPIC_AUTH_TOKEN: 'sk-ant-123',
|
ANTHROPIC_AUTH_TOKEN: 'sk-ant-123',
|
||||||
},
|
},
|
||||||
model: 'opus',
|
model: 'opus',
|
||||||
includeCoAuthoredBy: true,
|
|
||||||
tags: ['tag1'],
|
tags: ['tag1'],
|
||||||
availableModels: ['model1'],
|
availableModels: ['model1'],
|
||||||
settingsFile: '/path/to/file',
|
settingsFile: '/path/to/file',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Type assertion: all fields should be present and of correct type
|
|
||||||
assert.strictEqual(typeof validSettings.env, 'object');
|
assert.strictEqual(typeof validSettings.env, 'object');
|
||||||
assert.strictEqual(typeof validSettings.model, 'string');
|
assert.strictEqual(typeof validSettings.model, 'string');
|
||||||
assert.strictEqual(typeof validSettings.includeCoAuthoredBy, 'boolean');
|
|
||||||
assert.strictEqual(Array.isArray(validSettings.tags), true);
|
assert.strictEqual(Array.isArray(validSettings.tags), true);
|
||||||
assert.strictEqual(Array.isArray(validSettings.availableModels), true);
|
assert.strictEqual(Array.isArray(validSettings.availableModels), true);
|
||||||
assert.strictEqual(typeof validSettings.settingsFile, 'string');
|
assert.strictEqual(typeof validSettings.settingsFile, 'string');
|
||||||
|
|||||||
Reference in New Issue
Block a user