Unified · Split
| @@ -93,7 +93,7 @@ function ModelRow(props: { · 92 unmodified lines | ||
| 93 | 93 | }) { |
| 94 | 94 | return ( |
| 95 | 95 | <Pressable |
| 96 | − accessibilityLabel={props.option.label} | |
| 96 | + accessibilityLabel={[props.option.label, props.option.subtitle].filter(Boolean).join(", ")} | |
| 97 | 97 | accessibilityRole="radio" |
| 98 | 98 | accessibilityState={{ checked: props.selected }} |
| 99 | 99 | onPress={props.onPress} |
| @@ -103,20 +103,31 @@ function ModelRow(props: { · 3 unmodified lines | ||
| 103 | 103 | props.isLast ? "rounded-b-2xl" : "border-b border-border-subtle", |
| 104 | 104 | )} |
| 105 | 105 | > |
| 106 | − <Text className="min-w-0 shrink text-base font-t3-medium text-foreground" numberOfLines={1}> | |
| 107 | − {props.option.label} | |
| 108 | − </Text> | |
| 109 | − {props.option.isDefault ? ( | |
| 110 | − <View className="rounded-md bg-subtle-strong px-1.5 py-0.5"> | |
| 111 | − <Text className="text-3xs font-t3-bold text-foreground-muted">Default</Text> | |
| 112 | − </View> | |
| 113 | − ) : null} | |
| 114 | − {props.option.isLegacy ? ( | |
| 115 | − <View className="rounded-md bg-subtle px-1.5 py-0.5"> | |
| 116 | − <Text className="text-3xs font-t3-bold text-foreground-muted">Legacy</Text> | |
| 106 | + <View className="min-w-0 flex-1"> | |
| 107 | + <View className="flex-row items-center gap-2"> | |
| 108 | + <Text | |
| 109 | + className="min-w-0 shrink text-base font-t3-medium text-foreground" | |
| 110 | + numberOfLines={1} | |
| 111 | + > | |
| 112 | + {props.option.label} | |
| 113 | + </Text> | |
| 114 | + {props.option.isDefault ? ( | |
| 115 | + <View className="rounded-md bg-subtle-strong px-1.5 py-0.5"> | |
| 116 | + <Text className="text-3xs font-t3-bold text-foreground-muted">Default</Text> | |
| 117 | + </View> | |
| 118 | + ) : null} | |
| 119 | + {props.option.isLegacy ? ( | |
| 120 | + <View className="rounded-md bg-subtle px-1.5 py-0.5"> | |
| 121 | + <Text className="text-3xs font-t3-bold text-foreground-muted">Legacy</Text> | |
| 122 | + </View> | |
| 123 | + ) : null} | |
| 117 | 124 | </View> |
| 118 | − ) : null} | |
| 119 | − <View className="flex-1" /> | |
| 125 | + {props.option.subtitle ? ( | |
| 126 | + <Text className="text-xs text-foreground-muted" numberOfLines={1}> | |
| 127 | + {props.option.subtitle} | |
| 128 | + </Text> | |
| 129 | + ) : null} | |
| 130 | + </View> | |
| 120 | 131 | {props.selected ? ( |
| 121 | 132 | <SymbolView |
| 122 | 133 | name="checkmark" |
| @@ -12,7 +12,7 @@ function modelOption( · 11 unmodified lines | ||
| 12 | 12 | return { |
| 13 | 13 | key: `codex:${model}`, |
| 14 | 14 | label: model, |
| 15 | − subtitle: "Codex", | |
| 15 | + subtitle: "", | |
| 16 | 16 | providerKey: "codex", |
| 17 | 17 | providerLabel: "Codex", |
| 18 | 18 | providerDriver: "codex", |
| @@ -48,6 +48,21 @@ describe("thread settings sheet state", () => { · 29 unmodified lines | ||
| 48 | 48 | ).toBe(true); |
| 49 | 49 | }); |
| 50 | 50 | |
| 51 | + it("matches the upstream provider's display name", () => { | |
| 52 | + const model = { | |
| 53 | + ...modelOption("opencode/claude-fable-5"), | |
| 54 | + label: "Claude Fable 5", | |
| 55 | + subtitle: "OpenCode Zen", | |
| 56 | + }; | |
| 57 | + | |
| 58 | + expect(modelMatchesCatalogQuery({ model, providerLabel: "OpenCode", query: " ZEN " })).toBe( | |
| 59 | + true, | |
| 60 | + ); | |
| 61 | + expect(modelMatchesCatalogQuery({ model, providerLabel: "OpenCode", query: "copilot" })).toBe( | |
| 62 | + false, | |
| 63 | + ); | |
| 64 | + }); | |
| 65 | + | |
| 51 | 66 | it("clears staging when the applied model is pressed", () => { |
| 52 | 67 | expect( |
| 53 | 68 | pendingModelAfterPress({ |
| @@ -44,13 +44,62 @@ describe("mobile model options", () => { · 43 unmodified lines | ||
| 44 | 44 | providerKey: "codex", |
| 45 | 45 | providerLabel: "Codex", |
| 46 | 46 | models: [ |
| 47 | − { key: "codex:gpt-5.6-sol", label: "GPT-5.6 Sol", isLegacy: false }, | |
| 47 | + { key: "codex:gpt-5.6-sol", label: "GPT-5.6 Sol", subtitle: "", isLegacy: false }, | |
| 48 | 48 | { key: "codex:gpt-5.4", label: "GPT-5.4", isLegacy: true }, |
| 49 | 49 | ], |
| 50 | 50 | }, |
| 51 | 51 | ]); |
| 52 | 52 | }); |
| 53 | 53 | |
| 54 | + it("distinguishes same-name OpenCode models without changing their routing", () => { | |
| 55 | + const sources = [ | |
| 56 | + { id: "anthropic", label: "Anthropic" }, | |
| 57 | + { id: "github-copilot", label: "GitHub Copilot" }, | |
| 58 | + { id: "opencode", label: "OpenCode Zen" }, | |
| 59 | + ]; | |
| 60 | + const config = { | |
| 61 | + providers: [ | |
| 62 | + { | |
| 63 | + instanceId: "opencode_work", | |
| 64 | + driver: "opencode", | |
| 65 | + displayName: "OpenCode Work", | |
| 66 | + enabled: true, | |
| 67 | + installed: true, | |
| 68 | + auth: { status: "authenticated" }, | |
| 69 | + models: sources.map((source) => ({ | |
| 70 | + slug: `${source.id}/claude-fable-5`, | |
| 71 | + name: "Claude Fable 5", | |
| 72 | + subProvider: source.label, | |
| 73 | + isCustom: false, | |
| 74 | + capabilities: null, | |
| 75 | + })), | |
| 76 | + }, | |
| 77 | + ], | |
| 78 | + } as unknown as ServerConfig; | |
| 79 | + const selection = { | |
| 80 | + instanceId: ProviderInstanceId.make("opencode_work"), | |
| 81 | + model: "github-copilot/claude-fable-5", | |
| 82 | + }; | |
| 83 | + | |
| 84 | + const options = buildModelOptions(config, selection); | |
| 85 | + | |
| 86 | + expect(options).toMatchObject( | |
| 87 | + sources.map((source) => ({ | |
| 88 | + key: `opencode_work:${source.id}/claude-fable-5`, | |
| 89 | + label: "Claude Fable 5", | |
| 90 | + subtitle: source.label, | |
| 91 | + providerLabel: "OpenCode Work", | |
| 92 | + selection: { | |
| 93 | + instanceId: "opencode_work", | |
| 94 | + model: `${source.id}/claude-fable-5`, | |
| 95 | + }, | |
| 96 | + })), | |
| 97 | + ); | |
| 98 | + expect(groupByProvider(options)).toEqual([ | |
| 99 | + { providerKey: "opencode_work", providerLabel: "OpenCode Work", models: options }, | |
| 100 | + ]); | |
| 101 | + }); | |
| 102 | + | |
| 54 | 103 | it("normalizes a legacy fallback selection against current capabilities", () => { |
| 55 | 104 | const config = { |
| 56 | 105 | providers: [ |
| @@ -121,7 +121,7 @@ export function buildModelOptions( · 120 unmodified lines | ||
| 121 | 121 | options.set(key, { |
| 122 | 122 | key, |
| 123 | 123 | label: model.name, |
| 124 | − subtitle: providerLabel, | |
| 124 | + subtitle: model.subProvider ?? "", | |
| 125 | 125 | providerKey: provider.instanceId, |
| 126 | 126 | providerLabel, |
| 127 | 127 | providerDriver: provider.driver, |
| @@ -152,7 +152,7 @@ export function buildModelOptions( · 24 unmodified lines | ||
| 152 | 152 | options.set(key, { |
| 153 | 153 | key, |
| 154 | 154 | label: fallbackModelSelection.model, |
| 155 | − subtitle: providerLabel, | |
| 155 | + subtitle: "", | |
| 156 | 156 | providerKey: fallbackModelSelection.instanceId, |
| 157 | 157 | providerLabel, |
| 158 | 158 | providerDriver: fallbackModelSelection.instanceId, |
| @@ -10,6 +10,10 @@ becomes available after every upload finishes. Failed uploads can be retried or · 9 unmodified lines | ||
| 10 | 10 | On web and desktop, HEIC and HEIF photos are automatically converted to JPEG when you drag them into |
| 11 | 11 | the composer or paste them into a message. |
| 12 | 12 | |
| 13 | +On mobile, the model picker shows each OpenCode model's upstream provider, such as Anthropic, | |
| 14 | +GitHub Copilot, or OpenCode Zen, beneath its name. Search by that provider name to narrow the list | |
| 15 | +when starting a thread or changing an existing thread's model. | |
| 16 | + | |
| 13 | 17 | ## Commands and skills |
| 14 | 18 | |
| 15 | 19 | Type `/` to open the command menu. Type `$` to find and add a skill. Skill rows show their source, |