| import { clsx, type ClassValue } from 'clsx'; |
| import { twMerge } from 'tailwind-merge'; |
| import type { ModelAlias, ModelProvider } from '@/store/app-store'; |
| import { |
| normalizeThinkingLevelForModel, |
| normalizeReasoningEffortForModel, |
| LEGACY_CLAUDE_ALIAS_MAP, |
| type PhaseModelEntry, |
| } from '@automaker/types'; |
|
|
| export function cn(...inputs: ClassValue[]) { |
| return twMerge(clsx(inputs)); |
| } |
|
|
| |
| |
| |
| |
| export { getErrorMessage } from '@automaker/utils/error-handler'; |
|
|
| |
| |
| |
| |
| export function migrateModelId(modelId: string | undefined): string | undefined { |
| if (!modelId) return modelId; |
| return LEGACY_CLAUDE_ALIAS_MAP[modelId as keyof typeof LEGACY_CLAUDE_ALIAS_MAP] || modelId; |
| } |
|
|
| |
| |
| |
| |
| export function normalizeModelEntry(entry: PhaseModelEntry): PhaseModelEntry { |
| const model = entry.model; |
|
|
| return { |
| model, |
| providerId: entry.providerId, |
| thinkingLevel: normalizeThinkingLevelForModel(model, entry.thinkingLevel), |
| reasoningEffort: normalizeReasoningEffortForModel(model, entry.reasoningEffort), |
| }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function modelSupportsThinking(_model?: ModelAlias | string): boolean { |
| if (!_model) return true; |
|
|
| |
| if (_model.startsWith('cursor-')) { |
| return false; |
| } |
|
|
| |
| if (_model.startsWith('codex-')) { |
| return false; |
| } |
|
|
| |
| if (_model.startsWith('gpt-')) { |
| return false; |
| } |
|
|
| |
| return true; |
| } |
|
|
| |
| |
| |
| |
| export function getProviderFromModel(model?: string): ModelProvider { |
| if (!model) return 'claude'; |
|
|
| |
| if (model.startsWith('cursor-') || model.startsWith('cursor:')) { |
| return 'cursor'; |
| } |
|
|
| |
| if ( |
| model.startsWith('codex-') || |
| model.startsWith('codex:') || |
| model.startsWith('gpt-') || |
| /^o\d/.test(model) |
| ) { |
| return 'codex'; |
| } |
|
|
| |
| return 'claude'; |
| } |
|
|
| |
| |
| |
| |
| export function getModelDisplayName(model: ModelAlias | string): string { |
| const displayNames: Record<string, string> = { |
| |
| haiku: 'Claude Haiku', |
| sonnet: 'Claude Sonnet', |
| opus: 'Claude Opus', |
| |
| 'claude-haiku': 'Claude Haiku', |
| 'claude-sonnet': 'Claude Sonnet', |
| 'claude-opus': 'Claude Opus', |
| |
| 'claude-haiku-4-5': 'Claude Haiku', |
| 'claude-sonnet-4-20250514': 'Claude Sonnet', |
| 'claude-opus-4-6': 'Claude Opus', |
| |
| 'codex-gpt-5.2': 'GPT-5.2', |
| 'codex-gpt-5.1-codex-max': 'GPT-5.1 Codex Max', |
| 'codex-gpt-5.1-codex': 'GPT-5.1 Codex', |
| 'codex-gpt-5.1-codex-mini': 'GPT-5.1 Codex Mini', |
| 'codex-gpt-5.1': 'GPT-5.1', |
| |
| 'cursor-auto': 'Cursor Auto', |
| 'cursor-composer-1': 'Composer 1', |
| 'cursor-gpt-5.2': 'GPT-5.2', |
| 'cursor-gpt-5.1': 'GPT-5.1', |
| }; |
| return displayNames[model] || model; |
| } |
|
|
| |
| |
| |
| export function truncateDescription(description: string, maxLength = 50): string { |
| if (description.length <= maxLength) { |
| return description; |
| } |
| return `${description.slice(0, maxLength)}...`; |
| } |
|
|
| |
| |
| |
| |
| export function normalizePath(p: string): string { |
| return p.replace(/\\/g, '/'); |
| } |
|
|
| |
| |
| |
| |
| export function pathsEqual(p1: string | undefined | null, p2: string | undefined | null): boolean { |
| if (!p1 || !p2) return p1 === p2; |
| return normalizePath(p1) === normalizePath(p2); |
| } |
|
|
| |
| |
| |
| |
| export const isMac = |
| typeof process !== 'undefined' && process.platform === 'darwin' |
| ? true |
| : typeof navigator !== 'undefined' && |
| (/Mac/.test(navigator.userAgent) || |
| (navigator.platform ? navigator.platform.toLowerCase().includes('mac') : false)); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function sanitizeForTestId(name: string): string { |
| return name |
| .toLowerCase() |
| .replace(/\s+/g, '-') |
| .replace(/[^a-z0-9-]/g, '') |
| .replace(/-+/g, '-') |
| .replace(/^-|-$/g, ''); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function generateUUID(): string { |
| if (typeof crypto === 'undefined' || typeof crypto.getRandomValues === 'undefined') { |
| throw new Error('Cryptographically secure random number generator not available.'); |
| } |
| const bytes = new Uint8Array(16); |
| crypto.getRandomValues(bytes); |
|
|
| |
| bytes[6] = (bytes[6] & 0x0f) | 0x40; |
| bytes[8] = (bytes[8] & 0x3f) | 0x80; |
|
|
| |
| const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join(''); |
| return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; |
| } |
|
|
| |
| |
| |
| export function formatRelativeTime(date: Date): string { |
| const now = new Date(); |
| const diffMs = now.getTime() - date.getTime(); |
| const diffSec = Math.floor(diffMs / 1000); |
|
|
| if (diffSec < 0) return date.toLocaleDateString(); |
|
|
| const diffMin = Math.floor(diffSec / 60); |
| const diffHour = Math.floor(diffMin / 60); |
| const diffDay = Math.floor(diffHour / 24); |
|
|
| if (diffSec < 60) return 'just now'; |
| if (diffMin < 60) return `${diffMin} minute${diffMin === 1 ? '' : 's'} ago`; |
| if (diffHour < 24) return `${diffHour} hour${diffHour === 1 ? '' : 's'} ago`; |
| if (diffDay < 7) return `${diffDay} day${diffDay === 1 ? '' : 's'} ago`; |
| return date.toLocaleDateString(); |
| } |
|
|