feat: add search field to model selector on both tabs (#649)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Charles Packer
2026-01-22 18:09:57 -08:00
committed by GitHub
parent e735bb7c66
commit e782af748b
4 changed files with 67 additions and 12 deletions

View File

@@ -69,16 +69,62 @@ export function getModelUpdateArgs(
return modelInfo?.updateArgs;
}
/**
* Find a model entry by handle with fuzzy matching support
* @param handle - The full model handle
* @returns The model entry if found, null otherwise
*/
function findModelByHandle(handle: string): (typeof models)[number] | null {
// Try exact match first
const exactMatch = models.find((m) => m.handle === handle);
if (exactMatch) return exactMatch;
// For handles like "bedrock/claude-opus-4-5-20251101" where the API returns without
// vendor prefix or version suffix, but models.json has
// "bedrock/us.anthropic.claude-opus-4-5-20251101-v1:0", try fuzzy matching
const [provider, ...rest] = handle.split("/");
if (provider && rest.length > 0) {
const modelPortion = rest.join("/");
// Find models with the same provider where the model portion is contained
// in the models.json handle (handles vendor prefixes and version suffixes)
const partialMatch = models.find((m) => {
if (!m.handle.startsWith(`${provider}/`)) return false;
const mModelPortion = m.handle.slice(provider.length + 1);
// Check if either contains the other (handles both directions)
return (
mModelPortion.includes(modelPortion) ||
modelPortion.includes(mModelPortion)
);
});
if (partialMatch) return partialMatch;
}
return null;
}
/**
* Get a display-friendly name for a model by its handle
* @param handle - The full model handle (e.g., "anthropic/claude-sonnet-4-5-20250929")
* @returns The display name (e.g., "Sonnet 4.5") if found, null otherwise
*/
export function getModelDisplayName(handle: string): string | null {
const model = models.find((m) => m.handle === handle);
const model = findModelByHandle(handle);
return model?.label ?? null;
}
/**
* Get a short display name for a model (for status bar)
* Falls back to full label if no shortLabel is defined
* @param handle - The full model handle
* @returns The short name (e.g., "Opus 4.5 BR") if found, null otherwise
*/
export function getModelShortName(handle: string): string | null {
const model = findModelByHandle(handle);
if (!model) return null;
// Use shortLabel if available, otherwise fall back to label
return (model as { shortLabel?: string }).shortLabel ?? model.label;
}
/**
* Resolve a model ID from the llm_config.model value
* The llm_config.model is the model portion without the provider prefix

View File

@@ -43,7 +43,7 @@ import { getCurrentAgentId, setCurrentAgentId } from "../agent/context";
import { type AgentProvenance, createAgent } from "../agent/create";
import { ISOLATED_BLOCK_LABELS } from "../agent/memory";
import { sendMessageStream } from "../agent/message";
import { getModelDisplayName, getModelInfo } from "../agent/model";
import { getModelInfo, getModelShortName } from "../agent/model";
import { SessionStats } from "../agent/stats";
import {
INTERRUPTED_BY_USER,
@@ -1047,7 +1047,7 @@ export default function App({
? `${llmConfig.model_endpoint_type}/${llmConfig.model}`
: (llmConfig?.model ?? null);
const currentModelDisplay = currentModelLabel
? (getModelDisplayName(currentModelLabel) ??
? (getModelShortName(currentModelLabel) ??
currentModelLabel.split("/").pop())
: null;
const currentModelProvider = llmConfig?.provider_name ?? null;

View File

@@ -129,10 +129,20 @@ export function ModelSelector({
m.handle.startsWith(`${filterProvider}/`),
);
}
// Apply search filter
if (searchQuery) {
const query = searchQuery.toLowerCase();
available = available.filter(
(m) =>
m.label.toLowerCase().includes(query) ||
m.description.toLowerCase().includes(query) ||
m.handle.toLowerCase().includes(query),
);
}
const featured = available.filter((m) => m.isFeatured);
const nonFeatured = available.filter((m) => !m.isFeatured);
return [...featured, ...nonFeatured];
}, [typedModels, availableHandles, filterProvider]);
}, [typedModels, availableHandles, filterProvider, searchQuery]);
// All other models: API handles not in models.json
const otherModelHandles = useMemo(() => {
@@ -158,8 +168,8 @@ export function ModelSelector({
}));
}, [category, supportedModels, otherModelHandles]);
// Show 1 fewer item in "all" category because Search line takes space
const visibleCount = category === "all" ? VISIBLE_ITEMS - 1 : VISIBLE_ITEMS;
// Show 1 fewer item because Search line takes space
const visibleCount = VISIBLE_ITEMS - 1;
// Scrolling - keep selectedIndex in view
const startIndex = useMemo(() => {
@@ -276,8 +286,8 @@ export function ModelSelector({
if (selectedModel) {
onSelect(selectedModel.id);
}
} else if (category === "all" && input && input.length === 1) {
// Capture text input for search (only in "all" category)
} else if (input && input.length === 1) {
// Capture text input for search
setSearchQuery((prev) => prev + input);
setSelectedIndex(0);
}
@@ -328,9 +338,7 @@ export function ModelSelector({
{!isLoading && !refreshing && (
<Box flexDirection="column" paddingLeft={1}>
{renderTabBar()}
{category === "all" && (
<Text dimColor> Search: {searchQuery || "(type to filter)"}</Text>
)}
<Text dimColor> Search: {searchQuery || "(type to filter)"}</Text>
</Box>
)}
</Box>

View File

@@ -38,7 +38,8 @@
{
"id": "bedrock-opus",
"handle": "bedrock/us.anthropic.claude-opus-4-5-20251101-v1:0",
"label": "Bedrock Claude Opus 4.5",
"label": "Bedrock Opus 4.5",
"shortLabel": "Opus 4.5 BR",
"description": "Anthropic's best model (via AWS Bedrock)",
"isFeatured": true,
"updateArgs": {