| author | |
| committer | |
| log | acb599d2dc5b28d5bad82fa2a280ac2abe771ec9 |
| tree | 6708b80121e5149916b33ca864a28dc8315cba65 |
| parent | f94a0d646ed78a4788e4af6417f74202a628a5e9 |
| signature | Signed by PGP key B5690EEEBB952194 |
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 | 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: { |
| 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" |
apps/mobile/src/features/threads/thread-settings-sheet-state.test.ts+16-1| ... | ... | @@ -12,7 +12,7 @@ function modelOption( |
| 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", () => { |
| 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({ |
apps/mobile/src/lib/modelOptions.test.ts+50-1| ... | ... | @@ -44,13 +44,62 @@ describe("mobile model options", () => { |
| 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: [ |
apps/mobile/src/lib/modelOptions.ts+2-2| ... | ... | @@ -121,7 +121,7 @@ export function buildModelOptions( |
| 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( |
| 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, |
docs/user/composer.md+4| ... | ... | @@ -10,6 +10,10 @@ becomes available after every upload finishes. Failed uploads can be retried or |
| 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, |