mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2026-01-30 06:12:05 +00:00
- Add gemini-3-flash-preview to Google and Gemini CLI providers - Use name field from supported-models.json when available - Improve model search to match both display names and model IDs
224 lines
6.0 KiB
TypeScript
224 lines
6.0 KiB
TypeScript
/**
|
|
* @fileoverview Interactive prompt logic for model selection
|
|
*/
|
|
|
|
import search, { Separator } from '@inquirer/search';
|
|
import chalk from 'chalk';
|
|
import { getAvailableModels } from '../../lib/model-management.js';
|
|
import { getCustomProviderOptions } from './custom-providers.js';
|
|
import type {
|
|
CurrentModels,
|
|
ModelChoice,
|
|
ModelInfo,
|
|
ModelRole,
|
|
PromptData
|
|
} from './types.js';
|
|
|
|
/**
|
|
* Build prompt choices for a specific role
|
|
*/
|
|
export function buildPromptChoices(
|
|
role: ModelRole,
|
|
currentModels: CurrentModels,
|
|
allowNone = false
|
|
): PromptData {
|
|
const currentModel = currentModels[role];
|
|
const allModels = getAvailableModels();
|
|
|
|
// Group models by provider (filter out models without provider)
|
|
const modelsByProvider = allModels
|
|
.filter(
|
|
(model): model is ModelInfo & { provider: string } => !!model.provider
|
|
)
|
|
.reduce(
|
|
(acc, model) => {
|
|
if (!acc[model.provider]) {
|
|
acc[model.provider] = [];
|
|
}
|
|
acc[model.provider].push(model);
|
|
return acc;
|
|
},
|
|
{} as Record<string, ModelInfo[]>
|
|
);
|
|
|
|
// System options (cancel and no change)
|
|
const systemOptions: ModelChoice[] = [];
|
|
const cancelOption: ModelChoice = {
|
|
name: '⏹ Cancel Model Setup',
|
|
value: '__CANCEL__',
|
|
short: 'Cancel'
|
|
};
|
|
const noChangeOption: ModelChoice | null =
|
|
currentModel?.modelId && currentModel?.provider
|
|
? {
|
|
name: `✔ No change to current ${role} model (${currentModel.provider}/${currentModel.modelId})`,
|
|
value: '__NO_CHANGE__',
|
|
short: 'No change'
|
|
}
|
|
: null;
|
|
|
|
if (noChangeOption) {
|
|
systemOptions.push(noChangeOption);
|
|
}
|
|
systemOptions.push(cancelOption);
|
|
|
|
// Build role-specific model choices
|
|
const roleChoices: ModelChoice[] = Object.entries(modelsByProvider)
|
|
.flatMap(([provider, models]) => {
|
|
return models
|
|
.filter((m) => m.allowed_roles && m.allowed_roles.includes(role))
|
|
.map((m) => {
|
|
// Use model name if available, otherwise fall back to model ID
|
|
const displayName = m.name || m.id;
|
|
return {
|
|
name: `${provider} / ${displayName} ${
|
|
m.cost_per_1m_tokens
|
|
? chalk.gray(
|
|
`($${m.cost_per_1m_tokens.input.toFixed(2)} input | $${m.cost_per_1m_tokens.output.toFixed(2)} output)`
|
|
)
|
|
: ''
|
|
}`,
|
|
value: { id: m.id, provider },
|
|
short: `${provider}/${displayName}`
|
|
};
|
|
});
|
|
})
|
|
.filter((choice) => choice !== null);
|
|
|
|
// Find current model index
|
|
let currentChoiceIndex = -1;
|
|
if (currentModel?.modelId && currentModel?.provider) {
|
|
currentChoiceIndex = roleChoices.findIndex(
|
|
(choice) =>
|
|
typeof choice.value === 'object' &&
|
|
choice.value !== null &&
|
|
'id' in choice.value &&
|
|
choice.value.id === currentModel.modelId &&
|
|
choice.value.provider === currentModel.provider
|
|
);
|
|
}
|
|
|
|
// Get custom provider options
|
|
const customProviderOptions = getCustomProviderOptions();
|
|
|
|
// Build final choices array
|
|
const systemLength = systemOptions.length;
|
|
let choices: (ModelChoice | Separator)[];
|
|
let defaultIndex: number;
|
|
|
|
if (allowNone) {
|
|
choices = [
|
|
...systemOptions,
|
|
new Separator('\n── Standard Models ──'),
|
|
{ name: '⚪ None (disable)', value: null, short: 'None' },
|
|
...roleChoices,
|
|
new Separator('\n── Custom Providers ──'),
|
|
...customProviderOptions
|
|
];
|
|
const noneOptionIndex = systemLength + 1;
|
|
defaultIndex =
|
|
currentChoiceIndex !== -1
|
|
? currentChoiceIndex + systemLength + 2
|
|
: noneOptionIndex;
|
|
} else {
|
|
choices = [
|
|
...systemOptions,
|
|
new Separator('\n── Standard Models ──'),
|
|
...roleChoices,
|
|
new Separator('\n── Custom Providers ──'),
|
|
...customProviderOptions
|
|
];
|
|
defaultIndex =
|
|
currentChoiceIndex !== -1
|
|
? currentChoiceIndex + systemLength + 1
|
|
: noChangeOption
|
|
? 1
|
|
: 0;
|
|
}
|
|
|
|
// Ensure defaultIndex is valid
|
|
if (defaultIndex < 0 || defaultIndex >= choices.length) {
|
|
defaultIndex = 0;
|
|
console.warn(
|
|
`Warning: Could not determine default model for role '${role}'. Defaulting to 'Cancel'.`
|
|
);
|
|
}
|
|
|
|
return { choices, default: defaultIndex };
|
|
}
|
|
|
|
/**
|
|
* Create search source for inquirer search prompt
|
|
*/
|
|
export function createSearchSource(
|
|
choices: (ModelChoice | Separator)[],
|
|
_defaultValue: number
|
|
) {
|
|
return (searchTerm = '') => {
|
|
const filteredChoices = choices.filter((choice) => {
|
|
// Separators are always included
|
|
if (choice instanceof Separator) return true;
|
|
// Filter regular choices by search term (name and model ID)
|
|
const mc = choice as ModelChoice;
|
|
const displayText = mc.name || '';
|
|
const modelId =
|
|
typeof mc.value === 'object' && mc.value !== null && 'id' in mc.value
|
|
? mc.value.id
|
|
: '';
|
|
const searchText = `${displayText} ${modelId}`.toLowerCase();
|
|
return searchText.includes(searchTerm.toLowerCase());
|
|
});
|
|
// Map ModelChoice to the format inquirer expects
|
|
return Promise.resolve(
|
|
filteredChoices.map((choice) => {
|
|
if (choice instanceof Separator) return choice;
|
|
const mc = choice as ModelChoice;
|
|
return {
|
|
name: mc.name,
|
|
value: mc.value,
|
|
short: mc.short
|
|
};
|
|
})
|
|
);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Display introductory message for interactive setup
|
|
*/
|
|
export function displaySetupIntro(): void {
|
|
console.log(chalk.cyan('\n🎯 Interactive Model Setup'));
|
|
console.log(chalk.gray('━'.repeat(50)));
|
|
console.log(chalk.yellow('💡 Navigation tips:'));
|
|
console.log(chalk.gray(' • Type to search and filter options'));
|
|
console.log(chalk.gray(' • Use ↑↓ arrow keys to navigate results'));
|
|
console.log(
|
|
chalk.gray(
|
|
' • Standard models are listed first, custom providers at bottom'
|
|
)
|
|
);
|
|
console.log(chalk.gray(' • Press Enter to select\n'));
|
|
}
|
|
|
|
/**
|
|
* Prompt user to select a model for a specific role
|
|
*/
|
|
export async function promptForModel(
|
|
role: ModelRole,
|
|
promptData: PromptData
|
|
): Promise<string | { id: string; provider: string } | null> {
|
|
const roleLabels = {
|
|
main: 'main model for generation/updates',
|
|
research: 'research model',
|
|
fallback: 'fallback model (optional)'
|
|
};
|
|
|
|
const answer = await search({
|
|
message: `Select the ${roleLabels[role]}:`,
|
|
source: createSearchSource(promptData.choices, promptData.default),
|
|
pageSize: 15
|
|
});
|
|
|
|
return answer;
|
|
}
|