Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
142 commits
Select commit Hold shift + click to select a range
6903b70
feat(mobile): add shared tab-screen scroll container
iscekic Jul 10, 2026
65f4114
fix(mobile): parse backend timestamps with parseTimestamp
iscekic Jul 10, 2026
82e7d47
fix(mobile): gate sentry replay and screenshots on consent
iscekic Jul 10, 2026
8915cf3
fix(mobile): add recovery paths to login and device auth
iscekic Jul 10, 2026
3ddac73
test(mobile): pin kilo-chat token expiry cache behavior
iscekic Jul 10, 2026
4044e4a
fix(mobile): preserve agent drafts and block sends with failed attach…
iscekic Jul 10, 2026
4035790
fix(mobile): resolve kiloclaw instance context to explicit terminal s…
iscekic Jul 10, 2026
9515582
fix(mobile): advance security-agent settings baseline after save
iscekic Jul 10, 2026
76f1020
fix(mobile): gate model settings on resolved instance context
iscekic Jul 10, 2026
077c66e
fix(mobile): validate code-reviewer and security-agent route params
iscekic Jul 10, 2026
9208835
fix(mobile): treat code-reviewer domain failures as mutation errors
iscekic Jul 10, 2026
9c43278
fix(mobile): skeleton model picker until instance context resolves
iscekic Jul 10, 2026
11af150
fix(mobile): repair home-screen test mocks and unused export
iscekic Jul 10, 2026
33805c6
fix(mobile): respect bottom safe area in kilo-chat composer
iscekic Jul 11, 2026
1f53c38
refactor(mobile): drop compact-session-row passthrough
iscekic Jul 11, 2026
8163ddf
feat(mobile): screen-header heading semantics and fallback back action
iscekic Jul 11, 2026
6b62112
fix(mobile): toaster ordering and Kilo-token styling
iscekic Jul 11, 2026
bee6c9f
feat(mobile): 44pt button targets and loading state
iscekic Jul 11, 2026
2101281
feat(mobile): uncontrolled FormField primitive
iscekic Jul 11, 2026
03ca522
fix(mobile): hide chevron and dim disabled ConfigureRow
iscekic Jul 11, 2026
70a4cc8
fix(mobile): contrast-safe semantic tokens and input boundaries
iscekic Jul 11, 2026
f5d4c8d
feat(mobile): spinning icon with reduced-motion support
iscekic Jul 11, 2026
de7de40
fix(mobile): tab-bar clearance via shared container
iscekic Jul 11, 2026
9698db0
fix(mobile): derive kilo-chat clearance from tab-bar layout
iscekic Jul 11, 2026
9481523
fix(mobile): standardize detail-screen bottom insets
iscekic Jul 11, 2026
dbb47a9
fix(mobile): reliable FormField error announcement
iscekic Jul 11, 2026
6b3fccd
refactor(mobile): extract shared form-sheet detent options
iscekic Jul 11, 2026
bff6b06
fix(mobile): cancel infinite spin animations on unmount
iscekic Jul 11, 2026
c21bc3e
feat(mobile): shared choice-row primitive
iscekic Jul 11, 2026
fefbcf4
feat(mobile): add placement variants to EmptyState and QueryError
iscekic Jul 11, 2026
ecde156
fix(mobile): lint-clean animation cleanup returns
iscekic Jul 11, 2026
6590120
refactor(mobile): shared SheetHeader for picker sheets
iscekic Jul 11, 2026
658c12d
fix(mobile): webhook retry dismissal order and shared route-param val…
iscekic Jul 11, 2026
5a1b92e
refactor(mobile): shared PickerSheet shell
iscekic Jul 11, 2026
e0a3569
feat(mobile): QueryError variants; replace ad-hoc error rows
iscekic Jul 11, 2026
6abc483
feat(mobile): safe external-link helper
iscekic Jul 11, 2026
0092403
fix(mobile): drop unused QueryErrorVariant export
iscekic Jul 11, 2026
f495c20
refactor(mobile): shared RenameModal with validation and pending state
iscekic Jul 11, 2026
bab159b
refactor(mobile): use app-shared money formatting everywhere
iscekic Jul 11, 2026
71d08d2
fix(mobile): remount RenameModal per open for a clean slate
iscekic Jul 11, 2026
493cbd8
fix(mobile): empty-state placement, retry spinners, review money format
iscekic Jul 11, 2026
2ff86ca
fix(mobile): home surfaces query failures instead of hiding sections
iscekic Jul 11, 2026
b801cbb
fix(mobile): security-agent capability states and audit access
iscekic Jul 11, 2026
8630982
fix(mobile): security-agent repository states
iscekic Jul 11, 2026
f56eeac
fix(mobile): session list retry, filtered empty state, honest refresh
iscekic Jul 11, 2026
13fbe40
fix(mobile): entitlement-aware kiloclaw list and reachable create CTA
iscekic Jul 11, 2026
3f37502
fix(mobile): organization screens surface query errors
iscekic Jul 11, 2026
96e52a2
fix(mobile): honest analysis capacity states
iscekic Jul 11, 2026
817ce32
fix(mobile): code-reviewer provider and permission states
iscekic Jul 11, 2026
2796c10
feat(mobile): optimistic session rename and delete
iscekic Jul 11, 2026
e7bb7b3
fix(mobile): code-reviewer config states
iscekic Jul 11, 2026
179282f
fix(mobile): security dashboard sync and status truth
iscekic Jul 11, 2026
55d61c2
fix(mobile): code-reviewer scope list surfaces org errors
iscekic Jul 11, 2026
0d956c9
fix(mobile): organization sheet errors and validation
iscekic Jul 11, 2026
7c16578
fix(mobile): recoverable model and repo failures on new session
iscekic Jul 11, 2026
0bde571
fix(mobile): audit report reachable from disabled-agent settings
iscekic Jul 11, 2026
05e4e8f
fix(mobile): kiloclaw onboarding step feedback and completion gating
iscekic Jul 11, 2026
484055c
fix(mobile): security filter sheet with visible overdue filter
iscekic Jul 11, 2026
6b80447
fix(mobile): new-session input gating and attachment rejection feedback
iscekic Jul 11, 2026
d89b486
fix(mobile): access-conflict state links to kilo.ai
iscekic Jul 11, 2026
d494fa2
fix(mobile): gate question submission on all answers
iscekic Jul 11, 2026
817464f
fix(mobile): drop unused isProvisioningTerminal barrel export
iscekic Jul 11, 2026
45aff34
fix(mobile): code-reviewer repos error and empty states
iscekic Jul 11, 2026
7882edd
fix(mobile): organization route boundary and role guards
iscekic Jul 11, 2026
68d9887
fix(mobile): remediation cancel state and terminology
iscekic Jul 11, 2026
3416b0f
fix(mobile): organization empty-state actions
iscekic Jul 11, 2026
d333a06
fix(mobile): session mapper tests and stale-data-preserving list errors
iscekic Jul 11, 2026
7f99f8c
fix(mobile): kiloclaw lifecycle control states
iscekic Jul 11, 2026
5a2e05f
fix(mobile): destroy instance waits for confirmation
iscekic Jul 11, 2026
1bc03b1
fix(mobile): gate auto-review enablement on readiness
iscekic Jul 11, 2026
d65c2c8
fix(mobile): profile org-failure and zero-balance recovery
iscekic Jul 11, 2026
864c129
fix(mobile): manual review provider path and validation
iscekic Jul 11, 2026
762bdb7
fix(mobile): session detail recovery and loading structure
iscekic Jul 11, 2026
b385dc5
fix(mobile): billing final-term state and skeleton
iscekic Jul 11, 2026
9ed7517
fix(mobile): notification toggle failure feedback
iscekic Jul 11, 2026
bfc52b1
fix(mobile): forced-update store fallbacks
iscekic Jul 11, 2026
94bb8cc
fix(mobile): picker fallback headers and synchronous bridge init
iscekic Jul 11, 2026
4848b11
fix(mobile): code-reviewer saves await outcomes without racing
iscekic Jul 11, 2026
e65f43a
fix(mobile): kilo-chat route loading and cached-data precedence
iscekic Jul 11, 2026
5baa1e4
fix(mobile): org boundary discriminates errors from stale orgs
iscekic Jul 11, 2026
88e20bf
fix(mobile): surface kilo-chat token failures with retry
iscekic Jul 11, 2026
a1133ec
fix(mobile): security settings read-only and validation truth
iscekic Jul 11, 2026
2780bfe
fix(mobile): review list and detail states
iscekic Jul 11, 2026
a679f5e
fix(mobile): app-store-safe zero-balance copy on iOS
iscekic Jul 11, 2026
5cefd18
fix(mobile): dashboard scoped error states
iscekic Jul 11, 2026
5c31e7f
fix(mobile): instance picker empty state
iscekic Jul 11, 2026
7359299
fix(mobile): inline rename-conversation errors
iscekic Jul 11, 2026
081411c
fix(mobile): dismiss-finding sheet validation and inline errors
iscekic Jul 11, 2026
eb88815
refactor(mobile): shared access-issue URL resolution
iscekic Jul 11, 2026
188e87e
fix(mobile): bot-offline banner links to instance
iscekic Jul 11, 2026
df3087d
fix(mobile): drop unused exported kilo-chat state types
iscekic Jul 11, 2026
46eca1f
chore(mobile): drop comment prefixes
iscekic Jul 11, 2026
a0696fb
fix(mobile): security-agent targets, empty centering, SLA zero state
iscekic Jul 11, 2026
28554ba
fix(mobile): kilo-pass inline feedback and connection timeout
iscekic Jul 11, 2026
a1532ef
fix(mobile): consent card error and pending handling
iscekic Jul 11, 2026
3c4275d
fix(mobile): login field labeling and busy feedback
iscekic Jul 11, 2026
6d3ce96
fix(mobile): drop ponytail comment prefix in kilo-pass products hook
iscekic Jul 11, 2026
e76618f
fix(mobile): secure credential fields and single-credential rotation
iscekic Jul 11, 2026
f8112ea
fix(mobile): kiloclaw skeleton shapes, degraded-state precedence, des…
iscekic Jul 11, 2026
ddf43b8
fix(mobile): review-detail permanent errors, save-chain tests, gated …
iscekic Jul 11, 2026
0cc0ba4
fix(mobile): model and exec-policy selection states
iscekic Jul 11, 2026
8d94fce
fix(mobile): pending feedback on sync-findings action
iscekic Jul 11, 2026
a93acb9
fix(mobile): version pinning pagination and pin serialization
iscekic Jul 11, 2026
0fbdb92
fix(mobile): conversation stale-data precedence and offline CTA gating
iscekic Jul 11, 2026
71b7548
fix(mobile): single feedback owner for kilo-pass purchase errors
iscekic Jul 11, 2026
a7cd596
fix(mobile): google settings and device pairing feedback
iscekic Jul 11, 2026
1d13d97
fix(mobile): actionable changelog requirements
iscekic Jul 11, 2026
093e899
fix(mobile): kilo-chat message actions, links, and favorites feedback
iscekic Jul 11, 2026
13cc9a3
fix(mobile): reaction picker modal behavior
iscekic Jul 11, 2026
2155368
fix(mobile): single accessible control for reasoning toggle
iscekic Jul 11, 2026
ac4db30
fix(mobile): surface secure-store preference write failures
iscekic Jul 11, 2026
90eb2a5
fix(mobile): single creation CTA on empty surfaces
iscekic Jul 11, 2026
1b02323
fix(mobile): retry-send resolves failed original; session-filter writ…
iscekic Jul 11, 2026
c69bdce
fix(mobile): version-pin pagination bounds and stale-data preservation
iscekic Jul 11, 2026
de1cb98
chore(mobile): fix home-screen test mocks and prune dead code-reviewe…
iscekic Jul 11, 2026
7e2e93b
fix(mobile): consistent disabled control presentation
iscekic Jul 11, 2026
d8b0eac
fix(mobile): pressed feedback on inert-feeling controls
iscekic Jul 11, 2026
b28c4f3
fix(mobile): assorted consistency nits
iscekic Jul 11, 2026
ff18029
fix(mobile): KVRow overflow handling
iscekic Jul 11, 2026
edb90b4
fix(mobile): consistent empty and error state placement
iscekic Jul 11, 2026
2218e9c
fix(mobile): honest pull-to-refresh everywhere
iscekic Jul 11, 2026
305f726
refactor(mobile): shared formatDate in app-shared
iscekic Jul 11, 2026
008770e
fix(mobile): match skeletons to loaded content
iscekic Jul 11, 2026
b9da944
refactor(mobile): status colors from Focus tokens
iscekic Jul 11, 2026
9e3a15e
fix(mobile): UX copy and date formatting consistency
iscekic Jul 11, 2026
8e3a957
fix(mobile): centered initial-load errors and pending-state visuals
iscekic Jul 11, 2026
4c80697
fix(mobile): finish sentence-case copy sweep
iscekic Jul 11, 2026
d0b3892
test(mobile): align kilo-pass tests with formatDate; keep store-revie…
iscekic Jul 11, 2026
27779cd
fix(mobile): formatDate follows device locale
iscekic Jul 11, 2026
63f995d
fix(mobile): address e2e findings — provider-error states, filter a11…
iscekic Jul 11, 2026
b1601cf
feat(kilo-chat): redeliver failed messages on retry instead of recrea…
iscekic Jul 11, 2026
f0b11e9
fix(mobile): tear down Sentry before consent re-init
iscekic Jul 11, 2026
c2d5fd5
fix(mobile): address review findings across composer, sessions, kiloc…
iscekic Jul 11, 2026
58a0bf2
fix(mobile): session search retry, reduced-motion pulse, header touch…
iscekic Jul 11, 2026
07e1334
fix: address second-round review findings on redeliver, sentry consen…
iscekic Jul 11, 2026
504ca27
fix(mobile): reasoning settings sheet stays navigable for VoiceOver
iscekic Jul 11, 2026
9bb8762
fix(mobile): kiloclaw copy sentence case and pairing-refresh a11y label
iscekic Jul 11, 2026
5d53f64
fix(mobile): personal security-agent scope survives an org-list outage
iscekic Jul 12, 2026
cd66540
fix(mobile): formSheet picker headers render; device-auth timeout sco…
iscekic Jul 12, 2026
3f0f446
fix(kilo-chat): drop redundant non-null assertion on length-checked bot
iscekic Jul 12, 2026
9f2cdad
fix: third-round review — rename latest-intent, redeliver ordering + …
iscekic Jul 12, 2026
f2850dc
fix(kilo-chat): clear delivery-failed on redeliver success, not only …
iscekic Jul 12, 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
4 changes: 4 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -427,3 +427,7 @@ Kilo voice is clear, technical, calm, and direct. Use concrete verbs and specifi
- Invent status colors, spacing values, radii, or typography roles.
- Nest cards or use shadows as the default source of depth.
- Depend on color, hover, placeholders, or icon shape alone to convey meaning.

