Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ vi.mock('@/components/agents/use-effective-agent-profile', () => ({
useEffectiveAgentProfile: () => ({
allProfiles: [],
effectiveDefaultId: null,
effectiveDefaultProfile: null,
isLoading: false,
isError: false,
refetch: vi.fn(),
Expand Down
9 changes: 5 additions & 4 deletions apps/mobile/src/app/(app)/agent-chat/profile-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,8 @@ export default function ProfilePickerScreen() {
// "Options expired" flash before a later effect populates state.
const [bridge] = useState(() => profilePickerSlot.get(UNFENCED_ROUTE_KEY));

const { allProfiles, effectiveDefaultId, isLoading, isError, refetch } = useEffectiveAgentProfile(
bridge?.organizationId,
bridge?.selectedOverrideProfileId ?? null
);
const { allProfiles, effectiveDefaultId, effectiveDefaultProfile, isLoading, isError, refetch } =
useEffectiveAgentProfile(bridge?.organizationId, bridge?.selectedOverrideProfileId ?? null);

if (!bridge) {
return (
Expand Down Expand Up @@ -60,6 +58,9 @@ export default function ProfilePickerScreen() {
// A default exists, so clearing the override is not "no profile": the
// server resolves that default on top of any repository binding.
defaultProfileApplies={effectiveDefaultId !== null}
// Name it, so the row that clears the override says which profile the
// session then runs on instead of only that some default applies.
defaultProfileName={effectiveDefaultProfile?.name ?? null}
isLoading={isLoading}
isError={isError}
needsAttention={picker.overrideNeedsAttention}
Expand Down
6 changes: 6 additions & 0 deletions apps/mobile/src/app/+native-intent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { redirectSystemPath as mapWebPath } from '@/lib/deep-link-handler';
import { handleGlanceableFixturePath } from '@/lib/glanceable/fixture-harness';
import { handleHomeFixturePath } from '@/lib/home-fixture';
import { modelPickerFixtureRoute } from '@/lib/model-picker-fixture';
import { profilePickerFixtureRoute } from '@/lib/profile-picker-fixture';

// Composes the native-intent concerns in order: the share-extension check
// returns early before web-path mapping (a share URL is never a web route), then
Expand Down Expand Up @@ -74,6 +75,11 @@ export function redirectSystemPath({ path, initial }: { path: string; initial: b
if (fixtureRoute) {
return fixtureRoute;
}
// Dev-only profile picker sheet, reached through the development fixture path.
const profilePickerRoute = profilePickerFixtureRoute(path);
if (profilePickerRoute) {
return profilePickerRoute;
}
}
if (handleAppActionPath({ path, initial })) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,20 +178,48 @@ describe('ProfilePickerSheet', () => {
const renderer = mount({
selectedOverrideProfileId: 'backend',
defaultProfileApplies: true,
defaultProfileName: BACKEND.name,
onSelect,
});

// Clearing the override hands the session to the default, so the row says
// which profile that is instead of claiming no profile.
// which profile that is instead of claiming no profile. The name is what
// the Environment chip shows, so the checked row and the chip agree.
expect(radios(renderer).map(node => node.props.accessibilityLabel)).toEqual([
'Default profile',
'Default profile: Backend',
'Backend',
'Frontend',
]);
press(radio(renderer, 'Default profile'));
expect(texts(renderer)).toContain('Default profile');
expect(texts(renderer)).toContain('Resolves to Backend');
press(radio(renderer, 'Default profile: Backend'));
expect(onSelect).toHaveBeenLastCalledWith(null);
});

it('keeps the generic default row when the resolved profile name is unknown', () => {
const renderer = mount({ defaultProfileApplies: true });

expect(radios(renderer).map(node => node.props.accessibilityLabel)).toEqual([
'Default profile',
'Backend',
'Frontend',
]);
expect(texts(renderer)).not.toContainEqual(expect.stringContaining('Resolves to'));
});

it('does not name a default on the No profile row when none applies', () => {
// A name without an applicable default must not leak onto the row that
// clears the override: that row means the session runs on no profile.
const renderer = mount({ defaultProfileName: BACKEND.name });

expect(radios(renderer).map(node => node.props.accessibilityLabel)).toEqual([
'No profile',
'Backend',
'Frontend',
]);
expect(texts(renderer)).not.toContainEqual(expect.stringContaining('Resolves to'));
});

it('leaves every row unchecked and shows the attention copy for a stale override', () => {
const renderer = mount({ selectedOverrideProfileId: 'deleted', needsAttention: true });

Expand Down
32 changes: 28 additions & 4 deletions apps/mobile/src/components/agents/profile-picker-sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ export type ProfilePickerSheetProps = {
* runs on, so the row names the default instead of claiming no profile.
*/
defaultProfileApplies: boolean;
/**
* The profile the effective default resolves to, when known. The no-override
* row names it so the checked row matches the Environment chip, which shows
* that same profile's name.
*/
defaultProfileName?: string | null;
isLoading: boolean;
isError: boolean;
/** The picked id no longer resolves to a profile. */
Expand All @@ -49,6 +55,7 @@ export function ProfilePickerSheet({
candidates,
hasProfiles,
defaultProfileApplies,
defaultProfileName,
selectedOverrideProfileId,
isLoading,
isError,
Expand All @@ -66,6 +73,16 @@ export function ProfilePickerSheet({
const noOverrideLabelKey = defaultProfileApplies
? 'profiles.defaultSectionTitle'
: 'agentChat.newSession.noProfile';
const noOverrideLabel = t(noOverrideLabelKey);
// The Environment chip names the profile the default resolves to; the row
// has to say the same thing, or the checked row and the chip disagree.
const resolvedDefaultName = defaultProfileApplies ? (defaultProfileName ?? null) : null;
const noOverrideAccessibilityLabel =
resolvedDefaultName === null
? noOverrideLabel
: t('agentChat.newSession.defaultProfileResolvesToAccessibility', {
name: resolvedDefaultName,
});

if (isLoading) {
return (
Expand Down Expand Up @@ -142,13 +159,20 @@ export function ProfilePickerSheet({
onSelect(null);
}}
{...radioItemA11y({
label: t(noOverrideLabelKey),
label: noOverrideAccessibilityLabel,
checked: selectedOverrideProfileId === null && !needsAttention,
})}
>
<Text className="flex-1 text-base text-foreground" numberOfLines={1}>
{t(noOverrideLabelKey)}
</Text>
<View className="min-w-0 flex-1">
<Text className="text-base text-foreground" numberOfLines={1}>
{noOverrideLabel}
</Text>
{resolvedDefaultName === null ? null : (
<Text className="text-sm text-muted-foreground" numberOfLines={1}>
{t('agentChat.newSession.defaultProfileResolvesTo', { name: resolvedDefaultName })}
</Text>
)}
</View>
{selectedOverrideProfileId === null && !needsAttention ? (
<Check size={18} color={colors.primary} />
) : null}
Expand Down
27 changes: 19 additions & 8 deletions apps/mobile/src/components/agents/use-effective-agent-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,23 +58,33 @@ export function useEffectiveAgentProfile(
const isOrg = organizationId !== undefined;
const query = isOrg ? combined : personal;

const { allProfiles, effectiveDefaultId } = useMemo(() => {
const { allProfiles, effectiveDefaultId, effectiveDefaultProfile } = useMemo(() => {
if (isOrg) {
const data = combined.data;
if (!data || !Array.isArray(data.orgProfiles) || !Array.isArray(data.personalProfiles)) {
return { allProfiles: [] as EffectiveAgentProfile[], effectiveDefaultId: null };
return {
allProfiles: [] as EffectiveAgentProfile[],
effectiveDefaultId: null,
effectiveDefaultProfile: null as EffectiveAgentProfile | null,
};
}
const profiles = [...data.orgProfiles, ...data.personalProfiles].map(profile =>
toEffective(profile)
);
return {
allProfiles: [...data.orgProfiles, ...data.personalProfiles].map(profile =>
toEffective(profile)
),
allProfiles: profiles,
effectiveDefaultId: data.effectiveDefaultId,
effectiveDefaultProfile:
profiles.find(profile => profile.id === data.effectiveDefaultId) ?? null,
};
}
const profiles = personal.data ?? [];
const rows = personal.data ?? [];
const profiles = rows.map(profile => toEffective(profile));
const defaultId = rows.find(profile => profile.isDefault)?.id ?? null;
return {
allProfiles: profiles.map(profile => toEffective(profile)),
effectiveDefaultId: profiles.find(profile => profile.isDefault)?.id ?? null,
allProfiles: profiles,
effectiveDefaultId: defaultId,
effectiveDefaultProfile: profiles.find(profile => profile.id === defaultId) ?? null,
};
}, [isOrg, personal.data, combined.data]);

Expand Down Expand Up @@ -102,6 +112,7 @@ export function useEffectiveAgentProfile(
profileId,
allProfiles,
effectiveDefaultId,
effectiveDefaultProfile,
// The user's explicit pick, echoed for the picker's selected row.
selectedProfileId: overrideProfileId ?? null,
hasOverride: query.isError ? false : picker.hasOverride,
Expand Down
2 changes: 2 additions & 0 deletions apps/mobile/src/i18n/locales/af.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions apps/mobile/src/i18n/locales/am.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions apps/mobile/src/i18n/locales/ar.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions apps/mobile/src/i18n/locales/az.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions apps/mobile/src/i18n/locales/be.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions apps/mobile/src/i18n/locales/bg.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions apps/mobile/src/i18n/locales/bn.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions apps/mobile/src/i18n/locales/bs.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions apps/mobile/src/i18n/locales/ca.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions apps/mobile/src/i18n/locales/ckb.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions apps/mobile/src/i18n/locales/cs.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions apps/mobile/src/i18n/locales/cy.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions apps/mobile/src/i18n/locales/da.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions apps/mobile/src/i18n/locales/de.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions apps/mobile/src/i18n/locales/el.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions apps/mobile/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2438,6 +2438,8 @@
"forkFilesUnchangedOnComputer": "Files on this computer are not changed.",
"pickProfile": "Pick a profile",
"noProfile": "No profile",
"defaultProfileResolvesTo": "Resolves to {{name}}",
"defaultProfileResolvesToAccessibility": "Default profile: {{name}}",
"manageProfiles": "Manage profiles",
"noProfiles": "No profiles yet. Create one to add environment variables, MCP servers, and skills.",
"advancedConfig": "Advanced Configuration",
Expand Down
2 changes: 2 additions & 0 deletions apps/mobile/src/i18n/locales/es.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading