Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
4fbbf6a
feat(app-shared): add purchase presentation and moderation contracts
iscekic Aug 17, 2026
f41a6f9
feat(kilo-pass): server-authoritative purchase presentation and prefl…
iscekic Aug 17, 2026
9718e42
fix(kilo-pass): add preflight ended/Play coverage and shared copy con…
iscekic Aug 17, 2026
9aac533
feat(kilo-pass): run store purchase completion on the operation ledger
iscekic Aug 17, 2026
74e57bd
feat(code-review): remove roast from public review styles
iscekic Aug 17, 2026
be06e7a
feat(seed): add kilo-pass Stripe subscription seed
iscekic Aug 17, 2026
17f782d
feat(mobile): server-driven Kilo Pass presentation with a single IAP …
iscekic Aug 17, 2026
4e579bd
test(kilo-pass): cover Apple mismatch catch and failed-row replay
iscekic Aug 17, 2026
0a3dd0a
fix(seed): validate Stripe env before delete and upsert the subscription
iscekic Aug 17, 2026
b114722
feat(moderation): content moderation reports, blocks, mutes, and Term…
iscekic Aug 17, 2026
bf87193
test(pr-review): seed Terms for mutation tests and cover the gate
iscekic Aug 18, 2026
3e52e54
feat(mobile): add Report AI response to message details
iscekic Aug 18, 2026
306af64
feat(mobile): PR discussion report, mute, block, and Terms gate
iscekic Aug 18, 2026
f875832
fix(mobile): add Retry CTA to report AI retryable failures
iscekic Aug 18, 2026
8d57872
fix(mobile): add Retry CTAs and four-state coverage to PR moderation
iscekic Aug 18, 2026
5100bcd
chore(format): apply oxfmt to changed files
iscekic Aug 18, 2026
c43842b
fix(mobile): guard web URL before opening Kilo Pass management
iscekic Aug 18, 2026
4af76fc
fix(mobile): handle preflight failure states on Kilo Pass screen
iscekic Aug 18, 2026
ff8c58f
refactor(mobile): deduplicate tRPC error helpers and dead branches
iscekic Aug 18, 2026
8c12193
fix(moderation): normalize GitHub logins and drop unpersisted appeal …
iscekic Aug 18, 2026
e045c3c
fix(kilo-pass): guard settle and map CONFLICT to a friendly message
iscekic Aug 18, 2026
bf63d51
fix(mobile): recover purchases from server SKUs and guard preflight
iscekic Aug 18, 2026
0fd50b8
fix(mobile): settle Terms gate and make login filtering case-insensitive
iscekic Aug 18, 2026
d0617b4
fix(seed): delete conflicting rows before insert and drop unused export
iscekic Aug 18, 2026
dfcc949
fix(kilo-pass): type the settle mock so the root typecheck passes
iscekic Aug 18, 2026
c7e6358
chore(format): wrap settleOperation mock per oxfmt
iscekic Aug 18, 2026
9da7842
chore: retrigger review
iscekic Aug 18, 2026
8f49a66
Merge branch 'main' into audit-w4b-commerce-moderation-5698
iscekic Aug 18, 2026
26fd417
fix(mobile): honest Terms gate failure and cross-account purchase block
iscekic Aug 18, 2026
25aa99a
Merge remote-tracking branch 'origin/main' into audit-w4b-commerce-mo…
iscekic Aug 18, 2026
e1ace20
revert(code-review): restore roast as a public review style
iscekic Aug 18, 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
@@ -1,26 +1,29 @@
import { useLocalSearchParams } from 'expo-router';

import { OptionList } from '@/components/code-reviewer/option-list';
import { REVIEW_STYLES, type ReviewerPlatform } from '@/lib/code-reviewer-config';
import { type ReviewerPlatform } from '@/lib/code-reviewer-config';
import { PUBLIC_REVIEW_STYLES } from '@kilocode/app-shared/code-review';
import { useReviewConfig, useSaveReviewConfig } from '@/lib/hooks/use-code-reviewer';

const DESCRIPTIONS = {
strict: 'Flag everything, hold a high bar',
balanced: 'Meaningful findings without noise',
lenient: 'Only serious problems',
roast: 'Brutally honest, entertainingly so',
} as const;

