Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion apps/app/src/components/pickers/ModelReasoningPicker.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,17 +103,19 @@ const splitPaneContext: PaneContextValue = {
function availableModel({
value,
label,
description = "",
isDefault = false,
}: {
value: string;
label: string;
description?: string;
isDefault?: boolean;
}): AvailableModel {
return {
id: value,
model: value,
displayName: label,
description: "",
description,
supportedReasoningEfforts: [
{ reasoningEffort: "medium", description: "Medium" },
],
Expand Down Expand Up @@ -546,6 +548,43 @@ describe("ModelReasoningPicker", () => {
expect(onModelChange).toHaveBeenCalledWith(apiModel);
});

it("distinguishes duplicate preview models by description and preserves value", async () => {
const { onModelChange } = renderPicker({
alternateProviderModels: [
availableModel({
value: "zai/glm-4.7",
label: "GLM 4.7",
description: "zai/glm-4.7",
isDefault: true,
}),
availableModel({
value: "openrouter/glm-4.7",
label: "GLM 4.7",
description: "openrouter/glm-4.7",
}),
],
});
const trigger = screen.getByRole("button", {
name: "Provider, model and reasoning",
});
fireEvent.click(trigger);
fireEvent.click(screen.getByTitle("Claude Code"));

const zaiDescription = await screen.findByText("zai/glm-4.7");
const openRouterDescription = screen.getByText("openrouter/glm-4.7");
const zaiRow = zaiDescription.closest("button");
const openRouterRow = openRouterDescription.closest("button");
expect(zaiRow).not.toBeNull();
expect(openRouterRow).not.toBeNull();
expect(zaiRow).not.toBe(openRouterRow);
expect(zaiRow?.textContent).toContain("GLM 4.7");
expect(openRouterRow?.textContent).toContain("GLM 4.7");

fireEvent.click(openRouterDescription);

expect(onModelChange).toHaveBeenCalledWith("openrouter/glm-4.7");
});

it("fuzzy-filters a long model list and selects the match by keyboard", () => {
const { onModelChange } = renderPicker({ modelOptions: manyCodexModels });

Expand Down
31 changes: 21 additions & 10 deletions apps/app/src/components/pickers/ModelReasoningPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ export function ModelReasoningPicker({
...(model.routeProviderId
? { routeProviderId: model.routeProviderId }
: {}),
...(model.description ? { description: model.description } : {}),
}));
}, [isPreviewing, modelOptions, previewQuery.data?.models, formatModelLabel]);
const previewMoreModelOptions = useMemo((): readonly ModelPickerOption[] => {
Expand All @@ -405,6 +406,7 @@ export function ModelReasoningPicker({
...(model.routeProviderId
? { routeProviderId: model.routeProviderId }
: {}),
...(model.description ? { description: model.description } : {}),
}));
}, [
isPreviewing,
Expand Down Expand Up @@ -1045,6 +1047,7 @@ export function ModelReasoningPicker({
activeProviderId,
)}
qualifier={option.routeProviderId}
description={option.description}
selected={!isPreviewing && option.value === modelValue}
onClick={() => handleModelSelect(option.value)}
/>
Expand Down Expand Up @@ -1359,6 +1362,7 @@ function MoreModelsSubmenu({
key={option.value}
label={stripModelBrandPrefix(option.label, activeProviderId)}
qualifier={option.routeProviderId}
description={option.description}
selected={!isPreviewing && option.value === modelValue}
onClick={() => onSelect(option.value)}
/>
Expand Down Expand Up @@ -1386,6 +1390,7 @@ function ResetBrowseStateOnContentUnmount({
function MenuRowButton({
label,
qualifier,
description,
selected,
onClick,
isActive,
Expand All @@ -1396,6 +1401,7 @@ function MenuRowButton({
}: {
label: string;
qualifier?: string;
description?: string;
selected: boolean;
onClick: () => void;
isActive?: boolean;
Expand Down Expand Up @@ -1429,16 +1435,21 @@ function MenuRowButton({
)}
{...hoverProps}
>
<span
className="truncate"
title={qualifier ? `${label} · ${qualifier}` : label}
>
{base}
{tag ? (
<span className="ml-1.5 text-subtle-foreground">{tag}</span>
) : null}
{qualifier ? (
<span className="ml-1.5 text-subtle-foreground">{qualifier}</span>
<span className="flex min-w-0 flex-1 flex-col gap-0.5">
<span
className="truncate"
title={qualifier ? `${label} · ${qualifier}` : label}
>
{base}
{tag ? (
<span className="ml-1.5 text-subtle-foreground">{tag}</span>
) : null}
{qualifier ? (
<span className="ml-1.5 text-subtle-foreground">{qualifier}</span>
) : null}
</span>
{description ? (
<span className="truncate text-subtle-foreground">{description}</span>
) : null}
</span>
<span className="flex shrink-0 items-center gap-1.5">
Expand Down
2 changes: 2 additions & 0 deletions apps/app/src/hooks/useThreadCreationOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,7 @@ export function useThreadCreationOptions(
...(model.routeProviderId
? { routeProviderId: model.routeProviderId }
: {}),
...(model.description ? { description: model.description } : {}),
})),
[availableModels],
);
Expand All @@ -619,6 +620,7 @@ export function useThreadCreationOptions(
...(model.routeProviderId
? { routeProviderId: model.routeProviderId }
: {}),
...(model.description ? { description: model.description } : {}),
})),
[executionOptionsQuery.data?.selectedOnlyModels, availableModels],
);
Expand Down
23 changes: 23 additions & 0 deletions apps/mobile/src/data/compose/execution-options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,29 @@ describe("resolveModelSelection", () => {
expect(resolved.isRecovery).toBe(false);
});

it("carries the model description verbatim into the picker option", () => {
const resolved = resolveModelSelection({
executionOptions: response({
models: [
model({
model: "acme/glm-4.7",
displayName: "GLM 4.7",
description: "acme/glm-4.7",
}),
],
}),
selectedModel: "acme/glm-4.7",
catalogVerified: true,
});
expect(resolved.options).toEqual([
{
value: "acme/glm-4.7",
label: "GLM 4.7",
description: "acme/glm-4.7",
},
]);
});

it("promotes a retired-but-selected model instead of silently switching", () => {
const resolved = resolveModelSelection({
executionOptions: catalog,
Expand Down
41 changes: 41 additions & 0 deletions plugins/provider-acp/src/bridge/model-catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,47 @@ describe("acp configOptions model catalog", () => {
]);
});

it("keeps per-option descriptions that disambiguate duplicate model names", () => {
// OMP-style agents send description: "provider/modelId" on every model
// select option, and two providers can expose the same display name;
// the picker needs each option's description to tell them apart.
const models = buildModelCatalogFromConfigOptions({
id: "model",
category: "model",
type: "select",
options: [
{ value: "zai/glm-4.7", name: "GLM 4.7", description: "zai/glm-4.7" },
{
value: "openai/glm-4.7",
name: "GLM 4.7",
description: "openai/glm-4.7",
},
{ value: "opencode/big-pickle", name: "Big Pickle" },
],
});

expect(models).toMatchObject([
{
id: "zai/glm-4.7",
model: "zai/glm-4.7",
displayName: "GLM 4.7",
description: "zai/glm-4.7",
},
{
id: "openai/glm-4.7",
model: "openai/glm-4.7",
displayName: "GLM 4.7",
description: "openai/glm-4.7",
},
{
id: "opencode/big-pickle",
model: "opencode/big-pickle",
displayName: "Big Pickle",
description: "",
},
]);
});

it("finds and maps ACP thought_level config options", () => {
const thoughtLevel = {
id: "effort",
Expand Down
2 changes: 1 addition & 1 deletion plugins/provider-acp/src/bridge/model-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ export function buildModelCatalogFromConfigOptions(
id: option.value,
model: option.value,
displayName: option.name ?? option.value,
description: "",
description: option.description ?? "",
supportedReasoningEfforts: reasoning.supportedReasoningEfforts,
defaultReasoningEffort: reasoning.defaultReasoningEffort,
isDefault,
Expand Down
11 changes: 11 additions & 0 deletions plugins/provider-acp/src/wire.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ describe("acpSessionNewResultSchema", () => {
{
value: "openai-codex/gpt-5.5",
name: "openai-codex/GPT-5.5",
description: "openai-codex/gpt-5.5",
},
{
value: "zai/glm-4.7",
name: "GLM 4.7",
description: null,
},
],
Expand Down Expand Up @@ -74,6 +79,12 @@ describe("acpSessionNewResultSchema", () => {
);
expect(parsed.data.configOptions?.[1].category).toBeUndefined();
expect(parsed.data.configOptions?.[1].options?.[0].name).toBeUndefined();
expect(parsed.data.configOptions?.[0].options?.[0].description).toBe(
"openai-codex/gpt-5.5",
);
expect(
parsed.data.configOptions?.[0].options?.[1].description,
).toBeUndefined();
});
});

Expand Down
1 change: 1 addition & 0 deletions plugins/provider-acp/src/wire.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ const acpConfigOptionSelectOptionSchema = z
.object({
value: z.string(),
name: acpOptionalString,
description: acpOptionalString,
})
.passthrough();
export type AcpConfigOptionSelectOption = z.infer<
Expand Down