| 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,7 +93,7 @@ function ModelRow(props: { |
| 93 | }) { | 93 | }) { |
| 94 | return ( | 94 | return ( |
| 95 | <Pressable | 95 | <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 | <SymbolView | 132 | <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 | }); |
| 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", () => { | ... | @@ -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 | }); |
| 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( | ... | @@ -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 |
| 10 | On web and desktop, HEIC and HEIF photos are automatically converted to JPEG when you drag them into | 10 | On web and desktop, HEIC and HEIF photos are automatically converted to JPEG when you drag them into |
| 11 | the composer or paste them into a message. | 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 | ## Commands and skills | 17 | ## Commands and skills |
| 14 | 18 | ||
| 15 | Type `/` to open the command menu. Type `$` to find and add a skill. Skill rows show their source, | 19 | Type `/` to open the command menu. Type `$` to find and add a skill. Skill rows show their source, |