Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
183 changes: 94 additions & 89 deletions apps/mobile/src/app/(app)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { useSecurityLifecycleInvalidation } from '@/lib/hooks/use-security-lifec
import { CachePersistenceMount } from '@/lib/persist/cache-persistence-mount';
import { SystemSearchIndexMount } from '@/lib/system-search-index-mount';
import { ToolSummaryTranslationRetryMount } from '@/lib/tool-summary-translation/tool-summary-translation-retry-mount';
import { TabBarLabelContext } from '@/lib/tab-bar-clearance';
import { useTRPC } from '@/lib/trpc';

/**
Expand Down Expand Up @@ -134,97 +135,101 @@ export default function AppLayout() {
<TourAutoOpen />
<KiloChatProvider>
<KiloChatPresenceMount>
<Stack
screenLayout={appUnlockScreenLayout}
screenOptions={{
contentStyle: { backgroundColor: colors.background },
headerShown: false,
headerStyle: { backgroundColor: colors.background },
headerTintColor: colors.foreground,
}}
>
<Stack.Screen name="(tabs)" />
<Stack.Screen name="pr-review/index" options={{ headerShown: false }} />
<Stack.Screen
name="pr-review/[owner]/[repo]/[number]"
options={{ headerShown: false }}
/>
<Stack.Screen name="agent-chat/new" options={{ headerShown: false }} />
<Stack.Screen name="agent-chat/[session-id]" />
<Stack.Screen name="session-changes/[session-id]" options={{ headerShown: false }} />
<Stack.Screen name="agent-chat/model-picker" options={searchSheetOptions} />
<Stack.Screen name="agent-chat/repo-picker" options={searchSheetOptions} />
<Stack.Screen name="agent-chat/branch-picker" options={sheetOptions} />
<Stack.Screen
name="agent-chat/mode-picker"
options={{ ...sheetOptions, sheetAllowedDetents: [0.5] }}
/>
<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="todos/[list-id]" />
<Stack.Screen name="todos/item/[item-id]" />
<Stack.Screen
name="todos/list-settings"
options={{ ...sheetOptions, gestureEnabled: false }}
/>
<Stack.Screen name="agent-briefing/index" />
<Stack.Screen name="agent-briefing/create" />
<Stack.Screen name="agent-briefing/[report-id]" />
<Stack.Screen name="agent-briefing/settings" />
<Stack.Screen name="share-gate" options={sheetOptions} />
<Stack.Screen name="language-picker" options={searchSheetOptions} />
<Stack.Screen name="transcription-model-picker" options={sheetOptions} />
<Stack.Screen name="voice-language-picker" options={searchSheetOptions} />
<Stack.Screen
name="kilo-pass"
options={{
presentation: 'modal',
{/* The root stack draws no tab bar: the tabs live inside `(tabs)`,
whose layout publishes its own decision for every tab screen. */}
<TabBarLabelContext value={null}>
<Stack
screenLayout={appUnlockScreenLayout}
screenOptions={{
contentStyle: { backgroundColor: colors.background },
headerShown: false,
headerStyle: { backgroundColor: colors.background },
headerTintColor: colors.foreground,
}}
/>
<Stack.Screen
name="credits"
options={{
presentation: 'modal',
headerShown: false,
}}
/>
<Stack.Screen
name="onboarding"
options={{
presentation: 'modal',
headerShown: false,
gestureEnabled: false,
}}
/>
<Stack.Screen
name="tour"
options={{
presentation: 'modal',
headerShown: false,
// A swipe-down dismissal would bypass the tour's own dismissal
// (which records the per-account decision). Match `onboarding`
// and `consent`: the only exits are the tour's own controls and
// Android hardware Back, both of which record first.
gestureEnabled: false,
}}
/>
<Stack.Screen
name="consent"
options={{
presentation: 'modal',
headerShown: false,
gestureEnabled: false,
}}
/>
<Stack.Screen
name="consent-details"
options={{
headerShown: false,
}}
/>
</Stack>
>
<Stack.Screen name="(tabs)" />
<Stack.Screen name="pr-review/index" options={{ headerShown: false }} />
<Stack.Screen
name="pr-review/[owner]/[repo]/[number]"
options={{ headerShown: false }}
/>
<Stack.Screen name="agent-chat/new" options={{ headerShown: false }} />
<Stack.Screen name="agent-chat/[session-id]" />
<Stack.Screen name="session-changes/[session-id]" options={{ headerShown: false }} />
<Stack.Screen name="agent-chat/model-picker" options={searchSheetOptions} />
<Stack.Screen name="agent-chat/repo-picker" options={searchSheetOptions} />
<Stack.Screen name="agent-chat/branch-picker" options={sheetOptions} />
<Stack.Screen
name="agent-chat/mode-picker"
options={{ ...sheetOptions, sheetAllowedDetents: [0.5] }}
/>
<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="todos/[list-id]" />
<Stack.Screen name="todos/item/[item-id]" />
<Stack.Screen
name="todos/list-settings"
options={{ ...sheetOptions, gestureEnabled: false }}
/>
<Stack.Screen name="agent-briefing/index" />
<Stack.Screen name="agent-briefing/create" />
<Stack.Screen name="agent-briefing/[report-id]" />
<Stack.Screen name="agent-briefing/settings" />
<Stack.Screen name="share-gate" options={sheetOptions} />
<Stack.Screen name="language-picker" options={searchSheetOptions} />
<Stack.Screen name="transcription-model-picker" options={sheetOptions} />
<Stack.Screen name="voice-language-picker" options={searchSheetOptions} />
<Stack.Screen
name="kilo-pass"
options={{
presentation: 'modal',
headerShown: false,
}}
/>
<Stack.Screen
name="credits"
options={{
presentation: 'modal',
headerShown: false,
}}
/>
<Stack.Screen
name="onboarding"
options={{
presentation: 'modal',
headerShown: false,
gestureEnabled: false,
}}
/>
<Stack.Screen
name="tour"
options={{
presentation: 'modal',
headerShown: false,
// A swipe-down dismissal would bypass the tour's own dismissal
// (which records the per-account decision). Match `onboarding`
// and `consent`: the only exits are the tour's own controls and
// Android hardware Back, both of which record first.
gestureEnabled: false,
}}
/>
<Stack.Screen
name="consent"
options={{
presentation: 'modal',
headerShown: false,
gestureEnabled: false,
}}
/>
<Stack.Screen
name="consent-details"
options={{
headerShown: false,
}}
/>
</Stack>
</TabBarLabelContext>
</KiloChatPresenceMount>
</KiloChatProvider>
</UserWebConnectionProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default function CodeReviewerPlatformLayout() {
const params = useValidatedReviewerRouteParams();

if (!params) {
return <InvalidRouteState backTo={'/(app)/(tabs)/(3_profile)/code-reviewer' as Href} />;
return <InvalidRouteState backTo={'/(app)/code-reviewer' as Href} />;
}

return <Stack screenOptions={{ headerShown: false }} />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,27 +50,27 @@ beforeEach(() => {
});

describe('CodeReviewerScopeRoute invalid scope', () => {
it('renders InvalidRouteState with the profile backTo when scope is undefined', () => {
it('renders InvalidRouteState with the Code Reviewer backTo when scope is undefined', () => {
useLocalSearchParamsMock.mockReturnValue({ scope: undefined });
const renderer = mountRoute();

const invalid = findByType(renderer.root, 'InvalidRouteState');
expect(invalid).toHaveLength(1);
expect(propOf(invalid[0], 'backTo')).toBe('/(app)/(tabs)/(3_profile)');
expect(propOf(invalid[0], 'backTo')).toBe('/(app)/code-reviewer');
expect(findByType(renderer.root, 'PlatformListScreen')).toHaveLength(0);

act(() => {
renderer.unmount();
});
});

it('renders InvalidRouteState with the profile backTo when scope is an array', () => {
it('renders InvalidRouteState with the Code Reviewer backTo when scope is an array', () => {
useLocalSearchParamsMock.mockReturnValue({ scope: ['personal', 'org'] });
const renderer = mountRoute();

const invalid = findByType(renderer.root, 'InvalidRouteState');
expect(invalid).toHaveLength(1);
expect(propOf(invalid[0], 'backTo')).toBe('/(app)/(tabs)/(3_profile)');
expect(propOf(invalid[0], 'backTo')).toBe('/(app)/code-reviewer');
expect(findByType(renderer.root, 'PlatformListScreen')).toHaveLength(0);

act(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default function CodeReviewerScopeRoute() {
const scope = parseParam(rawScope);

if (!scope) {
return <InvalidRouteState backTo={'/(app)/(tabs)/(3_profile)' as Href} />;
return <InvalidRouteState backTo={'/(app)/code-reviewer' as Href} />;
}

return <PlatformListScreen scope={scope} />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,27 +50,27 @@ beforeEach(() => {
});

describe('CodeReviewerManualReviewRoute invalid scope', () => {
it('renders InvalidRouteState with the profile backTo when scope is undefined', () => {
it('renders InvalidRouteState with the Code Reviewer backTo when scope is undefined', () => {
useLocalSearchParamsMock.mockReturnValue({ scope: undefined });
const renderer = mountRoute();

const invalid = findByType(renderer.root, 'InvalidRouteState');
expect(invalid).toHaveLength(1);
expect(propOf(invalid[0], 'backTo')).toBe('/(app)/(tabs)/(3_profile)');
expect(propOf(invalid[0], 'backTo')).toBe('/(app)/code-reviewer');
expect(findByType(renderer.root, 'ManualReviewScreen')).toHaveLength(0);

act(() => {
renderer.unmount();
});
});

it('renders InvalidRouteState with the profile backTo when scope is an array', () => {
it('renders InvalidRouteState with the Code Reviewer backTo when scope is an array', () => {
useLocalSearchParamsMock.mockReturnValue({ scope: ['personal', 'org'] });
const renderer = mountRoute();

const invalid = findByType(renderer.root, 'InvalidRouteState');
expect(invalid).toHaveLength(1);
expect(propOf(invalid[0], 'backTo')).toBe('/(app)/(tabs)/(3_profile)');
expect(propOf(invalid[0], 'backTo')).toBe('/(app)/code-reviewer');
expect(findByType(renderer.root, 'ManualReviewScreen')).toHaveLength(0);

act(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default function CodeReviewerManualReviewRoute() {
const scope = parseParam(rawScope);

if (!scope) {
return <InvalidRouteState backTo={'/(app)/(tabs)/(3_profile)' as Href} />;
return <InvalidRouteState backTo={'/(app)/code-reviewer' as Href} />;
}

return <ManualReviewScreen scope={scope} />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,27 +50,27 @@ beforeEach(() => {
});

describe('CodeReviewerReviewDetailRoute invalid scope', () => {
it('renders InvalidRouteState with the profile backTo when scope is undefined', () => {
it('renders InvalidRouteState with the Code Reviewer backTo when scope is undefined', () => {
useLocalSearchParamsMock.mockReturnValue({ scope: undefined, id: 'rev-1' });
const renderer = mountRoute();

const invalid = findByType(renderer.root, 'InvalidRouteState');
expect(invalid).toHaveLength(1);
expect(propOf(invalid[0], 'backTo')).toBe('/(app)/(tabs)/(3_profile)');
expect(propOf(invalid[0], 'backTo')).toBe('/(app)/code-reviewer');
expect(findByType(renderer.root, 'ReviewDetailScreen')).toHaveLength(0);

act(() => {
renderer.unmount();
});
});

it('renders InvalidRouteState with the profile backTo when scope is an array', () => {
it('renders InvalidRouteState with the Code Reviewer backTo when scope is an array', () => {
useLocalSearchParamsMock.mockReturnValue({ scope: ['personal', 'org'], id: 'rev-1' });
const renderer = mountRoute();

const invalid = findByType(renderer.root, 'InvalidRouteState');
expect(invalid).toHaveLength(1);
expect(propOf(invalid[0], 'backTo')).toBe('/(app)/(tabs)/(3_profile)');
expect(propOf(invalid[0], 'backTo')).toBe('/(app)/code-reviewer');
expect(findByType(renderer.root, 'ReviewDetailScreen')).toHaveLength(0);

act(() => {
Expand All @@ -86,9 +86,7 @@ describe('CodeReviewerReviewDetailRoute invalid id', () => {

const invalid = findByType(renderer.root, 'InvalidRouteState');
expect(invalid).toHaveLength(1);
expect(propOf(invalid[0], 'backTo')).toBe(
'/(app)/(tabs)/(3_profile)/code-reviewer/personal/reviews'
);
expect(propOf(invalid[0], 'backTo')).toBe('/(app)/code-reviewer/personal/reviews');
expect(findByType(renderer.root, 'ReviewDetailScreen')).toHaveLength(0);

act(() => {
Expand All @@ -102,9 +100,7 @@ describe('CodeReviewerReviewDetailRoute invalid id', () => {

const invalid = findByType(renderer.root, 'InvalidRouteState');
expect(invalid).toHaveLength(1);
expect(propOf(invalid[0], 'backTo')).toBe(
'/(app)/(tabs)/(3_profile)/code-reviewer/personal/reviews'
);
expect(propOf(invalid[0], 'backTo')).toBe('/(app)/code-reviewer/personal/reviews');
expect(findByType(renderer.root, 'ReviewDetailScreen')).toHaveLength(0);

act(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@ export default function CodeReviewerReviewDetailRoute() {

if (!scope || !reviewId) {
const backTo = (
scope
? `/(app)/(tabs)/(3_profile)/code-reviewer/${scope}/reviews`
: '/(app)/(tabs)/(3_profile)'
scope ? `/(app)/code-reviewer/${scope}/reviews` : '/(app)/code-reviewer'
) as Href;
return <InvalidRouteState backTo={backTo} />;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,27 +50,27 @@ beforeEach(() => {
});

describe('CodeReviewerReviewListRoute invalid scope', () => {
it('renders InvalidRouteState with the profile backTo when scope is undefined', () => {
it('renders InvalidRouteState with the Code Reviewer backTo when scope is undefined', () => {
useLocalSearchParamsMock.mockReturnValue({ scope: undefined });
const renderer = mountRoute();

const invalid = findByType(renderer.root, 'InvalidRouteState');
expect(invalid).toHaveLength(1);
expect(propOf(invalid[0], 'backTo')).toBe('/(app)/(tabs)/(3_profile)');
expect(propOf(invalid[0], 'backTo')).toBe('/(app)/code-reviewer');
expect(findByType(renderer.root, 'ReviewListScreen')).toHaveLength(0);

act(() => {
renderer.unmount();
});
});

it('renders InvalidRouteState with the profile backTo when scope is an array', () => {
it('renders InvalidRouteState with the Code Reviewer backTo when scope is an array', () => {
useLocalSearchParamsMock.mockReturnValue({ scope: ['personal', 'org'] });
const renderer = mountRoute();

const invalid = findByType(renderer.root, 'InvalidRouteState');
expect(invalid).toHaveLength(1);
expect(propOf(invalid[0], 'backTo')).toBe('/(app)/(tabs)/(3_profile)');
expect(propOf(invalid[0], 'backTo')).toBe('/(app)/code-reviewer');
expect(findByType(renderer.root, 'ReviewListScreen')).toHaveLength(0);

act(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default function CodeReviewerReviewListRoute() {
const scope = parseParam(rawScope);

if (!scope) {
return <InvalidRouteState backTo={'/(app)/(tabs)/(3_profile)' as Href} />;
return <InvalidRouteState backTo={'/(app)/code-reviewer' as Href} />;
}

return <ReviewListScreen scope={scope} />;
Expand Down
Loading
Loading