From 99cb0d4357b5329cd7d901cece053a7f85672a2e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 24 Jun 2026 18:19:31 +0000 Subject: [PATCH] chore: format with prettier [skip ci] --- mobile/CLAUDE.md | 4 +- mobile/README.md | 4 +- mobile/app.json | 104 ++-- mobile/app/(tabs)/_layout.tsx | 118 ++-- mobile/app/(tabs)/index.tsx | 340 +++++------ mobile/app/(tabs)/orchestrator.tsx | 404 ++++++------- mobile/app/(tabs)/prs.tsx | 414 ++++++------- mobile/app/(tabs)/settings.tsx | 417 ++++++------- mobile/app/_layout.tsx | 56 +- mobile/app/session/[id].tsx | 931 ++++++++++++++--------------- mobile/app/spawn.tsx | 187 +++--- mobile/eas.json | 50 +- mobile/images.d.ts | 6 +- mobile/lib/ProjectSwitcher.tsx | 45 +- mobile/lib/SessionCard.tsx | 200 +++---- mobile/lib/api.ts | 365 ++++++----- mobile/lib/config.ts | 73 +-- mobile/lib/mux.ts | 363 ++++++----- mobile/lib/store.tsx | 595 +++++++++--------- mobile/lib/theme.ts | 200 +++---- mobile/lib/ui.tsx | 584 +++++++++--------- mobile/package.json | 72 +-- mobile/tsconfig.json | 8 +- 23 files changed, 2698 insertions(+), 2842 deletions(-) diff --git a/mobile/CLAUDE.md b/mobile/CLAUDE.md index b7b125a9e..f765e0607 100644 --- a/mobile/CLAUDE.md +++ b/mobile/CLAUDE.md @@ -20,8 +20,8 @@ orchestrator launch/open — over LAN or Tailscale. ## Expo SDK is pinned to 54 — do not bump Expo Go supports a **single** SDK at a time. This app is pinned to **SDK 54** to -match the test phone's Expo Go. Symptoms of a mismatch: *"incompatible with this -version of Expo Go."* Don't change the SDK unless the user's Expo Go updated. +match the test phone's Expo Go. Symptoms of a mismatch: _"incompatible with this +version of Expo Go."_ Don't change the SDK unless the user's Expo Go updated. Read **v54** docs () — APIs differ by SDK. Pinned: `expo 54`, `react 19.1.0`, `react-native 0.81.5`, `expo-router 6`, `react-native-webview 13.15.0` (react + webview match `@fressh` peers exactly). diff --git a/mobile/README.md b/mobile/README.md index 6fd889e42..140f970a0 100644 --- a/mobile/README.md +++ b/mobile/README.md @@ -6,7 +6,7 @@ session and drive it, review and merge PRs, and launch/open orchestrators — fr your phone, over your LAN or Tailscale. This is an [Expo](https://expo.dev) (React Native) app. It lives in the AO monorepo -at `mobile/` but is a **standalone npm project** — it is *not* part of AO's pnpm +at `mobile/` but is a **standalone npm project** — it is _not_ part of AO's pnpm workspace. Run all commands below from inside `mobile/`. ## Requirements — Expo Go is pinned to SDK 54 @@ -15,7 +15,7 @@ workspace. Run all commands below from inside `mobile/`. > **The Expo Go app supports only ONE Expo SDK at a time** (whatever the latest > store build targets). This project is **pinned to Expo SDK 54** to match the > Expo Go currently installed on the test phone. If your Expo Go shows -> *"Project is incompatible with this version of Expo Go"*, your Expo Go and this +> _"Project is incompatible with this version of Expo Go"_, your Expo Go and this > project's SDK don't match. > > - **Don't bump the Expo SDK** unless your Expo Go has updated to that SDK. When diff --git a/mobile/app.json b/mobile/app.json index 73fa3a824..895210af1 100644 --- a/mobile/app.json +++ b/mobile/app.json @@ -1,54 +1,54 @@ { - "expo": { - "name": "AO", - "slug": "ao-mobile", - "owner": "priyanchew", - "version": "1.0.0", - "orientation": "portrait", - "icon": "./assets/icon.png", - "userInterfaceStyle": "dark", - "backgroundColor": "#0a0b0d", - "scheme": "aomobile", - "splash": { - "image": "./assets/splash-icon.png", - "resizeMode": "contain", - "backgroundColor": "#0a0b0d" - }, - "ios": { - "supportsTablet": true, - "bundleIdentifier": "aoagents.ao", - "config": { - "usesNonExemptEncryption": false - } - }, - "android": { - "package": "aoagents.ao", - "adaptiveIcon": { - "backgroundColor": "#0a0b0d", - "foregroundImage": "./assets/android-icon-foreground.png" - }, - "predictiveBackGestureEnabled": false - }, - "web": { - "favicon": "./assets/favicon.png", - "bundler": "metro" - }, - "plugins": [ - "expo-router", - [ - "expo-build-properties", - { - "android": { - "usesCleartextTraffic": true - } - } - ] - ], - "extra": { - "router": {}, - "eas": { - "projectId": "5bd2863a-4238-4f2e-8017-f5df7e6899c3" - } - } - } + "expo": { + "name": "AO", + "slug": "ao-mobile", + "owner": "priyanchew", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/icon.png", + "userInterfaceStyle": "dark", + "backgroundColor": "#0a0b0d", + "scheme": "aomobile", + "splash": { + "image": "./assets/splash-icon.png", + "resizeMode": "contain", + "backgroundColor": "#0a0b0d" + }, + "ios": { + "supportsTablet": true, + "bundleIdentifier": "aoagents.ao", + "config": { + "usesNonExemptEncryption": false + } + }, + "android": { + "package": "aoagents.ao", + "adaptiveIcon": { + "backgroundColor": "#0a0b0d", + "foregroundImage": "./assets/android-icon-foreground.png" + }, + "predictiveBackGestureEnabled": false + }, + "web": { + "favicon": "./assets/favicon.png", + "bundler": "metro" + }, + "plugins": [ + "expo-router", + [ + "expo-build-properties", + { + "android": { + "usesCleartextTraffic": true + } + } + ] + ], + "extra": { + "router": {}, + "eas": { + "projectId": "5bd2863a-4238-4f2e-8017-f5df7e6899c3" + } + } + } } diff --git a/mobile/app/(tabs)/_layout.tsx b/mobile/app/(tabs)/_layout.tsx index d5e089608..1013fec4e 100644 --- a/mobile/app/(tabs)/_layout.tsx +++ b/mobile/app/(tabs)/_layout.tsx @@ -1,65 +1,59 @@ -import { Feather } from '@expo/vector-icons'; -import { Tabs } from 'expo-router'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { theme } from '../../lib/theme'; +import { Feather } from "@expo/vector-icons"; +import { Tabs } from "expo-router"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { theme } from "../../lib/theme"; export default function TabsLayout() { - const insets = useSafeAreaInsets(); - return ( - - , - }} - /> - ( - - ), - }} - /> - ( - - ), - }} - /> - ( - - ), - }} - /> - - ); + const insets = useSafeAreaInsets(); + return ( + + , + }} + /> + , + }} + /> + , + }} + /> + , + }} + /> + + ); } diff --git a/mobile/app/(tabs)/index.tsx b/mobile/app/(tabs)/index.tsx index 54b98a5b9..1f981b8aa 100644 --- a/mobile/app/(tabs)/index.tsx +++ b/mobile/app/(tabs)/index.tsx @@ -1,201 +1,185 @@ -import { Feather } from '@expo/vector-icons'; -import { useRouter } from 'expo-router'; -import { useCallback, useMemo, useState } from 'react'; -import { - ActivityIndicator, - Pressable, - RefreshControl, - SectionList, - StyleSheet, - Text, - View, -} from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { attentionOf, type DashboardSession } from '../../lib/api'; -import { ProjectSwitcher } from '../../lib/ProjectSwitcher'; -import { SessionCard } from '../../lib/SessionCard'; -import { useApp, useVisibleSessions } from '../../lib/store'; -import { attentionMeta, theme } from '../../lib/theme'; -import { Button, ConnectionPill, EmptyState, ScreenHeader, SectionHeader } from '../../lib/ui'; +import { Feather } from "@expo/vector-icons"; +import { useRouter } from "expo-router"; +import { useCallback, useMemo, useState } from "react"; +import { ActivityIndicator, Pressable, RefreshControl, SectionList, StyleSheet, Text, View } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { attentionOf, type DashboardSession } from "../../lib/api"; +import { ProjectSwitcher } from "../../lib/ProjectSwitcher"; +import { SessionCard } from "../../lib/SessionCard"; +import { useApp, useVisibleSessions } from "../../lib/store"; +import { attentionMeta, theme } from "../../lib/theme"; +import { Button, ConnectionPill, EmptyState, ScreenHeader, SectionHeader } from "../../lib/ui"; type Section = { key: string; label: string; color: string; order: number; data: DashboardSession[] }; function groupByAttention(sessions: DashboardSession[]): Section[] { - const buckets = new Map(); - for (const s of sessions) { - const key = attentionOf(s); - if (!buckets.has(key)) buckets.set(key, []); - buckets.get(key)!.push(s); - } - return [...buckets.entries()] - .map(([key, data]) => { - const meta = attentionMeta[key] ?? { - label: key, - color: theme.textTertiary, - order: 99, - }; - return { key, label: meta.label, color: meta.color, order: meta.order, data }; - }) - .sort((a, b) => a.order - b.order); + const buckets = new Map(); + for (const s of sessions) { + const key = attentionOf(s); + if (!buckets.has(key)) buckets.set(key, []); + buckets.get(key)!.push(s); + } + return [...buckets.entries()] + .map(([key, data]) => { + const meta = attentionMeta[key] ?? { + label: key, + color: theme.textTertiary, + order: 99, + }; + return { key, label: meta.label, color: meta.color, order: meta.order, data }; + }) + .sort((a, b) => a.order - b.order); } export default function FleetScreen() { - const router = useRouter(); - const insets = useSafeAreaInsets(); - const { configured, loading, error, connection, config, refresh } = useApp(); - const sessions = useVisibleSessions(); - const [refreshing, setRefreshing] = useState(false); + const router = useRouter(); + const insets = useSafeAreaInsets(); + const { configured, loading, error, connection, config, refresh } = useApp(); + const sessions = useVisibleSessions(); + const [refreshing, setRefreshing] = useState(false); - const sections = useMemo(() => groupByAttention(sessions), [sessions]); + const sections = useMemo(() => groupByAttention(sessions), [sessions]); - const counts = useMemo(() => { - let working = 0, - needsYou = 0, - mergeable = 0; - for (const s of sessions) { - const a = attentionOf(s); - if (a === 'working') working++; - else if (a === 'respond' || a === 'action') needsYou++; - else if (a === 'merge') mergeable++; - } - return { working, needsYou, mergeable }; - }, [sessions]); + const counts = useMemo(() => { + let working = 0, + needsYou = 0, + mergeable = 0; + for (const s of sessions) { + const a = attentionOf(s); + if (a === "working") working++; + else if (a === "respond" || a === "action") needsYou++; + else if (a === "merge") mergeable++; + } + return { working, needsYou, mergeable }; + }, [sessions]); - const onRefresh = useCallback(async () => { - setRefreshing(true); - await refresh(); - setRefreshing(false); - }, [refresh]); + const onRefresh = useCallback(async () => { + setRefreshing(true); + await refresh(); + setRefreshing(false); + }, [refresh]); - if (!configured) { - return ( - - - router.push('/settings')} /> - } - /> - - ); - } + if (!configured) { + return ( + + + router.push("/settings")} />} + /> + + ); + } - return ( - - - } - /> + return ( + + + } /> - - - - - + + + + + - + - {loading && sessions.length === 0 ? ( - - - - ) : ( - `${item.projectId}:${item.id}`} - contentContainerStyle={{ paddingBottom: 120 }} - stickySectionHeadersEnabled={false} - refreshControl={ - - } - renderSectionHeader={({ section }) => ( - - )} - renderItem={({ item }) => } - ListEmptyComponent={ - error ? ( - } - /> - ) : ( - router.push('/spawn')} />} - /> - ) - } - /> - )} + {loading && sessions.length === 0 ? ( + + + + ) : ( + `${item.projectId}:${item.id}`} + contentContainerStyle={{ paddingBottom: 120 }} + stickySectionHeadersEnabled={false} + refreshControl={} + renderSectionHeader={({ section }) => ( + + )} + renderItem={({ item }) => } + ListEmptyComponent={ + error ? ( + } + /> + ) : ( + router.push("/spawn")} />} + /> + ) + } + /> + )} - {/* Spawn FAB */} - router.push('/spawn')} - style={({ pressed }) => [styles.fab, pressed && { opacity: 0.85 }]} - > - - - - ); + {/* Spawn FAB */} + router.push("/spawn")} + style={({ pressed }) => [styles.fab, pressed && { opacity: 0.85 }]} + > + + + + ); } function Stat({ n, label, color }: { n: number; label: string; color: string }) { - return ( - - 0 ? color : theme.textFaint }]}>{n} - {label} - - ); + return ( + + 0 ? color : theme.textFaint }]}>{n} + {label} + + ); } const styles = StyleSheet.create({ - screen: { flex: 1, backgroundColor: theme.bgBase }, - center: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingVertical: 60 }, - stats: { - flexDirection: 'row', - gap: 10, - paddingHorizontal: 16, - paddingTop: 4, - paddingBottom: 14, - }, - stat: { - flex: 1, - backgroundColor: theme.bgElevated, - borderRadius: 12, - borderWidth: 1, - borderColor: theme.borderSubtle, - paddingVertical: 12, - paddingHorizontal: 14, - }, - statN: { fontSize: 24, fontWeight: '800', fontFamily: theme.fontMono }, - statLabel: { color: theme.textTertiary, fontSize: 11, fontWeight: '600', marginTop: 2 }, - fab: { - position: 'absolute', - right: 18, - bottom: 24, - width: 56, - height: 56, - borderRadius: 28, - backgroundColor: theme.blue, - alignItems: 'center', - justifyContent: 'center', - shadowColor: '#000', - shadowOpacity: 0.4, - shadowRadius: 12, - shadowOffset: { width: 0, height: 4 }, - elevation: 8, - }, + screen: { flex: 1, backgroundColor: theme.bgBase }, + center: { flex: 1, alignItems: "center", justifyContent: "center", paddingVertical: 60 }, + stats: { + flexDirection: "row", + gap: 10, + paddingHorizontal: 16, + paddingTop: 4, + paddingBottom: 14, + }, + stat: { + flex: 1, + backgroundColor: theme.bgElevated, + borderRadius: 12, + borderWidth: 1, + borderColor: theme.borderSubtle, + paddingVertical: 12, + paddingHorizontal: 14, + }, + statN: { fontSize: 24, fontWeight: "800", fontFamily: theme.fontMono }, + statLabel: { color: theme.textTertiary, fontSize: 11, fontWeight: "600", marginTop: 2 }, + fab: { + position: "absolute", + right: 18, + bottom: 24, + width: 56, + height: 56, + borderRadius: 28, + backgroundColor: theme.blue, + alignItems: "center", + justifyContent: "center", + shadowColor: "#000", + shadowOpacity: 0.4, + shadowRadius: 12, + shadowOffset: { width: 0, height: 4 }, + elevation: 8, + }, }); diff --git a/mobile/app/(tabs)/orchestrator.tsx b/mobile/app/(tabs)/orchestrator.tsx index 7332751ba..333857955 100644 --- a/mobile/app/(tabs)/orchestrator.tsx +++ b/mobile/app/(tabs)/orchestrator.tsx @@ -1,234 +1,222 @@ -import { Feather } from '@expo/vector-icons'; -import { useRouter } from 'expo-router'; -import { useState } from 'react'; -import { Alert, RefreshControl, ScrollView, StyleSheet, Text, View } from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { attentionOf, type DashboardSession, type OrchestratorLink } from '../../lib/api'; -import { useApp } from '../../lib/store'; -import { attentionMeta, statusVisual, theme, type AttentionLevel, type StatusVisual } from '../../lib/theme'; -import { Button, ConnectionPill, Dot, EmptyState, ScreenHeader } from '../../lib/ui'; +import { Feather } from "@expo/vector-icons"; +import { useRouter } from "expo-router"; +import { useState } from "react"; +import { Alert, RefreshControl, ScrollView, StyleSheet, Text, View } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { attentionOf, type DashboardSession, type OrchestratorLink } from "../../lib/api"; +import { useApp } from "../../lib/store"; +import { attentionMeta, statusVisual, theme, type AttentionLevel, type StatusVisual } from "../../lib/theme"; +import { Button, ConnectionPill, Dot, EmptyState, ScreenHeader } from "../../lib/ui"; -const ZONE_ORDER: AttentionLevel[] = ['merge', 'respond', 'review', 'pending', 'working', 'done']; +const ZONE_ORDER: AttentionLevel[] = ["merge", "respond", "review", "pending", "working", "done"]; export default function OrchestratorScreen() { - const insets = useSafeAreaInsets(); - const { configured, connection, projects, sessions, orchestrators, refresh } = useApp(); - const [refreshing, setRefreshing] = useState(false); + const insets = useSafeAreaInsets(); + const { configured, connection, projects, sessions, orchestrators, refresh } = useApp(); + const [refreshing, setRefreshing] = useState(false); - // Always show every project's orchestrator here — no per-project filtering. - const visibleProjects = projects; + // Always show every project's orchestrator here — no per-project filtering. + const visibleProjects = projects; - const onRefresh = async () => { - setRefreshing(true); - await refresh(); - setRefreshing(false); - }; + const onRefresh = async () => { + setRefreshing(true); + await refresh(); + setRefreshing(false); + }; - if (!configured) { - return ( - - - - - ); - } + if (!configured) { + return ( + + + + + ); + } - return ( - - - } - /> + return ( + + + } + /> - - } - > - {visibleProjects.length === 0 ? ( - - ) : ( - visibleProjects.map((p) => { - const link = orchestrators.find((o) => o.projectId === p.id) ?? null; - const workers = sessions.filter((s) => s.projectId === p.id && s.id !== link?.id); - return ( - - ); - }) - )} - - - ); + } + > + {visibleProjects.length === 0 ? ( + + ) : ( + visibleProjects.map((p) => { + const link = orchestrators.find((o) => o.projectId === p.id) ?? null; + const workers = sessions.filter((s) => s.projectId === p.id && s.id !== link?.id); + return ( + + ); + }) + )} + + + ); } function zoneCounts(sessions: DashboardSession[]): Record { - const out: Record = {}; - for (const s of sessions) { - const a = attentionOf(s); - out[a] = (out[a] ?? 0) + 1; - } - return out; + const out: Record = {}; + for (const s of sessions) { + const a = attentionOf(s); + out[a] = (out[a] ?? 0) + 1; + } + return out; } function OrchestratorCard({ - projectId, - projectName, - link, - workerCount, - zones, + projectId, + projectName, + link, + workerCount, + zones, }: { - projectId: string; - projectName: string; - link: OrchestratorLink | null; - workerCount: number; - zones: Record; + projectId: string; + projectName: string; + link: OrchestratorLink | null; + workerCount: number; + zones: Record; }) { - const router = useRouter(); - const { launchConductor } = useApp(); - const [busy, setBusy] = useState(false); + const router = useRouter(); + const { launchConductor } = useApp(); + const [busy, setBusy] = useState(false); - // The link only appears when an orchestrator exists — so its presence means - // it's openable. Some AO builds add hasRuntime/isTerminal; treat those as - // "stopped" only when explicitly flagged, never on a missing field. - const present = !!link?.id; - const stopped = present && (link.hasRuntime === false || link.isTerminal === true); - const open = present && !stopped; - const v: StatusVisual = link?.status - ? statusVisual(link.status) - : { color: theme.blue, label: 'Online' }; + // The link only appears when an orchestrator exists — so its presence means + // it's openable. Some AO builds add hasRuntime/isTerminal; treat those as + // "stopped" only when explicitly flagged, never on a missing field. + const present = !!link?.id; + const stopped = present && (link.hasRuntime === false || link.isTerminal === true); + const open = present && !stopped; + const v: StatusVisual = link?.status ? statusVisual(link.status) : { color: theme.blue, label: "Online" }; - const openTerminal = (id: string) => - router.push({ pathname: '/session/[id]', params: { id, projectId } }); + const openTerminal = (id: string) => router.push({ pathname: "/session/[id]", params: { id, projectId } }); - const onLaunch = async (clean: boolean) => { - setBusy(true); - try { - const l = await launchConductor(projectId, clean); - if (l?.id) openTerminal(l.id); - } catch (e) { - Alert.alert('Could not launch', e instanceof Error ? e.message : 'Unknown error'); - } finally { - setBusy(false); - } - }; + const onLaunch = async (clean: boolean) => { + setBusy(true); + try { + const l = await launchConductor(projectId, clean); + if (l?.id) openTerminal(l.id); + } catch (e) { + Alert.alert("Could not launch", e instanceof Error ? e.message : "Unknown error"); + } finally { + setBusy(false); + } + }; - return ( - - - - - - - {projectName} - - - - {open ? v.label : stopped ? 'Stopped' : 'Not started'} - - · {workerCount} worker{workerCount === 1 ? '' : 's'} - - - + return ( + + + + + + + {projectName} + + + + {open ? v.label : stopped ? "Stopped" : "Not started"} + + + · {workerCount} worker{workerCount === 1 ? "" : "s"} + + + + - {workerCount > 0 ? ( - - {ZONE_ORDER.filter((z) => zones[z]).map((z) => { - const m = attentionMeta[z]; - return ( - - - {zones[z]} - {m.label} - - ); - })} - - ) : null} + {workerCount > 0 ? ( + + {ZONE_ORDER.filter((z) => zones[z]).map((z) => { + const m = attentionMeta[z]; + return ( + + + {zones[z]} + {m.label} + + ); + })} + + ) : null} - - {open ? ( - <> -