From ca02ee5633ce47895f767b24cd793a173f824f0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 23 Sep 2026 21:55:04 +0200 Subject: [PATCH 1/4] fix(mobile): correct Arabic RTL alignment and duplicate free badge https://github.com/Kilo-Org/cloud/pull/6636 --- .../src/app/(app)/agent-chat/repo-picker.tsx | 8 ++- .../model-picker-content.mounted.test.tsx | 1 + .../agents/model-picker-content.tsx | 8 +++ .../agents/model-selector.mounted.test.tsx | 62 ++++++++++++++++- .../src/components/agents/model-selector.tsx | 12 +++- .../agents/picker-search.mounted.test.tsx | 67 ++++++++++++++++++- .../src/components/language-picker-row.tsx | 19 ++++-- .../language-picker-sheet.mounted.test.tsx | 43 ++++++++++++ .../lib/free-model-data-disclosure.test.ts | 18 +++++ .../src/lib/free-model-data-disclosure.ts | 32 +++++++++ 10 files changed, 260 insertions(+), 10 deletions(-) diff --git a/apps/mobile/src/app/(app)/agent-chat/repo-picker.tsx b/apps/mobile/src/app/(app)/agent-chat/repo-picker.tsx index 0af4c0c161..486cf08012 100644 --- a/apps/mobile/src/app/(app)/agent-chat/repo-picker.tsx +++ b/apps/mobile/src/app/(app)/agent-chat/repo-picker.tsx @@ -12,6 +12,7 @@ import { useThemeColors } from '@/lib/hooks/use-theme-colors'; import { REPO_PLATFORM_LABEL_KEYS, type RepoOption } from '@/lib/picker-bridge'; import { repoPickerSlot, UNFENCED_ROUTE_KEY, useRouteRegistry } from '@/lib/route-registry'; import { filterRepoPickerOptions } from '@/lib/repo-picker-filter'; +import { withRtlInputAlignment } from '@/lib/rtl-text'; type PickerListItem = | { key: string; kind: 'header'; titleKey: string } @@ -123,7 +124,12 @@ export default function RepoPickerScreen() { returnKeyType="search" textAlignVertical="center" className="h-8 p-0 text-base leading-[normal] text-foreground" - style={{ color: colors.foreground }} + // `textAlign: 'auto'` resolves against the query's first strong + // character, so a Latin query stays at the field's left edge in an + // RTL interface while the clear and search controls sit at the + // right. Naming the alignment keeps the query on the same side as + // the controls. + style={withRtlInputAlignment({ color: colors.foreground })} onChangeText={setSearch} /> {search.length === 0 ? ( diff --git a/apps/mobile/src/components/agents/model-picker-content.mounted.test.tsx b/apps/mobile/src/components/agents/model-picker-content.mounted.test.tsx index e6d76d16f8..dee171b710 100644 --- a/apps/mobile/src/components/agents/model-picker-content.mounted.test.tsx +++ b/apps/mobile/src/components/agents/model-picker-content.mounted.test.tsx @@ -100,6 +100,7 @@ const flatListMock = vi.hoisted( vi.mock('react-native', () => ({ FlatList: flatListMock, + I18nManager: { isRTL: false }, Pressable: 'Pressable', TextInput: 'TextInput', View: 'View', diff --git a/apps/mobile/src/components/agents/model-picker-content.tsx b/apps/mobile/src/components/agents/model-picker-content.tsx index 2159b066ea..db125889cd 100644 --- a/apps/mobile/src/components/agents/model-picker-content.tsx +++ b/apps/mobile/src/components/agents/model-picker-content.tsx @@ -22,6 +22,7 @@ import { import { commitModelPickerSelection, resolveModelPickerSelection } from '@/lib/picker-bridge'; import { parseParam } from '@/lib/route-params'; import { modelPickerSlot, UNFENCED_ROUTE_KEY, useRouteRegistry } from '@/lib/route-registry'; +import { withRtlInputAlignment } from '@/lib/rtl-text'; export function ModelPickerContent() { const router = useRouter(); @@ -189,6 +190,13 @@ export function ModelPickerContent() { autoCorrect={false} returnKeyType="search" className="h-8 flex-1 p-0 text-base leading-[normal] text-foreground" + // `textAlign: 'auto'` resolves against the query's first strong + // character, so a Latin query and the native placeholder stay at + // the field's left edge in an RTL interface while the clear and + // search controls sit at the right, leaving a dead gap between + // them. Naming the alignment keeps both on the interface's start + // edge, as the repository picker and language search fields do. + style={withRtlInputAlignment(undefined)} onChangeText={setSearch} /> {/* In-field clear, on every platform: `clearButtonMode` is iOS diff --git a/apps/mobile/src/components/agents/model-selector.mounted.test.tsx b/apps/mobile/src/components/agents/model-selector.mounted.test.tsx index 82d6333cc8..ad1b2b044e 100644 --- a/apps/mobile/src/components/agents/model-selector.mounted.test.tsx +++ b/apps/mobile/src/components/agents/model-selector.mounted.test.tsx @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { TestRenderer } from '@/test/renderer'; @@ -7,6 +7,7 @@ import { freeModelDataLabel, freeModelFreeLabel, } from '@/lib/free-model-data-disclosure'; +import { type SessionModelOption } from '@/lib/hooks/use-session-model-options'; import { i18n } from '@/i18n'; import { @@ -62,6 +63,27 @@ vi.mock('@/lib/utils', () => ({ cn: (...parts: unknown[]) => parts.filter(Boolean).join(' '), })); +// Locale cases switch the shared instance; put English back so the remaining +// rows keep rendering the English catalog. +afterEach(async () => { + if (i18n.language !== 'en') { + await i18n.changeLanguage('en'); + } +}); + +function gatewayCatalogOption(overrides: Partial = {}): SessionModelOption { + return { + id: 'gateway-model-0', + name: 'Laguna S 2.1 (free)', + displayId: 'laguna/s-2.1', + variants: [], + isPreferred: false, + isFree: true, + showGatewayMetadata: true, + ...overrides, + }; +} + // The trailing icons of a row, in render order. The favorite star must be the // last one so every row's star shares one right-alignment column, and the // selected check sits in the reserved column to its left. @@ -89,6 +111,44 @@ describe('ModelPickerOptionRow BYOK badge', () => { }); }); +describe('ModelPickerOptionRow free badge', () => { + // The gateway catalogue names free models "… (free)" and Kilo's own Auto Free + // model is named for it in every catalog, so the green free badge would print + // a fact the row title already carries. + it('renders no free badge when the displayed name already states it', () => { + const renderer = renderRow(gatewayCatalogOption({ name: 'Laguna S 2.1 (free)' })); + + expect(textStrings(renderer.root)).not.toContain(freeModelFreeLabel()); + }); + + it('still renders the free badge when the name does not state it', () => { + const renderer = renderRow(gatewayCatalogOption({ name: 'Laguna S 2.1' })); + + expect(textStrings(renderer.root)).toContain(freeModelFreeLabel()); + }); + + // The badge must be suppressed from the model's identity, not from matching + // copy: these nine catalogs name the Auto Free model with a free word the + // badge label does not literally contain (ru "Авто Бесплатный" vs + // "Бесплатно"), which was the localized half of the duplication. + it.each(['be', 'bg', 'bs', 'hr', 'mk', 'ru', 'sr', 'ta', 'uk'])( + 'renders no free badge for the localized Auto Free name (%s)', + async locale => { + await i18n.changeLanguage(locale); + const renderer = renderRow( + gatewayCatalogOption({ + id: 'kilo-auto/free', + displayId: 'kilo-auto/free', + name: 'backend name', + }) + ); + + expect(textStrings(renderer.root)).toContain(i18n.t('models.auto.free')); + expect(textStrings(renderer.root)).not.toContain(freeModelFreeLabel()); + } + ); +}); + describe('Auto model labels', () => { // The backend names Kilo's own Auto models in English ("Auto Efficient"), // which no catalog translates; the chip and the picker row must show the diff --git a/apps/mobile/src/components/agents/model-selector.tsx b/apps/mobile/src/components/agents/model-selector.tsx index b3d5d27eb0..350eafd034 100644 --- a/apps/mobile/src/components/agents/model-selector.tsx +++ b/apps/mobile/src/components/agents/model-selector.tsx @@ -15,6 +15,7 @@ import { freeModelDataLabel, freeModelFreeLabel, getFreeModelDataAccessibilityLabel, + modelNameStatesFree, } from '@/lib/free-model-data-disclosure'; import { type ModelOption, thinkingEffortLabel } from '@/lib/hooks/use-available-models'; import { type SessionModelOption } from '@/lib/hooks/use-session-model-options'; @@ -254,13 +255,18 @@ export function ModelPickerOptionRow({ const { free, byok, collectsData } = modelSelectorBadges(option); const costLabel = modelPickerCostLabel(option); const name = autoModelLabel(option.displayId, option.name); + // The name may already state the fact ("… (free)", "Auto Free" in every + // locale), so the badge and its accessibility phrase only render when it + // does not. The free Auto model is decided by id too: nine catalogs name it + // with a free word the badge label does not contain. + const showFreeBadge = free && !byok && !modelNameStatesFree(name, option.displayId); const accessibilityLabel = formatList( [ option.provider?.name, name, option.displayId, byok ? BYOK_MODEL_LABEL : undefined, - free && !byok ? freeModelFreeLabel() : undefined, + showFreeBadge ? freeModelFreeLabel() : undefined, collectsData ? freeModelDataLabel() : undefined, costLabel ?? undefined, option.unavailable ? t('agentChat.modelSelector.unavailableState') : undefined, @@ -311,9 +317,9 @@ export function ModelPickerOptionRow({ {t('agentChat.modelSelector.unavailable')} ) : null} - {free || byok || collectsData ? ( + {showFreeBadge || byok || collectsData ? ( - {free && !byok ? ( + {showFreeBadge ? ( {freeModelFreeLabel()} diff --git a/apps/mobile/src/components/agents/picker-search.mounted.test.tsx b/apps/mobile/src/components/agents/picker-search.mounted.test.tsx index 00135c5589..6f214e7f21 100644 --- a/apps/mobile/src/components/agents/picker-search.mounted.test.tsx +++ b/apps/mobile/src/components/agents/picker-search.mounted.test.tsx @@ -9,9 +9,17 @@ import { type RepoOption } from '@/lib/picker-bridge'; import { modelPickerSlot, repoPickerSlot, UNFENCED_ROUTE_KEY } from '@/lib/route-registry'; import '@/i18n'; +// Live so a test can flip the interface direction before it mounts; the input +// alignment helper reads `I18nManager.isRTL` when it composes the style. +const i18nManager = vi.hoisted(() => ({ isRTL: false })); +// The one token the picker's search input passes inline; the assertions read +// the same value the mock hands the component. +const theme = vi.hoisted(() => ({ foreground: '#111111' })); + vi.mock('@/components/centered-state', () => ({ CenteredState: 'CenteredState' })); vi.mock('react-native', () => ({ FlatList: 'FlatList', + I18nManager: i18nManager, Pressable: 'Pressable', ScrollView: 'ScrollView', TextInput: 'TextInput', @@ -42,7 +50,9 @@ vi.mock('@/components/ui/icons', () => ({ vi.mock('@/components/agents/model-selector', () => ({ ModelPickerOptionRow: 'ModelPickerOptionRow', })); -vi.mock('@/lib/hooks/use-theme-colors', () => ({ useThemeColors: () => ({}) })); +vi.mock('@/lib/hooks/use-theme-colors', () => ({ + useThemeColors: () => ({ foreground: theme.foreground }), +})); vi.mock('@/lib/hooks/use-model-preferences', () => ({ useModelPreferences: () => ({ favorites: [], addFavorite: vi.fn(), removeFavorite: vi.fn() }), })); @@ -59,6 +69,7 @@ const repo: RepoOption = { platform: 'github', fullName: 'org/repo', isPrivate: beforeEach(() => { (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + i18nManager.isRTL = false; modelPickerSlot.set(UNFENCED_ROUTE_KEY, { options: [model], currentValue: '', @@ -178,6 +189,60 @@ describe('repository picker search placeholder', () => { }); }); +describe('repository picker query alignment', () => { + function searchInput(renderer: Awaited>) { + const input = hosts(renderer, 'TextInput')[0]; + if (!input) { + throw new Error('Picker search input did not mount'); + } + return input; + } + + it('aligns the typed query to the field start edge in RTL', async () => { + // `textAlign: 'auto'` resolves against the first strong character, so a + // Latin query stays at the left edge while the clear and search controls + // sit at the right, leaving a dead gap between them. + i18nManager.isRTL = true; + const renderer = await mount(RepoPickerScreen); + expect(searchInput(renderer).props.style).toEqual([ + { textAlign: 'right' }, + { color: theme.foreground }, + ]); + }); + + it('leaves the input style to the caller in LTR so English is unchanged', async () => { + i18nManager.isRTL = false; + const renderer = await mount(RepoPickerScreen); + expect(searchInput(renderer).props.style).toEqual({ color: theme.foreground }); + }); +}); + +describe('model picker query alignment', () => { + function searchInput(renderer: Awaited>) { + const input = hosts(renderer, 'TextInput')[0]; + if (!input) { + throw new Error('Picker search input did not mount'); + } + return input; + } + + it('aligns the query and its native placeholder to the field start edge in RTL', async () => { + // `textAlign: 'auto'` resolves against the first strong character, so a + // Latin query and the Arabic placeholder stay at the left edge while the + // clear and search controls sit at the right, leaving a dead gap between + // them. The model picker had no alignment at all before this. + i18nManager.isRTL = true; + const renderer = await mount(ModelPickerContent); + expect(searchInput(renderer).props.style).toEqual([{ textAlign: 'right' }, undefined]); + }); + + it('leaves the input style to the caller in LTR so English is unchanged', async () => { + i18nManager.isRTL = false; + const renderer = await mount(ModelPickerContent); + expect(searchInput(renderer).props.style).toBeUndefined(); + }); +}); + async function mount(Component: () => ReactNode) { const mounted = await renderWithProviders(createElement(Component)); onTestFinished(mounted.unmount); diff --git a/apps/mobile/src/components/language-picker-row.tsx b/apps/mobile/src/components/language-picker-row.tsx index a159b06111..8bded52d68 100644 --- a/apps/mobile/src/components/language-picker-row.tsx +++ b/apps/mobile/src/components/language-picker-row.tsx @@ -5,6 +5,7 @@ import { ChoiceRow } from '@/components/ui/choice-row'; import { Text } from '@/components/ui/text'; import { type LanguagePickerItem } from '@/i18n/language-rows'; import { type LanguagePreference } from '@/lib/hooks/use-language-preference'; +import { cn } from '@/lib/utils'; type LanguagePickerRowProps = Readonly<{ item: LanguagePickerItem; @@ -34,6 +35,14 @@ export function LanguagePickerRow({ const { t } = useTranslation(); const dividerClass = showDivider ? 'border-b-[0.5px] border-hair-soft' : undefined; const textClass = `flex-1 ${isRtl ? 'pl-3' : 'pr-3'}`; + // iOS resolves `textAlign: 'auto'` from the paragraph's first strong + // character, so an Arabic endonym right-aligns itself inside an LTR + // interface while its Latin subtitle stays left — the two lines of one row + // then do not share an edge. Naming the physical start edge in LTR keeps + // them together. RTL needs no counterpart: `@/components/ui/text` names the + // paragraph direction there, and React Native swaps a physical `text-left` + // under an RTL layout, which would float it to the wrong edge. + const alignClass = isRtl ? undefined : 'text-left'; if (item.kind === 'section') { return ( @@ -57,8 +66,10 @@ export function LanguagePickerRow({ }} > - {t('language.deviceLanguage')} - + + {t('language.deviceLanguage')} + + {deviceEndonym} @@ -80,8 +91,8 @@ export function LanguagePickerRow({ a Latin name under a right-aligned row must not jump to the left edge. Unicode bidi already renders each script correctly inside the line. */} - {item.row.endonym} - + {item.row.endonym} + {item.row.englishName} diff --git a/apps/mobile/src/components/language-picker-sheet.mounted.test.tsx b/apps/mobile/src/components/language-picker-sheet.mounted.test.tsx index 167278141f..966afda8ef 100644 --- a/apps/mobile/src/components/language-picker-sheet.mounted.test.tsx +++ b/apps/mobile/src/components/language-picker-sheet.mounted.test.tsx @@ -522,6 +522,49 @@ describe('LanguagePickerSheet apply', () => { }); }); +describe('LanguagePickerSheet row alignment', () => { + beforeEach(() => { + (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + i18nManager.isRTL = false; + }); + + function rowLineClassNames(row: TestRenderer.ReactTestInstance): string[] { + return findByType(row, 'Text').map(line => line.props.className ?? ''); + } + + it('pins both lines of a row to the interface start edge in an LTR interface', async () => { + i18nManager.isRTL = false; + const renderer = await mountSheet(vi.fn<() => void>()); + + // `العربية` is the row whose endonym right-aligned itself under LTR; the + // device row carries the same two-line shape. + for (const label of ['العربية', 'Device language']) { + const lines = rowLineClassNames(findChoiceRow(renderer.root, label)); + expect(lines).toHaveLength(2); + for (const className of lines) { + expect(className).toContain('text-left'); + } + } + + renderer.unmount(); + }); + + it('leaves an RTL row to the paragraph direction, never a physical edge', async () => { + i18nManager.isRTL = true; + const renderer = await mountSheet(vi.fn<() => void>()); + + for (const label of ['العربية', 'Device language']) { + const lines = rowLineClassNames(findChoiceRow(renderer.root, label)); + expect(lines).toHaveLength(2); + for (const className of lines) { + expect(className).not.toContain('text-left'); + } + } + + renderer.unmount(); + }); +}); + describe('LanguagePickerSheet search field', () => { beforeEach(() => { (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; diff --git a/apps/mobile/src/lib/free-model-data-disclosure.test.ts b/apps/mobile/src/lib/free-model-data-disclosure.test.ts index 26a5561384..fb36f33f88 100644 --- a/apps/mobile/src/lib/free-model-data-disclosure.test.ts +++ b/apps/mobile/src/lib/free-model-data-disclosure.test.ts @@ -7,6 +7,7 @@ import { hasUserByokAvailable, isFreeModelOption, mayTrainOnYourPrompts, + modelNameStatesFree, } from './free-model-data-disclosure'; describe('free model data disclosure', () => { @@ -58,6 +59,23 @@ describe('free model data disclosure', () => { expect(hasUserByokAvailable({ id: 'anthropic/claude' })).toBe(false); }); + it('detects a displayed name that already states the model is free', () => { + expect(modelNameStatesFree('Laguna S 2.1 (free)')).toBe(true); + expect(modelNameStatesFree('Nemotron 3 Ultra (free)')).toBe(true); + expect(modelNameStatesFree('Auto Free')).toBe(true); + expect(modelNameStatesFree('Laguna S 2.1')).toBe(false); + expect(modelNameStatesFree('Auto Efficient')).toBe(false); + }); + + it('states free from the free Auto model identity even when the name does not', () => { + // Nine catalogs name the Auto Free model with a free word the free badge's + // own label does not literally contain (ru "Авто Бесплатный" vs + // "Бесплатно"), so the model's identity decides, not a substring of copy. + expect(modelNameStatesFree('Авто Бесплатный', 'kilo-auto/free')).toBe(true); + expect(modelNameStatesFree('Авто Бесплатный', 'kilocode/kilo-auto/free')).toBe(true); + expect(modelNameStatesFree('Auto Efficient', 'kilo-auto/efficient')).toBe(false); + }); + it('adds a data collection phrase to accessibility labels', () => { expect(getFreeModelDataAccessibilityLabel('Kilo Auto')).toBe('Kilo Auto, Data collected'); }); diff --git a/apps/mobile/src/lib/free-model-data-disclosure.ts b/apps/mobile/src/lib/free-model-data-disclosure.ts index 0802a7c18d..cebf7671c6 100644 --- a/apps/mobile/src/lib/free-model-data-disclosure.ts +++ b/apps/mobile/src/lib/free-model-data-disclosure.ts @@ -1,5 +1,7 @@ import { i18n } from '@/i18n'; +import { stripModelPrefix } from './model-id'; + /** An acronym the product keeps in English in every locale. */ export const BYOK_MODEL_LABEL = 'BYOK'; @@ -11,6 +13,36 @@ export function freeModelFreeLabel(): string { return i18n.t('models.free'); } +/** The gateway catalogue's own marker for a free model: `Laguna S 2.1 (free)`. */ +const CATALOG_FREE_MARKER = /\(free\)/i; + +/** Kilo's own Auto Free model: its localized name is a free name in every catalog. */ +const FREE_AUTO_MODEL_ID = 'kilo-auto/free'; + +/** + * Whether the displayed model name already states that the model is free. + * The gateway catalogue names free models "… (free)", and Kilo's own Auto Free + * model is named for it in every catalog, so a free badge beside such a name + * would print the same fact twice on one row. + * + * Nine catalogs name Auto Free with a free word the badge's own label does not + * literally contain (ru "Авто Бесплатный" vs "Бесплатно"), so the model's + * identity decides as well: `displayId` is the model the picker resolved the + * label from, and the free Auto model always states free. + */ +export function modelNameStatesFree(name: string, displayId?: string): boolean { + if (displayId && stripModelPrefix(displayId) === FREE_AUTO_MODEL_ID) { + return true; + } + const freeLabel = freeModelFreeLabel(); + const locale = i18n.language; + return ( + CATALOG_FREE_MARKER.test(name) || + (freeLabel.length > 0 && + name.toLocaleLowerCase(locale).includes(freeLabel.toLocaleLowerCase(locale))) + ); +} + export type ModelDataDisclosure = { id: string; isFree?: boolean; From b963d6ce050415ad9a52c0171ba3db12d03b2e86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 24 Sep 2026 13:12:02 +0000 Subject: [PATCH 2/4] fix(drizzle): regenerate the migration chain the branch left inconsistent --- packages/db/src/migrations/0258_woozy_morbius.sql | 9 +++++++++ packages/db/src/migrations/meta/0258_snapshot.json | 2 +- packages/db/src/migrations/meta/_journal.json | 4 ++-- 3 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 packages/db/src/migrations/0258_woozy_morbius.sql diff --git a/packages/db/src/migrations/0258_woozy_morbius.sql b/packages/db/src/migrations/0258_woozy_morbius.sql new file mode 100644 index 0000000000..9a6cd7809b --- /dev/null +++ b/packages/db/src/migrations/0258_woozy_morbius.sql @@ -0,0 +1,9 @@ +ALTER TABLE "platform_integrations" ADD COLUMN "github_connection_role" text;--> statement-breakpoint +CREATE UNIQUE INDEX CONCURRENTLY "UQ_platform_integrations_github_workflow_canonical" ON "platform_integrations" USING btree ("github_installation_id") WHERE "platform_integrations"."github_connection_role" = 'workflow';--> statement-breakpoint +CREATE UNIQUE INDEX CONCURRENTLY "UQ_platform_integrations_github_workflow_identity" ON "platform_integrations" USING btree (COALESCE("github_app_type", 'standard'),"platform_installation_id") WHERE "platform_integrations"."github_connection_role" = 'workflow';--> statement-breakpoint +ALTER TABLE "platform_integrations" ADD CONSTRAINT "platform_integrations_github_connection_role_check" CHECK ("platform_integrations"."github_connection_role" IS NULL OR ( + "platform_integrations"."platform" = 'github' AND "platform_integrations"."integration_type" = 'app' + AND "platform_integrations"."platform_installation_id" IS NOT NULL + AND "platform_integrations"."github_connection_role" IN ('workflow', 'agent_only') + AND ("platform_integrations"."github_connection_role" <> 'agent_only' OR "platform_integrations"."github_installation_id" IS NOT NULL) + )); \ No newline at end of file diff --git a/packages/db/src/migrations/meta/0258_snapshot.json b/packages/db/src/migrations/meta/0258_snapshot.json index c9a0b64dd1..4474d1292e 100644 --- a/packages/db/src/migrations/meta/0258_snapshot.json +++ b/packages/db/src/migrations/meta/0258_snapshot.json @@ -1,5 +1,5 @@ { - "id": "9a5cac5a-f089-425b-95ff-d1542d7c0b63", + "id": "dd42816b-0574-4645-adc3-5284feb80b35", "prevId": "6895dcc0-c940-4977-a174-22a6ab734ec8", "version": "7", "dialect": "postgresql", diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json index c0e68dd3a9..b24b7e6512 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -1811,8 +1811,8 @@ { "idx": 258, "version": "7", - "when": 1790208815286, - "tag": "0258_slim_hobgoblin", + "when": 1790255522257, + "tag": "0258_woozy_morbius", "breakpoints": true } ] From 452b4f96f61ff7b668b1eca06f06e3296bd747ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 25 Sep 2026 15:11:03 +0200 Subject: [PATCH 3/4] fix(mobile): use the shared Input for picker search fields main's shared single-line Input already applies withRtlInputAlignment, so the repository and model picker search fields keep that contract instead of re-applying the alignment themselves, and the picker search test keeps one live I18nManager mock. --- apps/mobile/src/app/(app)/agent-chat/repo-picker.tsx | 10 ++-------- .../src/components/agents/model-picker-content.tsx | 10 +--------- .../components/agents/picker-search.mounted.test.tsx | 3 --- 3 files changed, 3 insertions(+), 20 deletions(-) diff --git a/apps/mobile/src/app/(app)/agent-chat/repo-picker.tsx b/apps/mobile/src/app/(app)/agent-chat/repo-picker.tsx index 74733d1c97..ea1126da52 100644 --- a/apps/mobile/src/app/(app)/agent-chat/repo-picker.tsx +++ b/apps/mobile/src/app/(app)/agent-chat/repo-picker.tsx @@ -13,7 +13,6 @@ import { useThemeColors } from '@/lib/hooks/use-theme-colors'; import { REPO_PLATFORM_LABEL_KEYS, type RepoOption } from '@/lib/picker-bridge'; import { repoPickerSlot, UNFENCED_ROUTE_KEY, useRouteRegistry } from '@/lib/route-registry'; import { filterRepoPickerOptions } from '@/lib/repo-picker-filter'; -import { withRtlInputAlignment } from '@/lib/rtl-text'; type PickerListItem = | { key: string; kind: 'header'; titleKey: string } @@ -124,13 +123,8 @@ export default function RepoPickerScreen() { clearButtonMode="while-editing" returnKeyType="search" textAlignVertical="center" - className="h-8 p-0 text-base leading-[normal] text-foreground" - // `textAlign: 'auto'` resolves against the query's first strong - // character, so a Latin query stays at the field's left edge in an - // RTL interface while the clear and search controls sit at the - // right. Naming the alignment keeps the query on the same side as - // the controls. - style={withRtlInputAlignment({ color: colors.foreground })} + className="px-0 text-base text-foreground" + style={{ color: colors.foreground }} onChangeText={setSearch} /> {search.length === 0 ? ( diff --git a/apps/mobile/src/components/agents/model-picker-content.tsx b/apps/mobile/src/components/agents/model-picker-content.tsx index 829ab31db3..dba89e0944 100644 --- a/apps/mobile/src/components/agents/model-picker-content.tsx +++ b/apps/mobile/src/components/agents/model-picker-content.tsx @@ -24,7 +24,6 @@ import { import { commitModelPickerSelection, resolveModelPickerSelection } from '@/lib/picker-bridge'; import { parseParam } from '@/lib/route-params'; import { modelPickerSlot, UNFENCED_ROUTE_KEY, useRouteRegistry } from '@/lib/route-registry'; -import { withRtlInputAlignment } from '@/lib/rtl-text'; // The picker sheet renders its own scroll container (`scrollable={false}`), so // the list fills the sheet's body. @@ -226,14 +225,7 @@ export function ModelPickerContent() { autoCapitalize="none" autoCorrect={false} returnKeyType="search" - className="h-8 flex-1 p-0 text-base leading-[normal] text-foreground" - // `textAlign: 'auto'` resolves against the query's first strong - // character, so a Latin query and the native placeholder stay at - // the field's left edge in an RTL interface while the clear and - // search controls sit at the right, leaving a dead gap between - // them. Naming the alignment keeps both on the interface's start - // edge, as the repository picker and language search fields do. - style={withRtlInputAlignment(undefined)} + className="flex-1 px-0 text-base text-foreground" onChangeText={setSearch} /> {/* In-field clear, on every platform: `clearButtonMode` is iOS diff --git a/apps/mobile/src/components/agents/picker-search.mounted.test.tsx b/apps/mobile/src/components/agents/picker-search.mounted.test.tsx index 098c4ee4b7..c9de951f40 100644 --- a/apps/mobile/src/components/agents/picker-search.mounted.test.tsx +++ b/apps/mobile/src/components/agents/picker-search.mounted.test.tsx @@ -52,9 +52,6 @@ vi.mock('react-native', () => ({ ScrollView: 'ScrollView', TextInput: 'TextInput', View: 'View', - // `@/components/ui/input` reads `I18nManager.isRTL` through - // `withRtlInputAlignment` on every render. - I18nManager: { isRTL: false }, })); vi.mock('react-native-safe-area-context', () => ({ useSafeAreaInsets: () => ({ bottom: 0 }) })); vi.mock('expo-haptics', () => ({ selectionAsync: vi.fn() })); From 2ea4c01a86958979fc3bca55554a516fa555a76b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 25 Sep 2026 15:12:39 +0200 Subject: [PATCH 4/4] test(mobile): drop the redundant picker mock change main's shared Input already brought I18nManager into the model picker mount test's react-native mock. --- .../components/agents/model-picker-content.mounted.test.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/mobile/src/components/agents/model-picker-content.mounted.test.tsx b/apps/mobile/src/components/agents/model-picker-content.mounted.test.tsx index ca91e5c063..1fb797300a 100644 --- a/apps/mobile/src/components/agents/model-picker-content.mounted.test.tsx +++ b/apps/mobile/src/components/agents/model-picker-content.mounted.test.tsx @@ -103,10 +103,6 @@ vi.mock('@shopify/flash-list', async () => { }); vi.mock('react-native', () => ({ - // `lib/rtl-text` reads `I18nManager.isRTL` for `withRtlInputAlignment`, which - // the picker's search field passes its style through, so the mock must carry - // the module's direction flag. - I18nManager: { isRTL: false }, Pressable: 'Pressable', TextInput: 'TextInput', View: 'View',