export default function ReviewStyleRoute() {
const { scope, platform } = useLocalSearchParams<{ scope: string; platform: ReviewerPlatform }>();
const { data } = useReviewConfig(scope, platform);
const save = useSaveReviewConfig(scope, platform);

// A legacy 'roast' config has no matching public option; treat it as unselected.
const selected = data?.reviewStyle === 'roast' ? undefined : data?.reviewStyle;

return (
<OptionList
title="Review style"
options={REVIEW_STYLES}
selected={data?.reviewStyle}
options={PUBLIC_REVIEW_STYLES}
selected={selected}
descriptions={DESCRIPTIONS}
disabled={data == null}
// eslint-disable-next-line typescript-eslint/promise-function-async -- conflicting require-await rule
Expand Down
199 changes: 98 additions & 101 deletions apps/mobile/src/app/(app)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { attemptLogoutReconciliation } from '@/lib/auth/logout-reconciliation';
import { useFormSheetDetents } from '@/lib/form-sheet';
import { useThemeColors } from '@/lib/hooks/use-theme-colors';
import { useCurrentUserId } from '@/lib/hooks/use-current-user-id';
import { StoreKiloPassPurchaseProvider } from '@/lib/kilo-pass/use-store-kilo-pass-purchase';
import { CachePersistenceMount } from '@/lib/persist/cache-persistence-mount';

/**
Expand Down Expand Up @@ -57,108 +56,106 @@ export default function AppLayout() {
<SharePayloadNavigator />
<KiloChatProvider>
<KiloChatPresenceMount>
<StoreKiloPassPurchaseProvider>
<Stack
screenOptions={{
contentStyle: { backgroundColor: colors.background },
<Stack
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="agent-chat/model-picker"
options={{
presentation: 'formSheet',
sheetAllowedDetents: [0.5, fullSheetDetent],
sheetGrabberVisible: true,
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="agent-chat/model-picker"
options={{
presentation: 'formSheet',
sheetAllowedDetents: [0.5, fullSheetDetent],
sheetGrabberVisible: true,
headerShown: false,
}}
/>
<Stack.Screen
name="agent-chat/repo-picker"
options={{
presentation: 'formSheet',
sheetAllowedDetents: [0.5, fullSheetDetent],
sheetGrabberVisible: true,
headerShown: false,
}}
/>
<Stack.Screen
name="agent-chat/mode-picker"
options={{
presentation: 'formSheet',
sheetAllowedDetents: [0.5],
sheetGrabberVisible: true,
headerShown: false,
}}
/>
<Stack.Screen
name="agent-chat/instance-picker"
options={{
presentation: 'formSheet',
sheetAllowedDetents: [0.5, fullSheetDetent],
sheetGrabberVisible: true,
headerShown: false,
}}
/>
<Stack.Screen
name="agent-chat/continue-picker"
options={{
presentation: 'formSheet',
sheetAllowedDetents: [0.5, fullSheetDetent],
sheetGrabberVisible: true,
headerShown: false,
}}
/>
<Stack.Screen
name="share-gate"
options={{
presentation: 'formSheet',
sheetAllowedDetents: [0.5, fullSheetDetent],
sheetGrabberVisible: true,
headerShown: false,
}}
/>
<Stack.Screen
name="kilo-pass"
options={{
presentation: 'modal',
headerShown: false,
}}
/>
<Stack.Screen
name="onboarding"
options={{
presentation: 'modal',
headerShown: false,
gestureEnabled: false,
}}
/>
<Stack.Screen
name="consent"
options={{
presentation: 'modal',
headerShown: false,
gestureEnabled: false,
}}
/>
<Stack.Screen
name="consent-details"
options={{
headerShown: false,
}}
/>
</Stack>
</StoreKiloPassPurchaseProvider>
/>
<Stack.Screen
name="agent-chat/repo-picker"
options={{
presentation: 'formSheet',
sheetAllowedDetents: [0.5, fullSheetDetent],
sheetGrabberVisible: true,
headerShown: false,
}}
/>
<Stack.Screen
name="agent-chat/mode-picker"
options={{
presentation: 'formSheet',
sheetAllowedDetents: [0.5],
sheetGrabberVisible: true,
headerShown: false,
}}
/>
<Stack.Screen
name="agent-chat/instance-picker"
options={{
presentation: 'formSheet',
sheetAllowedDetents: [0.5, fullSheetDetent],
sheetGrabberVisible: true,
headerShown: false,
}}
/>
<Stack.Screen
name="agent-chat/continue-picker"
options={{
presentation: 'formSheet',
sheetAllowedDetents: [0.5, fullSheetDetent],
sheetGrabberVisible: true,
headerShown: false,
}}
/>
<Stack.Screen
name="share-gate"
options={{
presentation: 'formSheet',
sheetAllowedDetents: [0.5, fullSheetDetent],
sheetGrabberVisible: true,
headerShown: false,
}}
/>
<Stack.Screen
name="kilo-pass"
options={{
presentation: 'modal',
headerShown: false,
}}
/>
<Stack.Screen
name="onboarding"
options={{
presentation: 'modal',
headerShown: false,
gestureEnabled: false,
}}
/>
<Stack.Screen
name="consent"
options={{
presentation: 'modal',
headerShown: false,
gestureEnabled: false,
}}
/>
<Stack.Screen
name="consent-details"
options={{
headerShown: false,
}}
/>
</Stack>
</KiloChatPresenceMount>
</KiloChatProvider>
</UserWebConnectionProvider>
Expand Down
74 changes: 72 additions & 2 deletions apps/mobile/src/components/agents/message-details-sheet.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import { type StoredMessage } from '@kilocode/cloud-agent-sdk';
import { useMemo } from 'react';
import { Modal, Pressable, ScrollView, View } from 'react-native';
import { useMutation } from '@tanstack/react-query';
import { useMemo, useState } from 'react';
import { Alert, Modal, Pressable, ScrollView, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

import { announcingToast } from '@/lib/a11y/announcing-toast';
import { useTRPC } from '@/lib/trpc';
import { SheetHeader } from '@/components/sheet-header';
import { Text } from '@/components/ui/text';
import { type SessionModelOption } from '@/lib/hooks/use-session-model-options';

import { formatExactTokens } from './context-usage-display';
import { handleMessageDetailsCopy } from './message-details-copy';
import { getMessageDetailsContent } from './message-details-content';
import {
buildReportAiResponseErrorToast,
buildReportAiResponseInput,
classifyReportAiResponseFailure,
reportAiResponseSubmittedToast,
shouldShowReportAiResponse,
} from './report-ai-response';

type MessageDetailsSheetProps = {
visible: boolean;
Expand All @@ -25,15 +35,61 @@ export function MessageDetailsSheet({
onClose,
}: Readonly<MessageDetailsSheetProps>) {
const insets = useSafeAreaInsets();
const trpc = useTRPC();
const [reportedMessageId, setReportedMessageId] = useState<string | null>(null);
const content = useMemo(
() => (message ? getMessageDetailsContent(message, modelOptions) : null),
[message, modelOptions]
);

const reportMutation = useMutation(
trpc.moderation.reportContent.mutationOptions({
onSuccess: (result, input) => {
setReportedMessageId(input.targetId);
announcingToast.success(reportAiResponseSubmittedToast(result.receiptId));
},
onError: (error, input) => {
const failure = classifyReportAiResponseFailure(error);
const errorToast = buildReportAiResponseErrorToast(failure, () => {
reportMutation.mutate(input);
});
announcingToast.error(
errorToast.message,
errorToast.action ? { action: errorToast.action } : undefined
);
},
})
);

const showReport =
message !== null &&
shouldShowReportAiResponse(message) &&
message.info.id !== reportedMessageId;

const handleCopy = () => {
handleMessageDetailsCopy(content?.copyableText);
};

const handleReport = () => {
if (!message) {
return;
}
const input = buildReportAiResponseInput(message);
if (!input) {
return;
}
Alert.alert('Report AI response', 'Report this response for review?', [
{ text: 'Cancel', style: 'cancel' },
{
text: 'Report',
style: 'destructive',
onPress: () => {
reportMutation.mutate(input);
},
},
]);
};

return (
<Modal
visible={visible}
Expand All @@ -60,6 +116,20 @@ export function MessageDetailsSheet({
</Pressable>
) : null}

{showReport ? (
<Pressable
onPress={handleReport}
accessibilityRole="button"
accessibilityLabel="Report AI response"
className="mb-6 rounded-md border border-border px-4 py-3 active:opacity-70"
testID="message-details-report"
>
<Text className="text-center text-base font-medium text-foreground">
Report AI response
</Text>
</Pressable>
) : null}

<View className="gap-4">
<Row label="Role">
<Text className="text-base font-medium text-foreground">{content.roleLabel}</Text>
Expand Down
Loading
Loading