History

fix(mobile): show OpenCode model sources in picker (#8573)

Julius Marminge committed 2 months ago

Commit acb599d1 parent f94a0d65 files · +97 −18Browse files

Unified · Split

5 files
  1. apps/mobile/src/features/threads/ThreadSettingsSheet.tsx +25 −14
  2. apps/mobile/src/features/threads/thread-settings-sheet-state.test.ts +16 −1
  3. apps/mobile/src/lib/modelOptions.test.ts +50 −1
  4. apps/mobile/src/lib/modelOptions.ts +2 −2
  5. docs/user/composer.md +4 −0
apps/mobile/src/features/threads/ThreadSettingsSheet.tsx+25−14
@@ -93,7 +93,7 @@ function ModelRow(props: { · 92 unmodified lines
9393 }) {
9494 return (
9595 <Pressable
96− accessibilityLabel={props.option.label}
96+ accessibilityLabel={[props.option.label, props.option.subtitle].filter(Boolean).join(", ")}
9797 accessibilityRole="radio"
9898 accessibilityState={{ checked: props.selected }}
9999 onPress={props.onPress}
@@ -103,20 +103,31 @@ function ModelRow(props: { · 3 unmodified lines
103103 props.isLast ? "rounded-b-2xl" : "border-b border-border-subtle",
104104 )}
105105 >
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}
117124 </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>
120131 {props.selected ? (
121132 <SymbolView
122133 name="checkmark"
apps/mobile/src/features/threads/thread-settings-sheet-state.test.ts+16−1
@@ -12,7 +12,7 @@ function modelOption( · 11 unmodified lines
1212 return {
1313 key: `codex:${model}`,
1414 label: model,
15− subtitle: "Codex",
15+ subtitle: "",
1616 providerKey: "codex",
1717 providerLabel: "Codex",
1818 providerDriver: "codex",
@@ -48,6 +48,21 @@ describe("thread settings sheet state", () => { · 29 unmodified lines
4848 ).toBe(true);
4949 });
5050
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+
5166 it("clears staging when the applied model is pressed", () => {
5267 expect(
5368 pendingModelAfterPress({
apps/mobile/src/lib/modelOptions.test.ts+50−1
@@ -44,13 +44,62 @@ describe("mobile model options", () => { · 43 unmodified lines
4444 providerKey: "codex",
4545 providerLabel: "Codex",
4646 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 },
4848 { key: "codex:gpt-5.4", label: "GPT-5.4", isLegacy: true },
4949 ],
5050 },
5151 ]);
5252 });
5353
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+
54103 it("normalizes a legacy fallback selection against current capabilities", () => {
55104 const config = {
56105 providers: [
apps/mobile/src/lib/modelOptions.ts+2−2
@@ -121,7 +121,7 @@ export function buildModelOptions( · 120 unmodified lines
121121 options.set(key, {
122122 key,
123123 label: model.name,
124− subtitle: providerLabel,
124+ subtitle: model.subProvider ?? "",
125125 providerKey: provider.instanceId,
126126 providerLabel,
127127 providerDriver: provider.driver,
@@ -152,7 +152,7 @@ export function buildModelOptions( · 24 unmodified lines
152152 options.set(key, {
153153 key,
154154 label: fallbackModelSelection.model,
155− subtitle: providerLabel,
155+ subtitle: "",
156156 providerKey: fallbackModelSelection.instanceId,
157157 providerLabel,
158158 providerDriver: fallbackModelSelection.instanceId,
docs/user/composer.md+4−0
@@ -10,6 +10,10 @@ becomes available after every upload finishes. Failed uploads can be retried or · 9 unmodified lines
1010 On web and desktop, HEIC and HEIF photos are automatically converted to JPEG when you drag them into
1111 the composer or paste them into a message.
1212
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+
1317 ## Commands and skills
1418
1519 Type `/` to open the command menu. Type `$` to find and add a skill. Skill rows show their source,