Skip to content

Commit d773511

Browse files
authored
feat(kilo-chat): rip out stream chat
* fix(notifications): record badges without push tokens * fix(kilo-chat): roll back failed action webhooks * fix(mobile): add kilo chat message actions * fix(kilo-chat): retry failed mark-read attempts * fix(mobile): add reply and delivery failure parity * fix(event-service): retry after token refresh * fix(mobile): dispatch message actions by identity * fix(kiloclaw): disable bot-added chat members * fix(mobile): show chat action errors * feat(mobile): add chat typing indicators * fix(mobile): generate ulid chat client ids * fix(kilo-chat): reorder conversations after activity * fix(kilo-chat): bound mark-read to observed message * fix(kilo-chat): keep first auto-title * fix(kilo-chat): suppress pending message actions * fix(notifications): fail open on presence lookup errors * fix(notifications): dedupe presence suppressed pushes * fix(kilo-chat): ignore pending ids for read boundary * fix(event-service): keep reconnecting after pre-open failures * fix(kilo-chat): reject bot additional members * fix(web): wait for kilo chat user id * fix(kilo-chat): use message timestamp for read activity * fix(kilo-chat): clear badges from mark read * fix(kilo-chat): order resolved optimistic messages * fix(mobile): keep mark-read success authoritative * fix(mobile): enforce chat message length * fix(mobile): gate kilo chat sends on bot status * fix(kilo-chat): revoke mutations after leave * fix(kilo-chat): include reply parent snapshots * fix(kilo-chat): preserve live reply snapshots * fix(kilo-chat): preserve failed edit drafts * fix(mobile): refresh unread badges under chat provider * fix(web): retry failed kilo chat mark-read * fix(kilo-chat): suppress false delivery-failed events * fix(mobile): retry kilo chat mark read failures * fix(mobile): hide failed message actions * fix(kilo-chat): suppress stale action failure events * fix(mobile): hide deleted message reactions * fix(kilo-chat): skip duplicate action failure events * fix(mobile): redirect stale chat routes * fix(kilo-chat): suppress duplicate delivery failure events * fix(web): filter delivery failure toasts by context * fix(kilo-chat): guard optimistic message rollbacks * fix(kilo-chat): order delayed message events * chore: sync schemas * fix(kilo-chat): handle send queue cleanup rejection * fix(kilo-chat): guard read rollback state * fix(kilo-chat): keep conversation timestamps monotonic * fix(kilo-chat): ignore stale message update events * fix(kilo-chat): preserve state on delayed creates * fix(kilo-chat): preserve resolved actions on delayed creates * fix(kilo-chat): guard stale reaction events * fix(kilo-chat): preserve badges for stale mark-read * fix(kilo-chat): seed local reaction operations * fix(kilo-chat): clear deleted conversation badges * fix(kilo-chat): seed idempotent reaction removes * fix(kilo-chat): gate badge clears on mark-read response * fix(mobile): guard stale chat badge writes * fix(kilo-chat): invalidate messages on edit conflict * fix(kilo-chat): resolve ci drift * fix(kilo-chat): guard mobile pagination fetches * fix(kilo-chat): remove redundant typing fanout * perf(kilo-chat): contain web message rows * perf(kilo-chat): collapse membership post-commit update * perf(kilo-chat): share reply parent lookup * perf(kilo-chat): reuse create message member info * perf(kilo-chat): resolve mark read in conversation do * perf(notifications): reuse incremented badge total * perf(web): stabilize message failure handlers * perf(kilo-chat): slim mark-read resolver payload * perf(web): scope kilo chat cache events * perf(kilo-chat): patch loaded activity rows * perf(kilo-chat): conditionally advance read markers * perf(kilo-chat): scope mark-read cache updates * perf(kilo-chat): localize action pending state * perf(notifications): skip noop badge total writes * feat(kilo-chat): return created conversation rows * feat(kilo-chat): return message page cursors * feat(kilo-chat): return canonical mark-read state * fix(kilo-chat): align conversation create response * feat(kilo-chat): return execute-action result * fix(kilo-chat): return canonical created messages * fix(kiloclaw): expose message pagination cursors * fix(kilo-chat): preserve message page cursors * fix(kilo-chat): scope create fallback invalidation * perf(kilo-chat): return execute action fanout context * test(kilo-chat): align response contract assertions * perf(kilo-chat): scope rename leave invalidations * fix(chat): scope public chat token auth * fix(events): use websocket connection tickets * refactor(kilo-chat-hooks): normalize message page cache * refactor(kilo-chat-hooks): share conversation list event handling * refactor(kilo-chat-hooks): share mark-read retry helpers * refactor(kilo-chat): share bot status hook * test(kilo-chat-hooks): consolidate helper coverage * fix(kiloclaw): align image hash inputs * fix(kiloclaw): hash local OpenClaw images * fix(kiloclaw): preserve local OpenClaw image mode * fix(kiloclaw): include image mode in dev handoff * fix(kiloclaw): include dockerignore in image hashes * fix(kilo-chat): validate event fanout payloads * fix(web): invalidate conversation status on reconnect * fix(event-service): allow local web connect tickets * fix(mobile): subscribe chat routes to instance events * fix(notifications): validate chat push rpc payloads * fix(notifications): retry transient expo push failures * fix(mobile): reconcile native badge on hydration * fix(mobile): update chat stack route registration * fix(notifications): surface expo ticket errors * fix(notifications): make no-token dispatch terminal * fix(notifications): surface lifecycle ticket errors * fix(notifications): report expo receipt errors * fix(notifications): report all-stale chat pushes * fix(notifications): terminalize partial ticket failures * fix(mobile): show message pagination loader only while fetching * fix(web): guard kilo chat conversation sandbox * fix(web): preserve kilo chat scroll anchor * fix(kilo-chat): seed cold message cache on send * fix(kilo-chat): refetch cold send history * fix(kilo-chat): patch renamed conversation detail * fix(web): preserve composer after send failure * fix(web): preserve kilo chat status errors * fix(web): preserve pending chat drafts * fix(kilo-chat): preserve sidebar state on leave rollback * fix(notifications): trust typed rpc inputs * fix(kilo-chat): trust event rpc payloads * fix(kiloclaw): trust chat webhook rpc input * fix(web): trust kilo chat token output * fix(kilo-chat): reject invalid delivery failure bodies * fix(kiloclaw): type kilo-chat request bodies * fix(kilo-chat): type client request payloads * fix(event-service): validate connect ticket responses * fix(event-service): type websocket messages * fix(kilo-chat): type notification rpc payloads * fix(event-service): type ticket mint requests * fix(event-service): validate connect ticket query * fix(event-service): schema-check ticket consume responses * fix(kilo-chat): type mark-read success responses * fix(kilo-chat): validate action failure group ids * fix(kilo-chat): return empty accepted action failures * fix(kiloclaw): type plugin typing requests * fix(kiloclaw): type chat webhook forwards * fix(event-service): share connect ticket query schema * fix(kilo-chat): return empty bot status nudge acks * fix(kilo-chat): return empty delivery failure acks * fix(web): route claw chat to kilo chat * fix(dev-env): sync local secrets store bindings * fix(event-service): align local auth env * refactor(event-service): use RPC for connection tickets * fix(event-service): reconnect pre-open socket failures * fix(kilo-chat): surface badge clear failures * fix(kilo-chat): return ok for void successes * fix(kilo-chat-hooks): refetch incomplete activity reorders * fix(notifications): omit tokens from ticket errors * fix(web): gate org chat layout server-side * fix(web): apply billing wrapper to personal chat * fix(env-sync): resolve suffixed local secret sources * fix(web): show chat creation progress * fix(mobile): recover kilo chat user id after token retry * fix(mobile): separate chat list loading states * fix(mobile): gate chat history initial load * fix(mobile): guard chat route sandbox mismatches * fix(mobile): persist focused kilo chat sandbox * fix(kilo-chat): retry HTTP calls after auth refresh * fix(event-service): stop exhausted auth reconnects * fix(notifications): retry transient Expo tickets * fix(env-sync): prefer exact secret sources * fix(env-sync): skip exec during secret discovery * fix(mobile): keep chat state types internal * refactor(kilo-chat): share bearer auth verification * refactor(kilo-chat): share presence subscriptions * refactor(kilo-chat): share message action policy * test(notifications): mint badge route tokens for worker env * refactor(worker-utils): remove unused chat token verifier * fix(kilo-chat): prevent trial banner page scroll * fix(kilo-chat): refresh inactive conversation messages * fix(kiloclaw): clarify kilo chat message tool contract * chore(mobile): add local env example * docs(mobile): document local env host access * fix(dev): respect configured app url * fix(mobile): center bottom tab labels * fix: fix kilo-chat-hooks dependency resolution * fix(mobile): use expo crypto for chat client ids * fix(mobile): route kiloclaw tab through instances * feat(mobile): add kiloclaw instance list * feat(mobile): polish kilo chat conversations * fix(mobile): preserve kilo chat drafts * feat(mobile): complete kilo chat message actions * feat(mobile): polish kilo chat shell * fix(mobile): address kiloclaw chat qa * fix(mobile): show greeting in home header * fix(mobile): keep kiloclaw navigation in tabs * fix(mobile): align kiloclaw tab header * fix(mobile): hide create instance with instances * fix(mobile): remove invalid kiloclaw route registration * fix(mobile): keep home tab navigation intact * fix(mobile): remove kiloclaw instance subtitle * fix(mobile): hide kiloclaw section counts * fix(mobile): remove kiloclaw current marker * fix(mobile): show agent identity in kiloclaw rows * fix(mobile): make kiloclaw conversation list route match * fix(mobile): polish kilo chat conversation list * fix(mobile): add full-page pull refresh * fix(mobile): hide refresh spinner during polling * fix(mobile): show bot name in chat header * fix(mobile): title instance switcher by bot * fix(mobile): move kilo chat rename route * fix(mobile): improve kilo chat list actions * fix(mobile): polish kilo chat composer layout * fix(mobile): match kilo chat composer shape * fix(mobile): float kilo chat composer * fix(mobile): restore kilo chat composer background * fix(mobile): center kilo chat composer text * fix(kilo-chat): avoid toSorted in message cache * fix(mobile): improve kilo chat message display * fix(mobile): add swipe reply for kilo chat * fix(mobile): animate kilo chat message long press * fix(mobile): disable kilo chat message text selection * fix(mobile): shorten kilo chat reaction actions * fix(mobile): preserve back gesture in kilo chat * docs(kiloclaw): note approval ownership invariant * docs(kiloclaw): explain resolved approval skip * fix(kiloclaw): validate webhook sandbox ids * fix(mobile): align kiloclaw tab content spacing * fix(dev): skip kiloclaw tunnels for docker-local * chore(dev): add mobile dev target * fix(mobile): preserve chat scroll on keyboard open * fix(mobile): reuse kiloclaw cards on claw tab * fix(mobile): add instance settings shortcut * fix(mobile): stabilize kilo chat composer scroll * fix(mobile): reduce composer keyboard padding * feat(kilo-chat): notify bot streamed messages * fix(kilo-chat): cancel deleted bot message notifications * fix(mobile): warm bot status on conversation list * fix(kilo-chat): dedupe bot status requests * fix(kilo-chat): raise bot notification threshold * fix(mobile): use normal chat composer keyboard * fix(mobile): stabilize chat composer on resume * fix(mobile): avoid duplicate notification route pushes * fix(mobile): auto-expand kilo chat composer * fix(mobile): avoid active chat message refetches * fix(mobile): skip own-message mark-read * fix(mobile): follow newest chat messages * fix(notifications): type scheduled action push data * fix(mobile): clean up knip unused code * fix(notifications): share push RPC types * fix(mobile): support dark mode in chat messages * fix(mobile): honor fresh kilo chat bot presence
1 parent 0fd0c33 commit d773511

