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
19 changes: 18 additions & 1 deletion apps/mobile/.oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@
},
"ignorePatterns": ["dist/*", "node_modules/**"],
"rules": {
"no-restricted-globals": [
"error",
{
"name": "Intl",
"message": "Use @/lib/intl-cache so Hermes support stays explicit and tested."
}
],
"no-restricted-imports": [
"error",
{
Expand Down Expand Up @@ -253,14 +260,24 @@
"anti-slop/no-reflect-get": "off",
"anti-slop/no-runtime-typeof": "off",
"anti-slop/no-unknown-returns": "off",
"no-restricted-globals": "off",
"zod-utils/no-inline-zod-schema": "off"
}
},
{
"files": ["src/i18n/catalogs.ts"],
"files": ["src/i18n/catalogs.ts", "src/lib/relative-time-locales.ts"],
"rules": {
"@typescript-eslint/no-require-imports": "off",
"@typescript-eslint/no-var-requires": "off",
"unicorn/prefer-module": "off"
}
},
{
"files": ["src/lib/intl-cache.ts"],
"rules": {
"@typescript-eslint/no-require-imports": "off",
"@typescript-eslint/no-var-requires": "off",
"no-restricted-globals": "off",
"unicorn/prefer-module": "off"
}
},
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const config: ExpoConfig = {
name: 'Kilo',
owner: 'kilocode',
slug: 'kilo-app',
version: '1.0.5',
version: '1.0.6',
// Portrait-only is an accepted, documented product deviation from WCAG 1.3.4
// (Orientation). Landscape layouts and iPad split-view/multitasking are out
// of scope; `ios.requireFullScreen` below enforces that. This is not claimed
Expand Down
6 changes: 6 additions & 0 deletions apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@
"@expo-google-fonts/jetbrains-mono": "0.4.1",
"@expo/app-integrity": "57.0.1",
"@expo/react-native-action-sheet": "4.1.1",
"@formatjs/intl-durationformat": "^0.10.18",
"@formatjs/intl-listformat": "^8.3.13",
"@formatjs/intl-locale": "^5.3.10",
"@formatjs/intl-numberformat": "^9.4.0",
"@formatjs/intl-relativetimeformat": "^12.3.13",
"@formatjs/intl-segmenter": "^12.2.13",
"@kilocode/app-shared": "workspace:*",
"@kilocode/cloud-agent-sdk": "workspace:*",
"@kilocode/event-service": "workspace:*",
Expand Down
39 changes: 18 additions & 21 deletions apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,47 +41,44 @@ export function buildGitHubInstallOutcomeAlert(
switch (result.kind) {
case 'success': {
return {
title: 'GitHub App installed',
message: 'Your repositories are now connected.',
buttons: [{ text: 'Continue' }],
title: i18n.t('agents.githubInstall.installedTitle'),
message: i18n.t('agents.githubInstall.installedMessage'),
buttons: [{ text: i18n.t('common.continue') }],
};
}
case 'pending': {
return {
title: 'Awaiting admin approval',
message: 'An organization admin must approve the installation request.',
buttons: [{ text: 'Done' }],
title: i18n.t('agents.githubInstall.pendingTitle'),
message: i18n.t('agents.githubInstall.pendingMessage'),
buttons: [{ text: i18n.t('common.done') }],
};
}
case 'error': {
if (result.code === 'install_state_user_mismatch') {
return {
title: 'Account mismatch',
message:
'This connection was started from the Kilo App signed in as a different account. Sign in to the web with that account, or start again from the app.',
buttons: [{ text: 'Back' }],
title: i18n.t('agents.githubInstall.accountMismatchTitle'),
message: i18n.t('agents.githubInstall.accountMismatchMessage'),
buttons: [{ text: i18n.t('common.back') }],
};
}
if (result.code === 'not_installation_admin') {
return {
title: 'Cannot complete installation',
message:
'Only a GitHub admin of that account can connect it. Ask an organization admin to install Kilo.',
buttons: [{ text: 'Back' }],
title: i18n.t('agents.githubInstall.cannotCompleteTitle'),
message: i18n.t('agents.githubInstall.notAdminMessage'),
buttons: [{ text: i18n.t('common.back') }],
};
}
if (result.code === 'installation_already_claimed') {
return {
title: 'Cannot complete installation',
message:
'That GitHub installation is already connected to another Kilo account. Disconnect it there first.',
buttons: [{ text: 'Back' }],
title: i18n.t('agents.githubInstall.cannotCompleteTitle'),
message: i18n.t('agents.githubInstall.alreadyClaimedMessage'),
buttons: [{ text: i18n.t('common.back') }],
};
}
return {
title: 'Installation did not complete',
message: 'The GitHub App installation was not completed. Try again from the app.',
buttons: [{ text: 'Try again', onPress: onRetry }],
title: i18n.t('agents.githubInstall.didNotCompleteTitle'),
message: i18n.t('agents.githubInstall.didNotCompleteMessage'),
buttons: [{ text: i18n.t('common.tryAgain'), onPress: onRetry }],
};
}
default: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ export default function FocusAreasRoute() {
const selected = data?.focusAreas ?? [];
const disabled = data == null;
const { t } = useTranslation();
const areaLabels = {
security: 'codeReviewer.focusArea.security',
performance: 'codeReviewer.focusArea.performance',
bugs: 'codeReviewer.focusArea.bugs',
style: 'codeReviewer.focusArea.style',
testing: 'codeReviewer.focusArea.testing',
documentation: 'codeReviewer.focusArea.documentation',
} as const;

const toggleArea = (area: string) => {
// Read the cache at call time, not the render-time snapshot above, so
Expand All @@ -43,7 +51,7 @@ export default function FocusAreasRoute() {
{REVIEW_FOCUS_AREAS.map(area => (
<ChoiceRow
key={area}
label={area}
label={t(areaLabels[area])}
multi
selected={selected.includes(area)}
disabled={disabled}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ export default function GateThresholdRoute() {
const { data } = useReviewConfig(scope, platform);
const save = useSaveReviewConfig(scope, platform);
const { t } = useTranslation();
const labels = {
off: t('codeReviewer.gateThreshold.off'),
all: t('codeReviewer.gateThreshold.all'),
warning: t('codeReviewer.gateThreshold.warning'),
critical: t('codeReviewer.gateThreshold.critical'),
} as const;
const descriptions = {
off: t('codeReviewer.gate.off'),
all: t('codeReviewer.gate.all'),
Expand All @@ -22,6 +28,7 @@ export default function GateThresholdRoute() {
title={t('codeReviewer.gate.title')}
options={GATE_THRESHOLDS}
selected={data?.gateThreshold}
labels={labels}
descriptions={descriptions}
disabled={data == null}
// eslint-disable-next-line typescript-eslint/promise-function-async -- conflicting require-await rule
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ export default function ReviewStyleRoute() {
const { data } = useReviewConfig(scope, platform);
const save = useSaveReviewConfig(scope, platform);
const { t } = useTranslation();
const labels = {
strict: t('codeReviewer.reviewStyle.strict'),
balanced: t('codeReviewer.reviewStyle.balanced'),
lenient: t('codeReviewer.reviewStyle.lenient'),
roast: t('codeReviewer.reviewStyle.roast'),
} as const;
const descriptions = {
strict: t('codeReviewer.style.strict'),
balanced: t('codeReviewer.style.balanced'),
Expand All @@ -22,6 +28,7 @@ export default function ReviewStyleRoute() {
title={t('codeReviewer.style.title')}
options={REVIEW_STYLES}
selected={data?.reviewStyle}
labels={labels}
descriptions={descriptions}
disabled={data == null}
// eslint-disable-next-line typescript-eslint/promise-function-async -- conflicting require-await rule
Expand Down
19 changes: 13 additions & 6 deletions apps/mobile/src/app/(app)/kiloclaw/[instance-id]/billing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ import { useThemeColors } from '@/lib/hooks/use-theme-colors';
import { useTRPC } from '@/lib/trpc';
import { cn } from '@/lib/utils';

const SUBSCRIPTION_PLAN_KEYS = {
commit: 'kiloclaw.billing.planName.commit',
standard: 'kiloclaw.billing.planName.standard',
} satisfies Record<string, string>;

/** Looks up a possibly-unknown key in a literal dictionary without widening its type. */
function lookup<V>(dictionary: Readonly<Record<string, V>>, key: string): V | undefined {
return (dictionary as Readonly<Record<string, V | undefined>>)[key];
}

function DetailRow({
label,
value,
Expand Down Expand Up @@ -132,8 +142,8 @@ function PlanDetails({
return <FinalCommitTermDetails billing={billing} />;
}
if (billing.subscription) {
const planName =
billing.subscription.plan.charAt(0).toUpperCase() + billing.subscription.plan.slice(1);
const planKey = lookup(SUBSCRIPTION_PLAN_KEYS, billing.subscription.plan);
const planName = planKey ? t(planKey) : billing.subscription.plan;
const cancelling = billing.subscription.cancelAtPeriodEnd;
return (
<View>
Expand Down Expand Up @@ -163,10 +173,7 @@ function PlanDetails({
);
}
if (billing.earlybird) {
const daysText =
billing.earlybird.daysRemaining === 1
? t('kiloclaw.billing.oneDayLeft')
: t('kiloclaw.billing.daysLeft', { count: billing.earlybird.daysRemaining });
const daysText = formatRemainingDays(billing.earlybird.daysRemaining);
return (
<View>
<DetailRow label={t('kiloclaw.billing.plan')} value={t('kiloclaw.billing.earlybird')} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
useKiloClawPairing,
} from '@/lib/hooks/use-kiloclaw-queries';
import { useThemeColors } from '@/lib/hooks/use-theme-colors';
import { capitalize } from '@/lib/utils';

const CHANNEL_LABELS = {
telegram: 'Telegram',
Expand All @@ -38,6 +39,12 @@ const CHANNEL_LABELS = {
github: 'GitHub',
} satisfies Record<string, string>;

function channelLabel(channel: string): string {
return Object.hasOwn(CHANNEL_LABELS, channel)
? CHANNEL_LABELS[channel as keyof typeof CHANNEL_LABELS]
: capitalize(channel);
}

export default function DevicePairingScreen() {
const { 'instance-id': instanceId } = useLocalSearchParams<{ 'instance-id': string }>();
const instanceContext = useInstanceContext(instanceId);
Expand Down Expand Up @@ -128,9 +135,7 @@ export default function DevicePairingScreen() {
const hasAnyRequests = channelRequests.length > 0 || deviceRequests.length > 0;

function handleApproveChannel(channel: string, code: string) {
const label = Object.hasOwn(CHANNEL_LABELS, channel)
? CHANNEL_LABELS[channel as keyof typeof CHANNEL_LABELS]
: channel.charAt(0).toUpperCase() + channel.slice(1);
const label = channelLabel(channel);
Alert.alert(
t('kiloclaw.devicePairing.approveTitle'),
t('kiloclaw.devicePairing.approveMessage', { label, code }),
Expand Down Expand Up @@ -212,11 +217,7 @@ export default function DevicePairingScreen() {
<MessageSquare size={18} color={colors.foreground} />
)}
<View className="flex-1 gap-0.5">
<Text className="text-sm font-medium">
{Object.hasOwn(CHANNEL_LABELS, request.channel)
? CHANNEL_LABELS[request.channel as keyof typeof CHANNEL_LABELS]
: request.channel.charAt(0).toUpperCase() + request.channel.slice(1)}
</Text>
<Text className="text-sm font-medium">{channelLabel(request.channel)}</Text>
<View className="flex-row items-center gap-1.5">
<View className="rounded bg-muted px-1.5 py-0.5">
<Text className="text-xs font-mono text-muted-foreground">
Expand Down
3 changes: 3 additions & 0 deletions apps/mobile/src/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import {
useLanguagePreference,
} from '@/lib/hooks/use-language-preference';
import { useTrackingPermissionPrompt } from '@/lib/hooks/use-tracking-permission-prompt';
import { prewarmIntl } from '@/lib/intl-cache';
import {
captureLaunchDeepLink,
getPendingDeepLink,
Expand Down Expand Up @@ -369,8 +370,10 @@ function RootLayoutNav() {
}
try {
await i18n.changeLanguage(resolved);
prewarmIntl(resolved);
} catch {
await i18n.changeLanguage('en');
prewarmIntl('en');
}
void renameAndroidNotificationChannels();
if (!cancelled) {
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/src/components/add-credits-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function AddCreditsRow({ url, className }: AddCreditsRowProps) {
size="sm"
variant="outline"
onPress={() => {
void openExternalUrl(url, { label: 'billing page' });
void openExternalUrl(url, { label: t('addCredits.billingPage') });
}}
>
<Text className="text-xs font-semibold">{t('addCredits.cta')}</Text>
Expand Down
13 changes: 7 additions & 6 deletions apps/mobile/src/components/agents/attachment-chip-description.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { formatFileSize } from '@kilocode/kilo-chat';

import { i18n } from '@/i18n';
import { formatFileSize, formatPercent } from '@/lib/format';

export type ChipStateInput = {
filename: string;
Expand Down Expand Up @@ -51,7 +50,7 @@ export function progressLabel(progress: number | null): string {
if (progress >= 1) {
return i18n.t('agentChat.attachmentPreview.uploaded');
}
return `${Math.round(progress * 100)}%`;
return formatPercent(progress * 100, i18n.language);
}

/**
Expand Down Expand Up @@ -85,11 +84,13 @@ export function describeAttachmentChip(state: ChipStateInput): ChipDescription {

return {
filename,
sizeText: formatFileSize(size),
sizeText: formatFileSize(size, i18n.language),
progressText,
message,
accessibilityLabel:
message !== null ? `${filename}, ${message}` : `${filename}, ${progressText}`,
accessibilityLabel: i18n.t('chat.attachmentPreview.accessibility', {
filename,
status: message ?? progressText,
}),
showRetry: isRetryable,
showRemove: true,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe('describeAttachmentChip — happy / in-flight', () => {
it('shows filename and human-readable size for a fully uploaded chip', () => {
const desc = describeAttachmentChip(baseState);
expect(desc.filename).toBe('doc.pdf');
expect(desc.sizeText).toBe('1 KB');
expect(desc.sizeText).toBe('1 kB');
expect(desc.progressText).toBe('Uploaded');
expect(desc.message).toBeNull();
expect(desc.accessibilityLabel).toBe('doc.pdf, Uploaded');
Expand All @@ -52,7 +52,7 @@ describe('describeAttachmentChip — happy / in-flight', () => {
expect(desc.showRetry).toBe(false);
expect(desc.showRemove).toBe(true);
// Size and filename are still surfaced so the user knows what is uploading.
expect(desc.sizeText).toBe('1 KB');
expect(desc.sizeText).toBe('1 kB');
expect(desc.filename).toBe('doc.pdf');
});

Expand All @@ -75,7 +75,7 @@ describe('describeAttachmentChip — happy / in-flight', () => {
progress: 0,
});
expect(desc.filename).toBe('doc.pdf');
expect(desc.sizeText).toBe('1 KB');
expect(desc.sizeText).toBe('1 kB');
expect(desc.progressText).toBe('0%');
expect(desc.message).toBeNull();
expect(desc.accessibilityLabel).toBe('doc.pdf, 0%');
Expand All @@ -95,7 +95,7 @@ describe('describeAttachmentChip — unhappy, retryable (network/timeout/408/429
expect(desc.showRetry).toBe(true);
expect(desc.showRemove).toBe(true);
// Size + filename are still surfaced so the user knows which file failed.
expect(desc.sizeText).toBe('1 KB');
expect(desc.sizeText).toBe('1 kB');
expect(desc.filename).toBe('doc.pdf');
});

Expand Down Expand Up @@ -130,7 +130,7 @@ describe('describeAttachmentChip — unhappy, non-retryable (presign BAD_REQUEST
// Filename + size are still surfaced so the user knows which file is
// permanently rejected.
expect(desc.filename).toBe('doc.pdf');
expect(desc.sizeText).toBe('1 KB');
expect(desc.sizeText).toBe('1 kB');
});

it('keeps progressText empty for terminal failures (no percent leaked into the message)', () => {
Expand Down
Loading