Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
794d8cf
feat(mobile): add Manage Profiles and profile picker at web parity
iscekic Sep 23, 2026
aeb4303
merge: origin/main into kwf/req-20260918-153926-dcb3
iscekic Sep 23, 2026
91fd685
merge: origin/main into kwf/req-20260918-153926-dcb3
iscekic Sep 23, 2026
f6b8270
fix(mobile): apply three review findings on the profile picker
iscekic Sep 24, 2026
0719403
feat(db): add nullable cli_sessions_v2.profile_id referencing the ses…
iscekic Sep 24, 2026
3332309
feat(session-ingest-contracts): carry an optional profileId on the se…
iscekic Sep 24, 2026
c3b7f3b
feat(session-ingest): persist the create payload's profileId on cli_s…
iscekic Sep 24, 2026
ba00753
feat(cloud-agent-next): persist the resolved session profile id at cr…
iscekic Sep 24, 2026
8d9bd03
feat(cloud-agent-sdk): expose the session's recorded profileId on Fet…
iscekic Sep 24, 2026
ac64550
feat: surface the session's recorded profile id through the session r…
iscekic Sep 24, 2026
a6bc2d1
fix(mobile): resolve the session-detail chip from the session's own p…
iscekic Sep 24, 2026
196d953
test(session-ingest): cover profile_id persistence on the create path
iscekic Sep 24, 2026
508fa19
test(web): cover the resolved profile id from mergeProfileConfiguration
iscekic Sep 24, 2026
e2173db
Merge remote-tracking branch 'origin/main' into kwf/req-20260918-1539…
iscekic Sep 24, 2026
c244bc2
fix(mobile): keep the session chip's profile-error state and ignore a…
iscekic Sep 24, 2026
c448248
feat(db): index cli_sessions_v2.profile_id
iscekic Sep 24, 2026
1d431ae
feat(db): build cli_sessions_v2.profile_id index concurrently
iscekic Sep 24, 2026
a6a285d
fix(ci): update the tests and the repo picker for the new session pro…
iscekic Sep 24, 2026
c4f73b2
test(mobile): provide I18nManager to the profile screen mocks
iscekic Sep 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,7 @@ export function createExtensionAgentSessionManager({
!rs
),
organizationId: sessionResult.organization_id,
profileId: sessionResult.profile_id,
prompt: rs?.prompt ?? null,
repository: rs?.githubRepo ?? null,
...(rs?.runtimeAgents === undefined ? {} : { runtimeAgents: rs.runtimeAgents }),
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/app/(app)/(tabs)/(3_profile)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export default function ProfileLayout() {
[['personalReviewAgent']],
[['securityAgent']],
[['kiloPass']],
[['agentProfiles']],
]);
return <Stack screenOptions={{ headerShown: false }} />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useLocalSearchParams } from 'expo-router';

import { InvalidRouteState } from '@/components/invalid-route-state';
import { ProfileAgentsScreen } from '@/components/profiles/profile-agents-screen';
import { getProfilesPath } from '@/lib/profile-agent-navigation';
import { parseParam } from '@/lib/route-params';