## Mobile (Focus palette)

The mobile app (`apps/mobile/`) intentionally does not use this token contract. It ships its own light/dark palette ("Focus", FL/FD) defined as CSS variables in `apps/mobile/src/global.css`, tuned for WCAG AA contrast against its own warmer neutral surfaces. This divergence is deliberate — mobile is not expected to converge onto the tokens above.
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ import { captureEvent, SESSION_VIEWED_EVENT } from '@/lib/analytics/posthog';

import { ChatSandboxRouteMounts } from '@/components/kilo-chat/chat-sandbox-route-mounts';
import { ConversationScreen } from '@/components/kilo-chat/conversation-screen';
import {
ConversationHistoryErrorView,
ConversationHistoryLoadingView,
} from '@/components/kilo-chat/conversation-history-state-views';
import {
getConversationRouteDecision,
getConversationRouteErrorMessage,
shouldRenderConversationScreen,
} from '@/components/kilo-chat/conversation-route-state';
import { useConversationDetail } from '@/components/kilo-chat/hooks/use-conversations';
import { useKiloChatClient } from '@/components/kilo-chat/hooks/use-kilo-chat-client';
Expand All @@ -29,7 +32,11 @@ export default function ChatConversationRoute() {
const conversationDetail = useConversationDetail(client, conversationId);
const redirectPath = chatSandboxPath(sandboxId);
const routeDecision = getConversationRouteDecision({
detail: conversationDetail,
detail: {
data: conversationDetail.data,
error: conversationDetail.error,
isError: conversationDetail.isError,
},
routeSandboxId: sandboxId,
});

Expand All @@ -43,24 +50,28 @@ export default function ChatConversationRoute() {
}, [conversationDetail.data, conversationId, openedVia]);

