authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-08-28 12:37:15-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-28 12:37:15-07:00
logacb599d2dc5b28d5bad82fa2a280ac2abe771ec9
tree6708b80121e5149916b33ca864a28dc8315cba65
parentf94a0d646ed78a4788e4af6417f74202a628a5e9
signature Signed by PGP key B5690EEEBB952194

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

Co-authored-by: Julius Marminge <julius@mac.lan>

5 files changed, 97 insertions(+), 18 deletions(-)

apps/mobile/src/features/threads/ThreadSettingsSheet.tsx+25-14
......@@ -93,7 +93,7 @@ function ModelRow(props: {
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: {
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(
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", () => {
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", () => {
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(
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(
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
......@@ -10,6 +10,10 @@ becomes available after every upload finishes. Failed uploads can be retried or
1010On web and desktop, HEIC and HEIF photos are automatically converted to JPEG when you drag them into
1111the composer or paste them into a message.
1212
13On mobile, the model picker shows each OpenCode model's upstream provider, such as Anthropic,
14GitHub Copilot, or OpenCode Zen, beneath its name. Search by that provider name to narrow the list
15when starting a thread or changing an existing thread's model.
16
1317## Commands and skills
1418
1519Type `/` to open the command menu. Type `$` to find and add a skill. Skill rows show their source,