@nanocollective/nanocoder
Version:
A local-first CLI coding agent that brings the power of agentic coding tools like Claude Code and Gemini CLI to local models or controlled APIs like OpenRouter
632 lines • 32 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { Box, Text, useInput } from 'ink';
import SelectInput from 'ink-select-input';
import Spinner from 'ink-spinner';
import { useEffect, useRef, useState } from 'react';
import TextInput from '../../components/text-input.js';
import { colors } from '../../config/index.js';
import { useResponsiveTerminal } from '../../hooks/useTerminalWidth.js';
import { PROVIDER_TEMPLATES, } from '../templates/provider-templates.js';
import { fetchCloudModels } from '../utils/fetch-cloud-models.js';
import { fetchLocalModels, } from '../utils/fetch-local-models.js';
// Helper to check if modelsEndpoint is a cloud provider type
const CLOUD_ENDPOINTS = [
'anthropic',
'openai',
'mistral',
'github',
];
const isCloudEndpoint = (endpoint) => {
return CLOUD_ENDPOINTS.includes(endpoint);
};
const LOCAL_ENDPOINTS = [
'ollama',
'openai-compatible',
];
const isLocalEndpoint = (endpoint) => {
return LOCAL_ENDPOINTS.includes(endpoint);
};
export function ProviderStep({ onComplete, onBack, onDelete, existingProviders = [], configExists = false, }) {
const { isNarrow } = useResponsiveTerminal();
const [providers, setProviders] = useState(existingProviders);
// Update providers when existingProviders prop changes
useEffect(() => {
setProviders(existingProviders);
}, [existingProviders]);
const [mode, setMode] = useState('select-template-or-custom');
const [selectedTemplate, setSelectedTemplate] = useState(null);
const [currentFieldIndex, setCurrentFieldIndex] = useState(0);
const [fieldAnswers, setFieldAnswers] = useState({});
const [currentValue, setCurrentValue] = useState('');
const [error, setError] = useState(null);
const [inputKey, setInputKey] = useState(0);
const [cameFromCustom, setCameFromCustom] = useState(false);
const [editingIndex, setEditingIndex] = useState(null);
const [fetchedModels, setFetchedModels] = useState([]);
const [selectedModelIds, setSelectedModelIds] = useState(new Set());
const [fetchError, setFetchError] = useState(null);
// Ref to store timeout ID for cleanup (prevents memory leak)
const fallbackTimeoutRef = useRef(null);
// Ref to track current template during async operations (prevents stale closure)
const currentTemplateRef = useRef(null);
// Ref to track if component is mounted (prevents setState after unmount)
const isMountedRef = useRef(true);
// Keep template ref in sync with state
useEffect(() => {
currentTemplateRef.current = selectedTemplate;
}, [selectedTemplate]);
// Track mount status and cleanup on unmount
useEffect(() => {
isMountedRef.current = true;
return () => {
isMountedRef.current = false;
if (fallbackTimeoutRef.current) {
clearTimeout(fallbackTimeoutRef.current);
}
};
}, []);
// Clear model-related state when template changes (prevents stale data leaking)
// Note: We intentionally depend on selectedTemplate to trigger cleanup on template change
useEffect(() => {
// Only clear if we have a template (avoid clearing on initial mount with null)
if (selectedTemplate !== null) {
setFetchedModels([]);
setSelectedModelIds(new Set());
setFetchError(null);
}
// Also clear any pending fallback timeout when template changes
if (fallbackTimeoutRef.current) {
clearTimeout(fallbackTimeoutRef.current);
fallbackTimeoutRef.current = null;
}
}, [selectedTemplate]);
const initialOptions = [
{ label: 'Choose from common templates', value: 'templates' },
{ label: 'Add custom provider manually', value: 'custom' },
...(providers.length > 0
? [{ label: 'Edit existing providers', value: 'edit' }]
: []),
...(providers.length > 0 ? [{ label: 'Done & Save', value: 'done' }] : []),
...(configExists && onDelete
? [{ label: 'Delete config file', value: 'delete' }]
: []),
];
const getTemplateOptions = () => [
...PROVIDER_TEMPLATES.map(template => ({
label: template.name,
value: template.id,
})),
...(providers.length > 0 ? [{ label: 'Done & Save', value: 'done' }] : []),
];
const editOptions = [
...providers.map((provider, index) => ({
label: `${index + 1}. ${provider.name}`,
value: `edit-${index}`,
})),
];
const handleInitialSelect = (item) => {
if (item.value === 'templates') {
setMode('template-selection');
setCameFromCustom(false);
}
else if (item.value === 'custom') {
// Find custom template
const customTemplate = PROVIDER_TEMPLATES.find(t => t.id === 'custom');
if (customTemplate) {
setSelectedTemplate(customTemplate);
setCurrentFieldIndex(0);
setFieldAnswers({});
setCurrentValue('');
setMode('field-input');
setCameFromCustom(true);
}
}
else if (item.value === 'edit') {
setMode('edit-selection');
}
else if (item.value === 'done') {
onComplete(providers);
}
else if (item.value === 'delete' && onDelete) {
onDelete();
}
};
const handleTemplateSelect = (item) => {
if (item.value === 'done') {
onComplete(providers);
return;
}
// Adding new provider
const template = PROVIDER_TEMPLATES.find(t => t.id === item.value);
if (template) {
setEditingIndex(null); // Not editing
setSelectedTemplate(template);
setCurrentFieldIndex(0);
setFieldAnswers({});
setCurrentValue(template.fields[0]?.default || '');
setError(null);
setMode('field-input');
setCameFromCustom(false);
}
};
const handleEditSelect = (item) => {
// Store the index and show edit/delete options
if (item.value.startsWith('edit-')) {
const index = Number.parseInt(item.value.replace('edit-', ''), 10);
setEditingIndex(index);
setMode('edit-or-delete');
}
};
const handleEditOrDeleteChoice = (item) => {
if (item.value === 'delete' && editingIndex !== null) {
// Delete the provider
const newProviders = providers.filter((_, i) => i !== editingIndex);
setProviders(newProviders);
setEditingIndex(null);
// Always go back to initial menu after deleting
setMode('select-template-or-custom');
return;
}
if (item.value === 'edit' && editingIndex !== null) {
const provider = providers[editingIndex];
if (provider) {
// Find matching template (or use custom)
const template = PROVIDER_TEMPLATES.find(t => t.id === provider.name) ||
PROVIDER_TEMPLATES.find(t => t.id === 'custom');
if (template) {
setSelectedTemplate(template);
setCurrentFieldIndex(0);
// Pre-populate field answers from existing provider
const answers = {};
if (provider.name)
answers.providerName = provider.name;
if (provider.baseUrl)
answers.baseUrl = provider.baseUrl;
if (provider.apiKey)
answers.apiKey = provider.apiKey;
if (provider.models)
answers.model = provider.models.join(', ');
setFieldAnswers(answers);
setCurrentValue(answers[template.fields[0]?.name] ||
template.fields[0]?.default ||
'');
setError(null);
setMode('field-input');
setCameFromCustom(false);
}
}
}
};
const handleFieldSubmit = () => {
if (!selectedTemplate)
return;
const currentField = selectedTemplate.fields[currentFieldIndex];
if (!currentField)
return;
// Validate required fields
if (currentField.required && !currentValue.trim()) {
setError('This field is required');
return;
}
// Validate with custom validator
if (currentField.validator && currentValue.trim()) {
const validationError = currentField.validator(currentValue);
if (validationError) {
setError(validationError);
return;
}
}
// Save answer
const newAnswers = {
...fieldAnswers,
[currentField.name]: currentValue.trim(),
};
setFieldAnswers(newAnswers);
setError(null);
// Move to next field or complete
if (currentFieldIndex < selectedTemplate.fields.length - 1) {
const nextField = selectedTemplate.fields[currentFieldIndex + 1];
// Check if we should offer to fetch models
// For local providers: after baseUrl field
// For cloud providers: after apiKey field
const shouldOfferModelFetch = nextField?.name === 'model' &&
selectedTemplate.modelsEndpoint &&
((currentField.name === 'baseUrl' &&
isLocalEndpoint(selectedTemplate.modelsEndpoint)) ||
(currentField.name === 'apiKey' &&
isCloudEndpoint(selectedTemplate.modelsEndpoint)));
if (shouldOfferModelFetch) {
setMode('model-source-choice');
return;
}
setCurrentFieldIndex(currentFieldIndex + 1);
setCurrentValue(newAnswers[nextField?.name] || nextField?.default || '');
}
else {
// Validate models array is not empty before building config
const modelsValue = newAnswers.model || '';
const modelsArray = modelsValue
.split(',')
.map(m => m.trim())
.filter(Boolean);
if (modelsArray.length === 0) {
setError('At least one model name is required');
return;
}
// Build config and add/update provider
try {
const providerConfig = selectedTemplate.buildConfig(newAnswers);
if (editingIndex !== null) {
// Replace existing provider
const newProviders = [...providers];
newProviders[editingIndex] = providerConfig;
setProviders(newProviders);
}
else {
// Add new provider
setProviders([...providers, providerConfig]);
}
// Reset for next provider
setSelectedTemplate(null);
setCurrentFieldIndex(0);
setFieldAnswers({});
setCurrentValue('');
setEditingIndex(null);
setMode('template-selection');
}
catch (err) {
setError(err instanceof Error ? err.message : 'Failed to build configuration');
}
}
};
const handleModelSourceChoice = async (item) => {
if (item.value === 'manual') {
// User chose to enter manually - go to model field input
const modelFieldIndex = selectedTemplate?.fields.findIndex(f => f.name === 'model');
if (modelFieldIndex !== undefined && modelFieldIndex >= 0) {
setCurrentFieldIndex(modelFieldIndex);
const modelField = selectedTemplate?.fields[modelFieldIndex];
setCurrentValue(fieldAnswers[modelField?.name || ''] || modelField?.default || '');
setMode('field-input');
}
return;
}
// User chose to fetch models
const endpointType = selectedTemplate?.modelsEndpoint;
const isCloud = isCloudEndpoint(endpointType);
// For cloud providers, we need the API key; for local providers, we need baseUrl
if (isCloud) {
const apiKey = fieldAnswers.apiKey;
if (!apiKey || !apiKey.trim()) {
setFetchError('API key is required');
const modelFieldIndex = selectedTemplate?.fields.findIndex(f => f.name === 'model');
if (modelFieldIndex !== undefined && modelFieldIndex >= 0) {
setCurrentFieldIndex(modelFieldIndex);
const modelField = selectedTemplate?.fields[modelFieldIndex];
setCurrentValue(fieldAnswers[modelField?.name || ''] || modelField?.default || '');
setMode('field-input');
}
return;
}
setMode('fetching-models');
setFetchError(null);
const result = await fetchCloudModels(endpointType, apiKey);
// Guard against setState after unmount
if (!isMountedRef.current)
return;
if (result.success && result.models.length > 0) {
setFetchedModels(result.models);
setSelectedModelIds(new Set());
setMode('model-selection');
return;
}
// API key validation failed - go back to API key field with error
// This is a meaningful check: invalid keys should be fixed, not bypassed
const apiKeyIndex = selectedTemplate?.fields.findIndex(f => f.name === 'apiKey');
if (apiKeyIndex !== undefined && apiKeyIndex >= 0) {
setCurrentFieldIndex(apiKeyIndex);
setCurrentValue(''); // Clear the invalid key
setError(result.error || 'Failed to validate API key');
setMode('field-input');
}
return;
}
else {
// Local provider - need baseUrl
const baseUrl = fieldAnswers.baseUrl;
if (!baseUrl || !baseUrl.trim()) {
setFetchError('Base URL is required');
const modelFieldIndex = selectedTemplate?.fields.findIndex(f => f.name === 'model');
if (modelFieldIndex !== undefined && modelFieldIndex >= 0) {
setCurrentFieldIndex(modelFieldIndex);
const modelField = selectedTemplate?.fields[modelFieldIndex];
setCurrentValue(fieldAnswers[modelField?.name || ''] || modelField?.default || '');
setMode('field-input');
}
return;
}
setMode('fetching-models');
setFetchError(null);
const localEndpoint = isLocalEndpoint(endpointType)
? endpointType
: 'openai-compatible';
const result = await fetchLocalModels(baseUrl, localEndpoint);
// Guard against setState after unmount
if (!isMountedRef.current)
return;
if (result.success && result.models.length > 0) {
setFetchedModels(result.models);
setSelectedModelIds(new Set());
setMode('model-selection');
return;
}
// Fetch failed - show brief error and fallback to manual input
setFetchError(result.error || 'Failed to fetch models');
}
// Both branches fall through here on failure - set up fallback timeout
// Clear any existing timeout before setting a new one
if (fallbackTimeoutRef.current) {
clearTimeout(fallbackTimeoutRef.current);
}
// Capture fieldAnswers at this moment to avoid stale closure
const capturedFieldAnswers = { ...fieldAnswers };
// After a brief delay, go to manual input (500ms - short enough to not frustrate)
fallbackTimeoutRef.current = setTimeout(() => {
// Guard against setState after unmount
if (!isMountedRef.current)
return;
// Use ref for template to get current value (prevents stale closure)
const template = currentTemplateRef.current;
if (!template)
return;
const modelFieldIndex = template.fields.findIndex(f => f.name === 'model');
if (modelFieldIndex !== undefined && modelFieldIndex >= 0) {
setCurrentFieldIndex(modelFieldIndex);
const modelField = template.fields[modelFieldIndex];
setCurrentValue(capturedFieldAnswers[modelField?.name || ''] ||
modelField?.default ||
'');
setMode('field-input');
}
}, 500);
};
const handleModelToggle = (modelId) => {
setSelectedModelIds(prev => {
const newSet = new Set(prev);
if (newSet.has(modelId)) {
newSet.delete(modelId);
}
else {
newSet.add(modelId);
}
return newSet;
});
};
const handleSelectAllModels = () => {
setSelectedModelIds(prev => {
if (prev.size === fetchedModels.length) {
// Deselect all
return new Set();
}
else {
// Select all
return new Set(fetchedModels.map(m => m.id));
}
});
};
const handleModelSelectionComplete = () => {
if (selectedModelIds.size === 0) {
setError('Please select at least one model');
return;
}
// Save selected models to fieldAnswers
const selectedModels = Array.from(selectedModelIds).join(', ');
const newAnswers = {
...fieldAnswers,
model: selectedModels,
};
setFieldAnswers(newAnswers);
setError(null);
// Find the model field index and continue to the next field or complete
if (!selectedTemplate)
return;
const modelFieldIndex = selectedTemplate.fields.findIndex(f => f.name === 'model');
if (modelFieldIndex < selectedTemplate.fields.length - 1) {
// There are more fields after model
setCurrentFieldIndex(modelFieldIndex + 1);
const nextField = selectedTemplate.fields[modelFieldIndex + 1];
setCurrentValue(newAnswers[nextField?.name] || nextField?.default || '');
setMode('field-input');
}
else {
// Model was the last field - build config
try {
const providerConfig = selectedTemplate.buildConfig(newAnswers);
if (editingIndex !== null) {
const newProviders = [...providers];
newProviders[editingIndex] = providerConfig;
setProviders(newProviders);
}
else {
setProviders([...providers, providerConfig]);
}
// Reset for next provider
setSelectedTemplate(null);
setCurrentFieldIndex(0);
setFieldAnswers({});
setCurrentValue('');
setEditingIndex(null);
setFetchedModels([]);
setSelectedModelIds(new Set());
setMode('template-selection');
}
catch (err) {
setError(err instanceof Error ? err.message : 'Failed to build configuration');
}
}
};
useInput((_input, key) => {
// Handle Shift+Tab for going back
if (key.shift && key.tab) {
if (mode === 'field-input') {
// In field input mode, check if we can go back to previous field
if (currentFieldIndex > 0) {
// Go back to previous field
setCurrentFieldIndex(currentFieldIndex - 1);
const prevField = selectedTemplate?.fields[currentFieldIndex - 1];
setCurrentValue(fieldAnswers[prevField?.name || ''] || prevField?.default || '');
setInputKey(prev => prev + 1); // Force remount to reset cursor position
setError(null);
}
else {
// At first field, go back based on where we came from
if (editingIndex !== null) {
// Was editing, go back to edit-or-delete choice
setMode('edit-or-delete');
}
else if (cameFromCustom) {
// Came from custom selection, go back to initial choice
setMode('select-template-or-custom');
}
else {
// Came from template selection, go back there
setMode('template-selection');
}
setSelectedTemplate(null);
setCurrentFieldIndex(0);
setFieldAnswers({});
setCurrentValue('');
setError(null);
}
}
else if (mode === 'template-selection') {
// In template selection, go back to initial choice
setMode('select-template-or-custom');
}
else if (mode === 'edit-or-delete') {
// In edit-or-delete, go back to edit selection
setEditingIndex(null);
setMode('edit-selection');
}
else if (mode === 'edit-selection') {
// In edit selection, go back to initial choice
setMode('select-template-or-custom');
}
else if (mode === 'model-source-choice') {
// In model source choice, go back to the appropriate field
// Cloud providers: go back to apiKey; Local providers: go back to baseUrl
const isCloud = isCloudEndpoint(selectedTemplate?.modelsEndpoint);
const fieldToGoBack = isCloud ? 'apiKey' : 'baseUrl';
const fieldIndex = selectedTemplate?.fields.findIndex(f => f.name === fieldToGoBack);
if (fieldIndex !== undefined && fieldIndex >= 0) {
setCurrentFieldIndex(fieldIndex);
const field = selectedTemplate?.fields[fieldIndex];
setCurrentValue(fieldAnswers[field?.name || ''] || field?.default || '');
setError(null);
setMode('field-input');
}
}
else if (mode === 'model-selection') {
// In model selection, go back to model source choice
setFetchedModels([]);
setSelectedModelIds(new Set());
setFetchError(null);
setError(null);
setMode('model-source-choice');
}
else if (mode === 'select-template-or-custom') {
// At root level, call parent's onBack
if (onBack) {
onBack();
}
}
return;
}
if (mode === 'field-input') {
if (key.return) {
handleFieldSubmit();
}
else if (key.escape) {
// Go back to template selection
setMode('template-selection');
setSelectedTemplate(null);
setCurrentFieldIndex(0);
setFieldAnswers({});
setCurrentValue('');
setError(null);
}
}
if (mode === 'model-selection') {
if (key.escape) {
// Go back to model source choice
setFetchedModels([]);
setSelectedModelIds(new Set());
setError(null);
setMode('model-source-choice');
}
}
});
if (mode === 'select-template-or-custom') {
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: colors.primary, children: "Let's add AI providers. Would you like to use a template?" }) }), providers.length > 0 && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: colors.success, children: [providers.length, " provider(s) already added"] }) })), _jsx(SelectInput, { items: initialOptions, onSelect: (item) => handleInitialSelect(item) })] }));
}
if (mode === 'template-selection') {
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: colors.primary, children: "Choose a provider template:" }) }), providers.length > 0 && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: colors.success, children: ["Added: ", providers.map(p => p.name).join(', ')] }) })), _jsx(SelectInput, { items: getTemplateOptions(), onSelect: (item) => handleTemplateSelect(item) })] }));
}
if (mode === 'edit-selection') {
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: colors.primary, children: "Select a provider to edit:" }) }), _jsx(SelectInput, { items: editOptions, onSelect: (item) => handleEditSelect(item) })] }));
}
if (mode === 'edit-or-delete') {
const provider = editingIndex !== null ? providers[editingIndex] : null;
const editOrDeleteOptions = [
{ label: 'Edit this provider', value: 'edit' },
{ label: 'Delete this provider', value: 'delete' },
];
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: colors.primary, children: [provider?.name, " - What would you like to do?"] }) }), _jsx(SelectInput, { items: editOrDeleteOptions, onSelect: (item) => handleEditOrDeleteChoice(item) })] }));
}
if (mode === 'field-input' && selectedTemplate) {
const currentField = selectedTemplate.fields[currentFieldIndex];
if (!currentField)
return null;
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { bold: true, color: colors.primary, children: [selectedTemplate.name, " Configuration"] }), _jsxs(Text, { dimColor: true, children: [' ', "(Field ", currentFieldIndex + 1, "/", selectedTemplate.fields.length, ")"] })] }), _jsx(Box, { children: _jsxs(Text, { children: [currentField.prompt, currentField.required && _jsx(Text, { color: colors.error, children: " *" }), ":", ' ', currentField.sensitive && '****'] }) }), !currentField.sensitive && (_jsx(Box, { marginBottom: 1, borderStyle: "round", borderColor: colors.secondary, children: _jsx(TextInput, { value: currentValue, onChange: setCurrentValue, onSubmit: handleFieldSubmit }, inputKey) })), currentField.sensitive && (_jsx(Box, { marginBottom: 1, borderStyle: "round", borderColor: colors.secondary, children: _jsx(TextInput, { value: currentValue, onChange: setCurrentValue, onSubmit: handleFieldSubmit, mask: "*" }, inputKey) })), error && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: colors.error, children: error }) })), isNarrow ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: colors.secondary, children: "Enter: continue" }), _jsx(Text, { color: colors.secondary, children: "Shift+Tab: go back" })] })) : (_jsx(Box, { children: _jsx(Text, { color: colors.secondary, children: "Press Enter to continue | Shift+Tab to go back" }) }))] }));
}
if (mode === 'model-source-choice' && selectedTemplate) {
const modelSourceOptions = [
{ label: 'Fetch available models from server', value: 'fetch' },
{ label: 'Enter model names manually', value: 'manual' },
];
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: colors.primary, children: [selectedTemplate.name, " Configuration"] }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: "How would you like to specify models?" }) }), _jsx(SelectInput, { items: modelSourceOptions, onSelect: (item) => handleModelSourceChoice(item) }), isNarrow ? (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(Text, { color: colors.secondary, children: "Shift+Tab: go back" }) })) : (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.secondary, children: "Shift+Tab to go back" }) }))] }));
}
if (mode === 'fetching-models' && selectedTemplate) {
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: colors.primary, children: [selectedTemplate.name, " Configuration"] }) }), fetchError ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: colors.error, children: fetchError }) }), _jsx(Text, { dimColor: true, children: "Falling back to manual input..." })] })) : (_jsx(Box, { children: _jsxs(Text, { color: colors.info, children: [_jsx(Spinner, { type: "dots" }), " Fetching models from", ' ', fieldAnswers.baseUrl || selectedTemplate.name, "..."] }) }))] }));
}
if (mode === 'model-selection' && selectedTemplate) {
const allSelected = selectedModelIds.size === fetchedModels.length;
const modelOptions = [
{
// Show checked when all selected, unchecked when not - matches visual state
label: allSelected
? '[✓] All selected (toggle to deselect)'
: '[ ] Select All',
value: '__select_all__',
},
...fetchedModels.map(m => ({
label: `${selectedModelIds.has(m.id) ? '[✓]' : '[ ]'} ${m.name}`,
value: m.id,
})),
{ label: 'Done - Continue with selected models', value: '__done__' },
];
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: colors.primary, children: [selectedTemplate.name, " Configuration"] }) }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { children: ["Select models to use (", selectedModelIds.size, " selected):"] }) }), _jsx(SelectInput, { items: modelOptions, onSelect: (item) => {
if (item.value === '__done__') {
handleModelSelectionComplete();
}
else if (item.value === '__select_all__') {
handleSelectAllModels();
}
else {
handleModelToggle(item.value);
}
} }), error && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.error, children: error }) })), isNarrow ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: colors.secondary, children: "Enter: toggle/continue" }), _jsx(Text, { color: colors.secondary, children: "Shift+Tab: go back" })] })) : (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.secondary, children: "Press Enter to toggle | Shift+Tab to go back" }) }))] }));
}
return null;
}
//# sourceMappingURL=provider-step.js.map