405 files changed

Lines changed: 44666 additions & 7320 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/skills/kilo-design/reference/ux-writing.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,8 @@ expectations: `Provisioning machine. This usually takes 30–60 seconds.`
8989
Confirm only for truly irreversible or high-stakes actions. Name the
9090
action in both buttons:
9191

92-
- `Delete workspace permanently?`
93-
Primary: `Delete workspace` (destructive variant).
92+
- `Delete workspace permanently?`
93+
Primary: `Delete workspace` (destructive variant).
9494
Secondary: `Keep workspace`.
9595

9696
### Accessibility copy

.github/workflows/deploy-kiloclaw.yml

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ jobs:
5050
# - container/ (COPY container/TOOLS.md → /usr/local/share/kiloclaw/)
5151
# - plugins/kiloclaw-customizer/ (COPY plugin package for image install)
5252
# - plugins/kilo-chat/ (COPY plugin package for image install)
53+
# - plugins/kiloclaw-morning-briefing/ (COPY plugin package for image install)
5354
# - openclaw-pairing-list.js, openclaw-device-pairing-list.js (COPY)
5455
# - skills/ (COPY skills/ → /root/clawd/skills/)
5556
#
@@ -70,16 +71,7 @@ jobs:
7071
fi
7172
done
7273
73-
CONTENT_HASH=$(
74-
find Dockerfile controller/ container/ plugins/kiloclaw-customizer/ plugins/kilo-chat/ plugins/kiloclaw-morning-briefing/ skills/ \
75-
openclaw-pairing-list.js openclaw-device-pairing-list.js \
76-
-type f \
77-
| sort \
78-
| xargs sha256sum \
79-
| sha256sum \
80-
| cut -d' ' -f1 \
81-
| cut -c1-12
82-
)
74+
CONTENT_HASH="$(scripts/image-content-hash.sh --hash --dockerfile Dockerfile)"
8375
8476
if [ -z "$CONTENT_HASH" ] || [ ${#CONTENT_HASH} -ne 12 ]; then
8577
echo "::error::Failed to compute valid content hash"
@@ -412,6 +404,7 @@ jobs:
412404
echo "FLY_IMAGE_DIGEST=${DIGEST}"
413405
fi
414406
echo "OPENCLAW_VERSION=${OPENCLAW}"
407+
echo "FLY_IMAGE_CONTENT_MODE=production"
415408
echo "FLY_IMAGE_CONTENT_HASH=${CONTENT}"
416409
echo '```'
417410
echo ""

apps/mobile/.env

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,6 @@ CLOUD_AGENT_WS_URL=wss://cloud-agent-next.kilosessions.ai
55
SESSION_INGEST_WS_URL=wss://ingest.kilosessions.ai
66
APPSFLYER_DEV_KEY=jnoVs6KzXanpbKrqXckPu9
77
APPSFLYER_APP_ID=6761193135
8+
KILO_CHAT_URL=https://chat.kiloapps.io
9+
EVENT_SERVICE_URL=wss://events.kiloapps.io
10+
NOTIFICATIONS_URL=https://notifications.kiloapps.io

apps/mobile/.env.local.example

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Client-side env vars are bundled into the app binary and are not secret.
2+
#
3+
# localhost works for local tooling and most simulator flows. When running on a
4+
# physical phone, replace localhost with your development machine's LAN IP.
5+
# On macOS, get the active LAN IP with:
6+
# route -n get default | awk '/interface:/{print $2}' | xargs ipconfig getifaddr
7+
API_BASE_URL=http://localhost:3000
8+
WEB_BASE_URL=http://localhost:3000
9+
CLOUD_AGENT_WS_URL=ws://localhost:8794
10+
SESSION_INGEST_WS_URL=ws://localhost:8800
11+
APPSFLYER_DEV_KEY=jnoVs6KzXanpbKrqXckPu9
12+
APPSFLYER_APP_ID=6761193135
13+
KILO_CHAT_URL=http://localhost:8808
14+
EVENT_SERVICE_URL=ws://localhost:8809
15+
NOTIFICATIONS_URL=http://localhost:8804

apps/mobile/package.json

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,23 @@
1616
"lint": "pnpm -w exec oxlint --config apps/mobile/.oxlintrc.json apps/mobile/src",
1717
"format": "oxfmt src",
1818
"format:check": "oxfmt --list-different src",
19-
"check:unused": "knip",
19+
"check:unused": "node --env-file=.env node_modules/knip/bin/knip.js --no-config-hints",
2020
"test": "vitest run",
2121
"test:watch": "vitest"
2222
},
2323
"dependencies": {
2424
"@expo-google-fonts/jetbrains-mono": "^0.4.1",
2525
"@expo/react-native-action-sheet": "^4.1.1",
26+
"@kilocode/event-service": "workspace:*",
27+
"@kilocode/kilo-chat": "workspace:*",
28+
"@kilocode/kilo-chat-hooks": "workspace:*",
29+
"@kilocode/notifications": "workspace:*",
2630
"@kilocode/trpc": "workspace:*",
2731
"@react-native-community/netinfo": "11.5.2",
2832
"@rn-primitives/portal": "^1.3.0",
2933
"@rn-primitives/slot": "^1.2.0",
3034
"@sentry/react-native": "~7.11.0",
35+
"@shopify/flash-list": "2.0.2",
3136
"@tailwindcss/postcss": "^4.2.2",
3237
"@tanstack/react-query": "catalog:",
3338
"@trpc/client": "catalog:",
@@ -49,7 +54,6 @@
4954
"expo-font": "~55.0.6",
5055
"expo-haptics": "~55.0.13",
5156
"expo-image": "~55.0.8",
52-
"expo-image-manipulator": "~55.0.14",
5357
"expo-image-picker": "~55.0.17",
5458
"expo-insights": "55.0.15",
5559
"expo-linking": "~55.0.11",
@@ -78,11 +82,9 @@
7882
"react-native-svg": "15.15.3",
7983
"react-native-worklets": "0.7.2",
8084
"sonner-native": "^0.23.1",
81-
"stream-chat": "catalog:",
82-
"stream-chat-expo": "^8.13.7",
8385
"tailwind-merge": "^3.5.0",
8486
"tailwindcss": "^4.2.2",
85-
"zod": "catalog:"
87+
"ulid": "3.0.1"
8688
},
8789
"devDependencies": {
8890
"@sentry/cli": "catalog:",
@@ -93,5 +95,10 @@
9395
"typescript": "catalog:",
9496
"vitest": "^4.1.0"
9597
},
98+
"dependenciesMeta": {
99+
"@kilocode/kilo-chat-hooks": {
100+
"injected": true
101+
}
102+
},
96103
"private": true
97104
}

