// ======================================== // CliConfigModal Component // ======================================== // Config modal for creating a new CLI session in Terminal Dashboard. import * as React from 'react'; import { useIntl } from 'react-intl'; import { FolderOpen, RefreshCw } from 'lucide-react'; import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { Label } from '@/components/ui/Label'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from '@/components/ui/Dialog'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/Select'; import { RadioGroup, RadioGroupItem } from '@/components/ui/RadioGroup'; import { useConfigStore, selectCliTools } from '@/stores/configStore'; import { useCliSettings } from '@/hooks/useApiSettings'; export type CliTool = string; export type LaunchMode = 'default' | 'yolo'; export type ShellKind = 'bash' | 'pwsh' | 'cmd'; export interface CliSessionConfig { tool: CliTool; model?: string; launchMode: LaunchMode; preferredShell: ShellKind; workingDir: string; /** Session tag for grouping (auto-generated if not provided) */ tag: string; /** CLI Settings endpoint ID for custom API configuration */ settingsEndpointId?: string; } export interface CliConfigModalProps { isOpen: boolean; onClose: () => void; defaultWorkingDir?: string | null; onCreateSession: (config: CliSessionConfig) => Promise; } const AUTO_MODEL_VALUE = '__auto__'; /** * Generate a tag name: {tool}-{HHmmss} * Example: gemini-143052 */ function generateTag(tool: string): string { const now = new Date(); const time = now.toTimeString().slice(0, 8).replace(/:/g, ''); return `${tool}-${time}`; } export function CliConfigModal({ isOpen, onClose, defaultWorkingDir, onCreateSession, }: CliConfigModalProps) { 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('gemini'); const [model, setModel] = React.useState(undefined); const [launchMode, setLaunchMode] = React.useState('yolo'); // Default to 'cmd' on Windows for better compatibility with npm CLI tools (.cmd files) const [preferredShell, setPreferredShell] = React.useState( typeof navigator !== 'undefined' && navigator.platform.toLowerCase().includes('win') ? 'cmd' : 'bash' ); const [workingDir, setWorkingDir] = React.useState(defaultWorkingDir ?? ''); const [tag, setTag] = React.useState(''); const [isSubmitting, setIsSubmitting] = React.useState(false); const [error, setError] = React.useState(null); // CLI Settings integration (for all tools) const { cliSettings } = useCliSettings({ enabled: true }); // Map tool names to provider types for filtering const toolProviderMap: Record = { 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(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; // Build models from primaryModel/secondaryModel, filtering out undefined const models: string[] = []; if (toolConfig.primaryModel) models.push(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 const regenerateTag = React.useCallback(() => { setTag(generateTag(tool)); }, [tool]); React.useEffect(() => { if (!isOpen) return; // Reset to a safe default each time the modal is opened. const nextWorkingDir = defaultWorkingDir ?? ''; setWorkingDir(nextWorkingDir); setError(null); setSettingsEndpointId(undefined); regenerateTag(); }, [isOpen, defaultWorkingDir, regenerateTag]); // Update tag prefix when tool changes React.useEffect(() => { if (tag) { const suffix = tag.split('-').pop() || ''; setTag(`${tool}-${suffix}`); } // eslint-disable-next-line react-hooks/exhaustive-deps -- only re-run when tool changes, reading tag intentionally stale }, [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) => { setTool(nextTool as CliTool); const nextConfig = cliTools[nextTool]; const nextModels = nextConfig?.availableModels?.length ? nextConfig.availableModels : [nextConfig?.primaryModel, nextConfig?.secondaryModel].filter(Boolean) as string[]; if (!model || !nextModels.includes(model)) { setModel(nextModels[0]); } }; const handleBrowse = () => { // Reserved for future file-picker integration console.log('[CliConfigModal] browse working directory - not implemented'); }; const handleCreate = async () => { const dir = workingDir.trim(); if (!dir) { setError(formatMessage({ id: 'terminalDashboard.cliConfig.errors.workingDirRequired' })); return; } const finalTag = tag.trim() || generateTag(tool); setIsSubmitting(true); setError(null); try { await onCreateSession({ tool, model, launchMode, preferredShell, workingDir: dir, tag: finalTag, settingsEndpointId, }); onClose(); } catch (err) { console.error('[CliConfigModal] create session failed:', err); setError(formatMessage({ id: 'terminalDashboard.cliConfig.errors.createFailed' })); } finally { setIsSubmitting(false); } }; return ( !open && onClose()}> {formatMessage({ id: 'terminalDashboard.cliConfig.title' })} {formatMessage({ id: 'terminalDashboard.cliConfig.description' })}
{/* Tag / Name */}
setTag(e.target.value)} placeholder={formatMessage({ id: 'terminalDashboard.cliConfig.tagPlaceholder', defaultMessage: 'e.g., gemini-143052' })} disabled={isSubmitting} className="flex-1" />

{formatMessage({ id: 'terminalDashboard.cliConfig.tagHint', defaultMessage: 'Auto-generated as {tool}-{time}. Used for grouping sessions.' })}

{/* Tool */}
{/* Model */}
{/* Config Profile (all tools with settings) */} {enabledCliSettings.length > 0 && (

{formatMessage({ id: 'terminalDashboard.cliConfig.configProfileHint', defaultMessage: 'Select a CLI Settings profile for custom API configuration.' })}

)} {/* Mode */}
setLaunchMode(v as LaunchMode)} className="flex items-center gap-4" >
{/* Shell */}
{/* Working Directory */}
{ setWorkingDir(e.target.value); if (error) setError(null); }} placeholder={formatMessage({ id: 'terminalDashboard.cliConfig.workingDirPlaceholder' })} disabled={isSubmitting} className={cn(error && 'border-destructive')} />
{error &&

{error}

}
); } export default CliConfigModal;