* Renders the current model display in the input area.
()
| 411 | const escapePricing = value => String(value || '').replace(/[&<>"']/g, character => ({ |
| 412 | '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' |
| 413 | }[character])); |
| 414 | const badgeClassName = 'text-xs px-2 py-1 rounded bg-muted text-muted-foreground font-medium flex-shrink-0 min-w-[34px] inline-flex items-center justify-center gap-1.5'; |
| 415 | const paymentBadge = pricing ? (pricing.balanceBadgeLabel ? ` |
| 416 | <span data-model-balance-badge class="${badgeClassName}" title="${escapePricing(pricing.balanceBadgeTooltip)}" aria-label="${escapePricing(pricing.balanceBadgeTooltip)}">${escapePricing(pricing.balanceBadgeLabel)}</span> |
| 417 | ` : '') : ` |
| 418 | <span class="${badgeClassName}" title="${ticketCost} ticket${ticketCost > 1 ? 's' : ''}"> |
| 419 | <svg class="w-3.5 h-3.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"> |
| 420 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 5v2m0 4v2m0 4v2M5 5a2 2 0 00-2 2v3a2 2 0 110 4v3a2 2 0 002 2h14a2 2 0 002-2v-3a2 2 0 110-4V7a2 2 0 00-2-2H5z"></path> |
| 421 | </svg> |
| 422 | <span>${ticketCost}</span> |
| 423 | </span> |
| 424 | `; |
| 425 | |
| 426 | return ` |
| 427 | <div class="model-option px-2 py-1.5 rounded-sm cursor-pointer transition-colors hover:bg-accent ${isSelected ? 'bg-accent' : ''}" data-model-name="${model.name}"> |
| 428 | <div class="flex items-center gap-2"> |
| 429 | <div class="flex items-center justify-center w-6 h-6 flex-shrink-0 rounded-full border border-border/50 ${bgClass}"> |
| 430 | ${iconData.html} |
| 431 | </div> |
| 432 | <div class="flex-1 min-w-0"> |
| 433 | <div class="font-medium text-sm text-foreground truncate">${model.name}</div> |
| 434 | ${pricing ? `<div class="mt-0.5 truncate text-[10px] text-muted-foreground" title="${escapePricing(pricing.description)}">${escapePricing(pricing.label)}</div>` : ''} |
| 435 | </div> |
| 436 | ${checkmarkSlot} |
| 437 | ${paymentBadge} |
| 438 | </div> |
| 439 | </div> |
| 440 | `; |
| 441 | } |
| 442 | |
| 443 | /** |
| 444 | * Selects a model by name and updates the session or stores as pending. |
| 445 | * @param {string} modelName - Display name chosen by the user |
| 446 | */ |
| 447 | async selectModel(modelName) { |
| 448 | if (this.isSecondarySelectionMode() && this.app.actions.selectCouncilSecondaryModel) { |
| 449 | await this.app.actions.selectCouncilSecondaryModel(modelName); |
| 450 | } else if (this.isSynthesisSelectionMode() && this.app.actions.selectCouncilSynthesisModel) { |
| 451 | await this.app.actions.selectCouncilSynthesisModel(modelName); |
| 452 | } else { |
| 453 | await this.app.actions.selectModel(modelName); |
| 454 | } |
| 455 | this.app.refreshEditModelPickerButton?.(); |
| 456 | this.close(); // close() handles input focus |
| 457 | } |
| 458 | |
| 459 | /** |
| 460 | * Checks if a model name/id exists in the available models list. |
| 461 | * @param {string} modelName - Model name or ID to check |
| 462 | * @returns {boolean} True if model exists in available models |
| 463 | */ |
| 464 | isModelAvailable(modelName) { |
| 465 | if (!modelName || !Array.isArray(this.app.state.models) || this.app.state.models.length === 0) { |
| 466 | return false; |
| 467 | } |
| 468 | // Check by name or id (imported chats may have model slugs or IDs) |
| 469 | return this.app.state.models.some( |
| 470 | m => (m.name === modelName || m.id === modelName) && !this.disabledModels.has(m.id) |
no test coverage detected