apps/mobile/src/app/(app)/(tabs)/(1_kiloclaw)/_layout.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,27 @@ export const unstable_settings = {
55
};
66

77
export default function KiloClawLayout() {
8-
return <Stack screenOptions={{ headerShown: false }} />;
8+
return (
9+
<Stack screenOptions={{ headerShown: false }}>
10+
<Stack.Screen name="index" />
11+
<Stack.Screen
12+
name="rename-conversation"
13+
options={{
14+
presentation: 'formSheet',
15+
sheetAllowedDetents: [0.5],
16+
sheetGrabberVisible: true,
17+
headerShown: false,
18+
}}
19+
/>
20+
<Stack.Screen
21+
name="chat/instance-picker"
22+
options={{
23+
presentation: 'formSheet',
24+
sheetAllowedDetents: [0.5, 1],
25+
sheetGrabberVisible: true,
26+
headerShown: false,
27+
}}
28+
/>
29+
</Stack>
30+
);
931
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { useLocalSearchParams, useRouter } from 'expo-router';
2+
import { useEffect } from 'react';
3+
import { toast } from 'sonner-native';
4+
5+
import { ChatSandboxRouteMounts } from '@/components/kilo-chat/chat-sandbox-route-mounts';
6+
import { ConversationScreen } from '@/components/kilo-chat/conversation-screen';
7+
import {
8+
getConversationRouteDecision,
9+
getConversationRouteErrorMessage,
10+
shouldRenderConversationScreen,
11+
} from '@/components/kilo-chat/conversation-route-state';
12+
import { useConversationDetail } from '@/components/kilo-chat/hooks/use-conversations';
13+
import { useKiloChatClient } from '@/components/kilo-chat/hooks/use-kilo-chat-client';
14+
import { chatSandboxPath } from '@/lib/kilo-chat-routes';
15+
16+
export default function ChatConversationRoute() {
17+
const params = useLocalSearchParams<{ 'sandbox-id': string; 'conversation-id': string }>();
18+
const sandboxId = params['sandbox-id'];
19+
const conversationId = params['conversation-id'];
20+
const router = useRouter();
21+
const client = useKiloChatClient();
22+
const conversationDetail = useConversationDetail(client, conversationId);
23+
const redirectPath = chatSandboxPath(sandboxId);
24+
const routeDecision = getConversationRouteDecision({
25+
detail: conversationDetail,
26+
routeSandboxId: sandboxId,
27+
});
28+
29+
useEffect(() => {
30+
if (conversationDetail.isError) {
31+
toast.error(getConversationRouteErrorMessage(conversationDetail.error));
32+
router.replace(redirectPath);
33+
return;
34+
}
35+
if (routeDecision === 'not-found') {
36+
toast.error('Conversation not found');
37+
router.replace(redirectPath);
38+
}
39+
}, [conversationDetail.error, conversationDetail.isError, redirectPath, routeDecision, router]);
40+
41+
if (
42+
!shouldRenderConversationScreen({
43+
detail: conversationDetail,
44+
routeSandboxId: sandboxId,
45+
}) ||
46+
!conversationDetail.data
47+
) {
48+
return null;
49+
}
50+
51+
return (
52+
<>
53+
<ChatSandboxRouteMounts activeConversationId={conversationId} />
54+
<ConversationScreen
55+
sandboxId={sandboxId}
56+
conversationId={conversationId}
57+
conversationTitle={conversationDetail.data.title ?? 'Untitled'}
58+
conversationRenameTitle={conversationDetail.data.title ?? ''}
59+
conversationMembers={conversationDetail.data.members}
60+
/>
61+
</>
62+
);
63+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { useLocalSearchParams } from 'expo-router';
2+
3+
import { ChatSandboxRouteMounts } from '@/components/kilo-chat/chat-sandbox-route-mounts';
4+
import { ConversationListScreen } from '@/components/kilo-chat/conversation-list-screen';
5+
import { useAllKiloClawInstances } from '@/lib/hooks/use-instance-context';
6+
7+
export default function ChatSandboxIndex() {
8+
const { 'sandbox-id': sandboxId } = useLocalSearchParams<{ 'sandbox-id': string }>();
9+
const { data: instances } = useAllKiloClawInstances();
10+
const instance = instances?.find(i => i.sandboxId === sandboxId);
11+
const sandboxLabel =
12+
instance?.botName ?? instance?.name ?? instance?.organizationName ?? 'KiloClaw';
13+
return (
14+
<>
15+
<ChatSandboxRouteMounts />
16+
<ConversationListScreen sandboxId={sandboxId} sandboxLabel={sandboxLabel} />
17+
</>
18+
);
19+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import * as Haptics from 'expo-haptics';
2+
import { useLocalSearchParams, useRouter } from 'expo-router';
3+
import { Check } from 'lucide-react-native';
4+
import { Pressable, ScrollView, View } from 'react-native';
5+
6+
import { StatusBadge } from '@/components/kiloclaw/status-badge';
7+
import { QueryError } from '@/components/query-error';
8+
import { Skeleton } from '@/components/ui/skeleton';
9+
import { Text } from '@/components/ui/text';
10+
import { useAllKiloClawInstances } from '@/lib/hooks/use-instance-context';
11+
import { useThemeColors } from '@/lib/hooks/use-theme-colors';
12+
import { kiloclawInstanceSwitcherTitle } from '@/lib/kiloclaw-display';
13+
import { chatSandboxPath } from '@/lib/kilo-chat-routes';
14+
15+
export default function InstancePickerScreen() {
16+
const router = useRouter();
17+
const colors = useThemeColors();
18+
const { currentId } = useLocalSearchParams<{ currentId: string }>();
19+
const instancesQuery = useAllKiloClawInstances();
20+
const { data: instances } = instancesQuery;
21+
22+
const handleSelect = (sandboxId: string) => {
23+
void Haptics.selectionAsync();
24+
if (sandboxId === currentId) {
25+
router.back();
26+
return;
27+
}
28+
router.dismissAll();
29+
router.push(chatSandboxPath(sandboxId));
30+
};
31+
32+
return (
33+
<ScrollView className="flex-1 bg-background">
34+
<View className="border-b border-border px-4 pb-3 pt-4">
35+
<View className="h-11 flex-row items-center justify-center">
36+
<Text className="text-lg font-semibold text-foreground">Switch Instance</Text>
37+
<Pressable
38+
onPress={() => {
39+
router.back();
40+
}}
41+
hitSlop={8}
42+
accessibilityRole="button"
43+
accessibilityLabel="Done"
44+
className="absolute right-0 rounded-full bg-secondary px-4 py-2 active:opacity-70 will-change-pressable"
45+
>
46+
<Text className="text-base font-medium text-foreground">Done</Text>
47+
</Pressable>
48+
</View>
49+
</View>
50+
51+
{instancesQuery.isPending ? (
52+
<View className="gap-3 px-4 py-4">
53+
<Skeleton className="h-16 rounded-xl" />
54+
<Skeleton className="h-16 rounded-xl" />
55+
<Skeleton className="h-16 rounded-xl" />
56+
</View>
57+
) : null}
58+
{instancesQuery.isError ? (
59+
<QueryError
60+
className="py-12"
61+
message="Could not load instances"
62+
onRetry={() => {
63+
void instancesQuery.refetch();
64+
}}
65+
/>
66+
) : null}
67+
{!instancesQuery.isPending && !instancesQuery.isError
68+
? (instances ?? []).map(instance => {
69+
const isCurrent = instance.sandboxId === currentId;
70+
const title = kiloclawInstanceSwitcherTitle(instance);
71+
return (
72+
<Pressable
73+
key={instance.sandboxId}
74+
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"
75+
onPress={() => {
76+
handleSelect(instance.sandboxId);
77+
}}
78+
accessibilityRole="button"
79+
accessibilityLabel={`${title}${isCurrent ? ', current' : ''}`}
80+
>
81+
<View className="min-w-0 flex-1 gap-1">
82+
<Text className="text-base font-semibold text-foreground" numberOfLines={1}>
83+
{title}
84+
</Text>
85+
<View className="flex-row flex-wrap items-center gap-x-3 gap-y-1">
86+
<Text variant="muted" numberOfLines={1}>
87+
{instance.organizationName ?? 'Personal'}
88+
</Text>
89+
<StatusBadge status={instance.status} />
90+
</View>
91+
</View>
92+
{isCurrent ? <Check size={18} color={colors.primary} /> : null}
93+
</Pressable>
94+
);
95+
})
96+
: null}
97+
</ScrollView>
98+
);
99+
}

0 commit comments

Comments
 (0)