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: {...@@ -93,7 +93,7 @@ function ModelRow(props: {
93}) {93}) {
94 return (94 return (
95 <Pressable95 <Pressable
96 accessibilityLabel={props.option.label}96 accessibilityLabel={[props.option.label, props.option.subtitle].filter(Boolean).join(", ")}
97 accessibilityRole="radio"97 accessibilityRole="radio"
98 accessibilityState={{ checked: props.selected }}98 accessibilityState={{ checked: props.selected }}
99 onPress={props.onPress}99 onPress={props.onPress}
...@@ -103,20 +103,31 @@ function ModelRow(props: {...@@ -103,20 +103,31 @@ function ModelRow(props: {
103 props.isLast ? "rounded-b-2xl" : "border-b border-border-subtle",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}>106 <View className="min-w-0 flex-1">
107 {props.option.label}107 <View className="flex-row items-center gap-2">
108 </Text>108 <Text
109 {props.option.isDefault ? (109 className="min-w-0 shrink text-base font-t3-medium text-foreground"
110 <View className="rounded-md bg-subtle-strong px-1.5 py-0.5">110 numberOfLines={1}
111 <Text className="text-3xs font-t3-bold text-foreground-muted">Default</Text>111 >
112 </View>112 {props.option.label}
113 ) : null}113 </Text>
114 {props.option.isLegacy ? (114 {props.option.isDefault ? (
115 <View className="rounded-md bg-subtle px-1.5 py-0.5">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">Legacy</Text>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 </View>124 </View>
118 ) : null}125 {props.option.subtitle ? (
119 <View className="flex-1" />126 <Text className="text-xs text-foreground-muted" numberOfLines={1}>
127 {props.option.subtitle}
128 </Text>
129 ) : null}
130 </View>
120 {props.selected ? (131 {props.selected ? (
121 <SymbolView132 <SymbolView
122 name="checkmark"133 name="checkmark"
apps/mobile/src/features/threads/thread-settings-sheet-state.test.ts+16-1
...@@ -12,7 +12,7 @@ function modelOption(...@@ -12,7 +12,7 @@ function modelOption(
12 return {12 return {
13 key: `codex:${model}`,13 key: `codex:${model}`,
14 label: model,14 label: model,
15 subtitle: "Codex",15 subtitle: "",
16 providerKey: "codex",16 providerKey: "codex",
17 providerLabel: "Codex",17 providerLabel: "Codex",
18 providerDriver: "codex",18 providerDriver: "codex",
...@@ -48,6 +48,21 @@ describe("thread settings sheet state", () => {...@@ -48,6 +48,21 @@ describe("thread settings sheet state", () => {
48 ).toBe(true);48 ).toBe(true);
49 });49 });
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
51 it("clears staging when the applied model is pressed", () => {66 it("clears staging when the applied model is pressed", () => {
52 expect(67 expect(
53 pendingModelAfterPress({68 pendingModelAfterPress({
apps/mobile/src/lib/modelOptions.test.ts+50-1
...@@ -44,13 +44,62 @@ describe("mobile model options", () => {...@@ -44,13 +44,62 @@ describe("mobile model options", () => {
44 providerKey: "codex",44 providerKey: "codex",
45 providerLabel: "Codex",45 providerLabel: "Codex",
46 models: [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 { key: "codex:gpt-5.4", label: "GPT-5.4", isLegacy: true },48 { key: "codex:gpt-5.4", label: "GPT-5.4", isLegacy: true },
49 ],49 ],
50 },50 },
51 ]);51 ]);
52 });52 });
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
54 it("normalizes a legacy fallback selection against current capabilities", () => {103 it("normalizes a legacy fallback selection against current capabilities", () => {
55 const config = {104 const config = {
56 providers: [105 providers: [
apps/mobile/src/lib/modelOptions.ts+2-2
...@@ -121,7 +121,7 @@ export function buildModelOptions(...@@ -121,7 +121,7 @@ export function buildModelOptions(
121 options.set(key, {121 options.set(key, {
122 key,122 key,
123 label: model.name,123 label: model.name,
124 subtitle: providerLabel,124 subtitle: model.subProvider ?? "",
125 providerKey: provider.instanceId,125 providerKey: provider.instanceId,
126 providerLabel,126 providerLabel,
127 providerDriver: provider.driver,127 providerDriver: provider.driver,
...@@ -152,7 +152,7 @@ export function buildModelOptions(...@@ -152,7 +152,7 @@ export function buildModelOptions(
152 options.set(key, {152 options.set(key, {
153 key,153 key,
154 label: fallbackModelSelection.model,154 label: fallbackModelSelection.model,
155 subtitle: providerLabel,155 subtitle: "",
156 providerKey: fallbackModelSelection.instanceId,156 providerKey: fallbackModelSelection.instanceId,
157 providerLabel,157 providerLabel,
158 providerDriver: fallbackModelSelection.instanceId,158 providerDriver: fallbackModelSelection.instanceId,
docs/user/composer.md+4
...@@ -10,6 +10,10 @@ becomes available after every upload finishes. Failed uploads can be retried or...@@ -10,6 +10,10 @@ becomes available after every upload finishes. Failed uploads can be retried or
10On web and desktop, HEIC and HEIF photos are automatically converted to JPEG when you drag them into10On web and desktop, HEIC and HEIF photos are automatically converted to JPEG when you drag them into
11the composer or paste them into a message.11the 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
13## Commands and skills17## Commands and skills
1418
15Type `/` to open the command menu. Type `$` to find and add a skill. Skill rows show their source,19Type `/` to open the command menu. Type `$` to find and add a skill. Skill rows show their source,