export default function ProfileAgentsRoute() {
const { profileId: rawProfileId, organizationId: rawOrganizationId } = useLocalSearchParams<{
profileId: string;
organizationId?: string;
}>();
const profileId = parseParam(rawProfileId);
if (!profileId) {
return <InvalidRouteState backTo={getProfilesPath()} />;
}
return (
<ProfileAgentsScreen
profileId={profileId}
organizationId={parseParam(rawOrganizationId) ?? undefined}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useLocalSearchParams } from 'expo-router';

import { InvalidRouteState } from '@/components/invalid-route-state';
import { ProfileCommandsScreen } from '@/components/profiles/profile-commands-screen';
import { getProfilesPath } from '@/lib/profile-agent-navigation';
import { parseParam } from '@/lib/route-params';

export default function ProfileCommandsRoute() {
const { profileId: rawProfileId, organizationId: rawOrganizationId } = useLocalSearchParams<{
profileId: string;
organizationId?: string;
}>();
const profileId = parseParam(rawProfileId);
if (!profileId) {
return <InvalidRouteState backTo={getProfilesPath()} />;
}
return (
<ProfileCommandsScreen
profileId={profileId}
organizationId={parseParam(rawOrganizationId) ?? undefined}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useLocalSearchParams } from 'expo-router';

import { InvalidRouteState } from '@/components/invalid-route-state';
import { ProfileOverviewScreen } from '@/components/profiles/profile-overview-screen';
import { getProfilesPath } from '@/lib/profile-agent-navigation';
import { parseParam } from '@/lib/route-params';

export default function ProfileOverviewRoute() {
const { profileId: rawProfileId, organizationId: rawOrganizationId } = useLocalSearchParams<{
profileId: string;
organizationId?: string;
}>();
const profileId = parseParam(rawProfileId);
if (!profileId) {
return <InvalidRouteState backTo={getProfilesPath()} />;
}
return (
<ProfileOverviewScreen
profileId={profileId}
organizationId={parseParam(rawOrganizationId) ?? undefined}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useLocalSearchParams } from 'expo-router';

import { InvalidRouteState } from '@/components/invalid-route-state';
import { ProfileMcpScreen } from '@/components/profiles/profile-mcp-screen';
import { getProfilesPath } from '@/lib/profile-agent-navigation';
import { parseParam } from '@/lib/route-params';

export default function ProfileMcpRoute() {
const { profileId: rawProfileId, organizationId: rawOrganizationId } = useLocalSearchParams<{
profileId: string;
organizationId?: string;
}>();
const profileId = parseParam(rawProfileId);
if (!profileId) {
return <InvalidRouteState backTo={getProfilesPath()} />;
}
return (
<ProfileMcpScreen
profileId={profileId}
organizationId={parseParam(rawOrganizationId) ?? undefined}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useLocalSearchParams } from 'expo-router';

import { InvalidRouteState } from '@/components/invalid-route-state';
import { ProfileSkillsScreen } from '@/components/profiles/profile-skills-screen';
import { getProfilesPath } from '@/lib/profile-agent-navigation';
import { parseParam } from '@/lib/route-params';

export default function ProfileSkillsRoute() {
const { profileId: rawProfileId, organizationId: rawOrganizationId } = useLocalSearchParams<{
profileId: string;
organizationId?: string;
}>();
const profileId = parseParam(rawProfileId);
if (!profileId) {
return <InvalidRouteState backTo={getProfilesPath()} />;
}
return (
<ProfileSkillsScreen
profileId={profileId}
organizationId={parseParam(rawOrganizationId) ?? undefined}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useLocalSearchParams } from 'expo-router';

import { InvalidRouteState } from '@/components/invalid-route-state';
import { ProfileKiloCommandsScreen } from '@/components/profiles/profile-kilo-commands-screen';
import { getProfilesPath } from '@/lib/profile-agent-navigation';
import { parseParam } from '@/lib/route-params';

export default function ProfileSlashCommandsRoute() {
const { profileId: rawProfileId, organizationId: rawOrganizationId } = useLocalSearchParams<{
profileId: string;
organizationId?: string;
}>();
const profileId = parseParam(rawProfileId);
if (!profileId) {
return <InvalidRouteState backTo={getProfilesPath()} />;
}
return (
<ProfileKiloCommandsScreen
profileId={profileId}
organizationId={parseParam(rawOrganizationId) ?? undefined}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useLocalSearchParams } from 'expo-router';

import { InvalidRouteState } from '@/components/invalid-route-state';
import { ProfileVariablesScreen } from '@/components/profiles/profile-variables-screen';
import { getProfilesPath } from '@/lib/profile-agent-navigation';
import { parseParam } from '@/lib/route-params';

export default function ProfileVariablesRoute() {
const { profileId: rawProfileId, organizationId: rawOrganizationId } = useLocalSearchParams<{
profileId: string;
organizationId?: string;
}>();
const profileId = parseParam(rawProfileId);
if (!profileId) {
return <InvalidRouteState backTo={getProfilesPath()} />;
}
return (
<ProfileVariablesScreen
profileId={profileId}
organizationId={parseParam(rawOrganizationId) ?? undefined}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ProfilesListScreen } from '@/components/profiles/profiles-list-screen';

export default function ProfilesListRoute() {
return <ProfilesListScreen />;
}
5 changes: 5 additions & 0 deletions apps/mobile/src/app/(app)/(tabs)/(3_profile)/profiles/new.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { NewProfileScreen } from '@/components/profiles/new-profile-screen';

export default function NewProfileRoute() {
return <NewProfileScreen />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useLocalSearchParams } from 'expo-router';

import { RepoBindingsScreen } from '@/components/profiles/repo-bindings-screen';
import { parseParam } from '@/lib/route-params';

export default function RepoBindingsRoute() {
const { organizationId: rawOrganizationId } = useLocalSearchParams<{
organizationId?: string;
}>();
return <RepoBindingsScreen organizationId={parseParam(rawOrganizationId) ?? undefined} />;
}
1 change: 1 addition & 0 deletions apps/mobile/src/app/(app)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ export default function AppLayout() {
/>
<Stack.Screen name="agent-chat/instance-picker" options={sheetOptions} />
<Stack.Screen name="agent-chat/folder-picker" options={sheetOptions} />
<Stack.Screen name="agent-chat/profile-picker" options={sheetOptions} />
<Stack.Screen name="share-gate" options={sheetOptions} />
<Stack.Screen name="language-picker" options={sheetOptions} />
<Stack.Screen name="transcription-model-picker" options={sheetOptions} />
Expand Down
81 changes: 81 additions & 0 deletions apps/mobile/src/app/(app)/agent-chat/profile-picker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { type Href, useRouter } from 'expo-router';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';

import { ProfilePickerSheet } from '@/components/agents/profile-picker-sheet';
import { resolveSessionProfilePicker } from '@/components/agents/session-profile-picker-model';
import { useEffectiveAgentProfile } from '@/components/agents/use-effective-agent-profile';
import { PickerSheet } from '@/components/picker-sheet';
import { profilePickerSlot, UNFENCED_ROUTE_KEY, useRouteRegistry } from '@/lib/route-registry';

const PROFILES_HREF = '/(app)/(tabs)/(3_profile)/profiles' as Href;

/**
* The new-session profile picker, presented as the standard native formSheet.
* The bridge carries the current pick and the context; the route re-queries the
* same profiles scope so an open sheet is fresh and a failure has a Retry.
*/
export default function ProfilePickerScreen() {
const router = useRouter();
const { t } = useTranslation();
useRouteRegistry(UNFENCED_ROUTE_KEY);
// Lazy init reads the slot synchronously on first render — no effect, no
// "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
);

if (!bridge) {
return (
<PickerSheet
title={t('agentChat.newSession.pickProfile')}
onDone={() => {
router.back();
}}
scrollable={false}
expired
/>
);
}

const picker = resolveSessionProfilePicker({
profiles: allProfiles,
repoBindingProfileId: bridge.repoBindingProfileId,
effectiveDefaultProfileId: effectiveDefaultId,
selectedOverrideProfileId: bridge.selectedOverrideProfileId,
});

function close() {
profilePickerSlot.clear(UNFENCED_ROUTE_KEY);
router.back();
}

return (
<ProfilePickerSheet
candidates={picker.candidates}
hasProfiles={allProfiles.length > 0}
selectedOverrideProfileId={bridge.selectedOverrideProfileId}
// 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}
isLoading={isLoading}
isError={isError}
needsAttention={picker.overrideNeedsAttention}
onSelect={id => {
bridge.onSelect(id);
close();
}}
onManageProfiles={() => {
close();
router.push(PROFILES_HREF);
}}
onRetry={() => {
void refetch();
}}
onClose={close}
/>
);
}
Loading