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
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: { · 3 unmodified lines
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( · 11 unmodified lines
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", () => { · 29 unmodified lines
48 ).toBe(true);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 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", () => { · 43 unmodified lines
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 });
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 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( · 120 unmodified lines
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( · 24 unmodified lines
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−0
@@ -10,6 +10,10 @@ becomes available after every upload finishes. Failed uploads can be retried or · 9 unmodified lines
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.
12 12
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
14 18
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,