useEffect(() => {
if (conversationDetail.isError) {
toast.error(getConversationRouteErrorMessage(conversationDetail.error));
router.replace(redirectPath);
return;
}
if (routeDecision === 'not-found') {
toast.error('Conversation not found');
router.replace(redirectPath);
}
}, [conversationDetail.error, conversationDetail.isError, redirectPath, routeDecision, router]);
}, [redirectPath, routeDecision, router]);

if (routeDecision === 'pending') {
return <ConversationHistoryLoadingView />;
}

if (routeDecision === 'retryable-error') {
return (
<ConversationHistoryErrorView
message={getConversationRouteErrorMessage(conversationDetail.error)}
onRetry={() => {
void conversationDetail.refetch();
}}
/>
);
}

if (
!shouldRenderConversationScreen({
detail: conversationDetail,
routeSandboxId: sandboxId,
}) ||
!conversationDetail.data
) {
if (routeDecision !== 'ready' || !conversationDetail.data) {
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ import { useAllKiloClawInstances } from '@/lib/hooks/use-instance-context';

export default function ChatSandboxIndex() {
const { 'sandbox-id': sandboxId } = useLocalSearchParams<{ 'sandbox-id': string }>();
const { data: instances } = useAllKiloClawInstances();
const { data: instances, isPending: instancesPending } = useAllKiloClawInstances();
const instance = instances?.find(i => i.sandboxId === sandboxId);
const sandboxLabel =
instance?.botName ?? instance?.name ?? instance?.organizationName ?? 'KiloClaw';
// Blank until instances resolve — never show a "KiloClaw" placeholder that
// then flashes to the real instance name once the query settles.
const sandboxLabel = instancesPending
? ''
: (instance?.botName ?? instance?.name ?? instance?.organizationName ?? 'KiloClaw');
return (
<>
<ChatSandboxRouteMounts />
Expand Down
150 changes: 90 additions & 60 deletions apps/mobile/src/app/(app)/(tabs)/(1_kiloclaw)/chat/instance-picker.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,59 @@
import * as Haptics from 'expo-haptics';
import { useLocalSearchParams, useRouter } from 'expo-router';
import { Check } from 'lucide-react-native';
import { Pressable, ScrollView, View } from 'react-native';
import { type Href, useLocalSearchParams, useRouter } from 'expo-router';
import { Check, Server } from 'lucide-react-native';
import { Pressable, View } from 'react-native';
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';

import { StatusBadge } from '@/components/kiloclaw/status-badge';
import { EmptyState } from '@/components/empty-state';
import { PickerSheet } from '@/components/picker-sheet';
import { QueryError } from '@/components/query-error';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';
import { Text } from '@/components/ui/text';
import { useAllKiloClawInstances } from '@/lib/hooks/use-instance-context';
import { type ClawInstance, useAllKiloClawInstances } from '@/lib/hooks/use-instance-context';
import { useThemeColors } from '@/lib/hooks/use-theme-colors';
import { kiloclawInstanceSwitcherTitle } from '@/lib/kiloclaw-display';
import { chatSandboxPath } from '@/lib/kilo-chat-routes';

function InstanceRow({
instance,
isCurrent,
onSelect,
}: {
instance: ClawInstance;
isCurrent: boolean;
onSelect: (sandboxId: string) => void;
}) {
const colors = useThemeColors();
const title = kiloclawInstanceSwitcherTitle(instance);
return (
<Pressable
className="mx-4 mt-3 min-h-16 flex-row items-center gap-3 rounded-xl border border-border bg-card px-4 py-3 active:bg-secondary will-change-pressable"
onPress={() => {
onSelect(instance.sandboxId);
}}
accessibilityRole="button"
accessibilityLabel={`${title}${isCurrent ? ', current' : ''}`}
>
<View className="min-w-0 flex-1 gap-1">
<Text className="text-base font-semibold text-foreground" numberOfLines={1}>
{title}
</Text>
<View className="flex-row flex-wrap items-center gap-x-3 gap-y-1">
<Text variant="muted" numberOfLines={1}>
{instance.organizationName ?? 'Personal'}
</Text>
<StatusBadge status={instance.status} />
</View>
</View>
{isCurrent ? <Check size={18} color={colors.primary} /> : null}
</Pressable>
);
}

export default function InstancePickerScreen() {
const router = useRouter();
const colors = useThemeColors();
const { currentId } = useLocalSearchParams<{ currentId: string }>();
const instancesQuery = useAllKiloClawInstances();
const { data: instances } = instancesQuery;
Expand All @@ -29,71 +68,62 @@ export default function InstancePickerScreen() {
router.push(chatSandboxPath(sandboxId));
};

return (
<ScrollView className="flex-1 bg-background">
<View className="border-b border-border px-4 pb-3 pt-4">
<View className="h-11 flex-row items-center justify-center">
<Text className="text-lg font-semibold text-foreground">Switch Instance</Text>
<Pressable
onPress={() => {
router.back();
}}
hitSlop={8}
accessibilityRole="button"
accessibilityLabel="Done"
className="absolute right-0 rounded-full bg-secondary px-4 py-2 active:opacity-70 will-change-pressable"
>
<Text className="text-base font-medium text-foreground">Done</Text>
</Pressable>
</View>
</View>
const showList = !instancesQuery.isPending && !instancesQuery.isError;
const loadedInstances = instances ?? [];

return (
<PickerSheet
title="Switch instance"
onDone={() => {
router.back();
}}
>
{instancesQuery.isPending ? (
<View className="gap-3 px-4 py-4">
<Skeleton className="h-16 rounded-xl" />
<Skeleton className="h-16 rounded-xl" />
<Skeleton className="h-16 rounded-xl" />
</View>
<Animated.View exiting={FadeOut.duration(150)}>
<Skeleton className="mx-4 mt-3 h-16 rounded-xl" />
<Skeleton className="mx-4 mt-3 h-16 rounded-xl" />
<Skeleton className="mx-4 mt-3 h-16 rounded-xl" />
</Animated.View>
) : null}
{instancesQuery.isError ? (
<QueryError
className="py-12"
message="Could not load instances"
description="Could not load instances"
onRetry={() => {
void instancesQuery.refetch();
}}
/>
) : null}
{!instancesQuery.isPending && !instancesQuery.isError
? (instances ?? []).map(instance => {
const isCurrent = instance.sandboxId === currentId;
const title = kiloclawInstanceSwitcherTitle(instance);
return (
<Pressable
key={instance.sandboxId}
className="mx-4 mt-3 min-h-16 flex-row items-center gap-3 rounded-xl border border-border bg-card px-4 py-3 active:bg-secondary will-change-pressable"
onPress={() => {
handleSelect(instance.sandboxId);
}}
accessibilityRole="button"
accessibilityLabel={`${title}${isCurrent ? ', current' : ''}`}
>
<View className="min-w-0 flex-1 gap-1">
<Text className="text-base font-semibold text-foreground" numberOfLines={1}>
{title}
</Text>
<View className="flex-row flex-wrap items-center gap-x-3 gap-y-1">
<Text variant="muted" numberOfLines={1}>
{instance.organizationName ?? 'Personal'}
</Text>
<StatusBadge status={instance.status} />
</View>
</View>
{isCurrent ? <Check size={18} color={colors.primary} /> : null}
</Pressable>
);
})
: null}
</ScrollView>
{showList && loadedInstances.length === 0 ? (
<EmptyState
className="py-12"
icon={Server}
title="No KiloClaw instances"
description="Set up an instance to start chatting."
action={
<Button
variant="outline"
onPress={() => {
router.push('/(app)/onboarding' as Href);
}}
>
<Text>Set up KiloClaw</Text>
</Button>
}
/>
) : null}
{showList ? (
<Animated.View entering={FadeIn.duration(200)}>
{loadedInstances.map(instance => (
<InstanceRow
key={instance.sandboxId}
instance={instance}
isCurrent={instance.sandboxId === currentId}
onSelect={handleSelect}
/>
))}
</Animated.View>
) : null}
</PickerSheet>
);
}
42 changes: 31 additions & 11 deletions apps/mobile/src/app/(app)/(tabs)/(1_kiloclaw)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ import { useCallback, useState } from 'react';
import { Platform, View } from 'react-native';
import Animated, { FadeIn, FadeOut, LinearTransition } from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { toast } from 'sonner-native';

import { EmptyStateContent } from '@/components/kiloclaw/empty-state-content';
import {
EmptyStateContent,
resolveAccessRequiredSubcase,
} from '@/components/kiloclaw/empty-state-content';
import { getKiloClawEntryDecision } from '@/components/kiloclaw/instance-entry-state';
import { InstanceListScreen } from '@/components/kiloclaw/instance-list-screen';
import { QueryError } from '@/components/query-error';
Expand All @@ -28,7 +32,13 @@ export default function KiloClawTab() {
const { byBadgeBucket: unreadByBadgeBucket } = useUnreadCounts();
const refetchInstances = instancesQuery.refetch;
const entryDecision = getKiloClawEntryDecision(instances);
const onboardingQuery = useKiloClawMobileOnboardingState(entryDecision.kind === 'empty');
// Always enabled (not just for the empty-list case) so a personal
// billing/access issue is still surfaced as a card annotation when the
// list is non-empty — see `personalAccessIssue` below.
const onboardingQuery = useKiloClawMobileOnboardingState();
const personalAccessIssue = onboardingQuery.data
? resolveAccessRequiredSubcase(onboardingQuery.data)
: null;
useForegroundInvalidateKiloclawState();

const showInstanceSkeleton = entryDecision.kind === 'loading' || onboardingQuery.isPending;
Expand All @@ -40,30 +50,39 @@ export default function KiloClawTab() {
void (async () => {
setManualRefreshing(true);
try {
await refetchInstances();
const result = await refetchInstances();
if (result.isError) {
toast.error('Could not refresh. Showing the last saved list.');
}
} finally {
setManualRefreshing(false);
}
})();
}, [refetchInstances]);

const onboardingQueryEnabled = entryDecision.kind === 'empty';
// A background billing-check failure while the list is already showing
// shouldn't blank the screen — only block on it before we have any
// instances to show (preserve stale/cached data).
const hasQueryError =
instancesQuery.isError || (onboardingQueryEnabled && onboardingQuery.isError);
instancesQuery.isError || (entryDecision.kind !== 'list' && onboardingQuery.isError);

if (hasQueryError) {
return (
<View className="flex-1 bg-background">
<ScreenHeader title="KiloClaw" size="large" showBackButton={false} className="px-[22px]" />
<Animated.View entering={FadeIn.duration(200)} className="flex-1">
<Animated.View
entering={FadeIn.duration(200)}
className="flex-1"
style={emptyStateContainerStyle}
>
<QueryError
className="flex-1"
message="Could not load KiloClaw instances"
onRetry={() => {
if (instancesQuery.isError) {
void instancesQuery.refetch();
}
if (onboardingQueryEnabled && onboardingQuery.isError) {
if (onboardingQuery.isError) {
void onboardingQuery.refetch();
}
}}
Expand All @@ -89,6 +108,7 @@ export default function KiloClawTab() {
onCreate={() => {
router.push('/(app)/onboarding' as Href);
}}
personalAccessIssue={personalAccessIssue}
/>
);
}
Expand All @@ -98,10 +118,10 @@ export default function KiloClawTab() {
<ScreenHeader title="KiloClaw" size="large" showBackButton={false} className="px-[22px]" />
<Animated.View layout={LinearTransition} className="flex-1 px-4">
{showInstanceSkeleton || onboardingQuery.data === undefined ? (
<Animated.View exiting={FadeOut.duration(150)} className="w-full gap-3 px-4 pt-5">
<Skeleton className="h-16 w-full rounded-xl" />
<Skeleton className="h-16 w-full rounded-xl" />
<Skeleton className="h-16 w-full rounded-xl" />
<Animated.View exiting={FadeOut.duration(150)} className="w-full gap-3 pt-5">
<Skeleton className="h-[72px] w-full rounded-2xl" />
<Skeleton className="h-[72px] w-full rounded-2xl" />
<Skeleton className="h-[72px] w-full rounded-2xl" />
</Animated.View>
) : (
<Animated.View
Expand Down
Loading
Loading