diff --git a/packages/mobile/src/navigation/RootNavigator.tsx b/packages/mobile/src/navigation/RootNavigator.tsx index 551ed7ef4..55f12b54e 100644 --- a/packages/mobile/src/navigation/RootNavigator.tsx +++ b/packages/mobile/src/navigation/RootNavigator.tsx @@ -6,6 +6,7 @@ import SessionDetailScreen from "../screens/SessionDetailScreen"; import TerminalScreen from "../screens/TerminalScreen"; import SettingsScreen from "../screens/SettingsScreen"; import SpawnSessionScreen from "../screens/SpawnSessionScreen"; +import OrchestratorScreen from "../screens/OrchestratorScreen"; export type RootStackParamList = { Home: undefined; @@ -13,6 +14,7 @@ export type RootStackParamList = { Terminal: { sessionId: string; terminalWsUrl: string }; Settings: undefined; SpawnSession: undefined; + Orchestrator: undefined; }; const Stack = createNativeStackNavigator(); @@ -68,6 +70,11 @@ export default function RootNavigator() { component={SpawnSessionScreen} options={{ title: "New Session" }} /> + ); diff --git a/packages/mobile/src/screens/HomeScreen.tsx b/packages/mobile/src/screens/HomeScreen.tsx index a6efe1f54..5ee205641 100644 --- a/packages/mobile/src/screens/HomeScreen.tsx +++ b/packages/mobile/src/screens/HomeScreen.tsx @@ -38,17 +38,22 @@ export default function HomeScreen({ navigation }: Props) { React.useLayoutEffect(() => { navigation.setOptions({ headerRight: () => ( - + navigation.navigate("SpawnSession")} > + + navigation.navigate("Orchestrator")} + > + AO + navigation.navigate("Settings")} style={{ paddingRight: 4 }} > - Settings + {"\u2699\uFE0F"} ), diff --git a/packages/mobile/src/screens/OrchestratorScreen.tsx b/packages/mobile/src/screens/OrchestratorScreen.tsx new file mode 100644 index 000000000..cc33cf53a --- /dev/null +++ b/packages/mobile/src/screens/OrchestratorScreen.tsx @@ -0,0 +1,303 @@ +import React from "react"; +import { + View, + Text, + StyleSheet, + ScrollView, + TouchableOpacity, + ActivityIndicator, +} from "react-native"; +import type { NativeStackScreenProps } from "@react-navigation/native-stack"; +import type { RootStackParamList } from "../navigation/RootNavigator"; +import { useSessions } from "../hooks/useSessions"; +import { getAttentionLevel, isTerminal, type DashboardSession } from "../types"; + +type Props = NativeStackScreenProps; + +const CLI_COMMANDS = [ + { cmd: "ao start [project]", desc: "Start orchestrator + dashboard" }, + { cmd: "ao stop [project]", desc: "Stop orchestrator + dashboard" }, + { cmd: "ao spawn [issue]", desc: "Spawn a session for an issue" }, + { cmd: "ao batch-spawn ", desc: "Spawn multiple sessions" }, + { cmd: "ao session ls [-p ]", desc: "List all active sessions" }, + { cmd: "ao session kill ", desc: "Kill a session" }, + { cmd: "ao session restore ", desc: "Restore a crashed session" }, + { cmd: "ao session cleanup [-p ]", desc: "Clean up merged/closed sessions" }, + { cmd: "ao send [message]", desc: "Send message to a session" }, + { cmd: "ao status [-p ]", desc: "Show sessions with PR/CI status" }, + { cmd: "ao review-check [project]", desc: "Check PRs and trigger agents" }, + { cmd: "ao dashboard [-p ]", desc: "Start the web dashboard" }, + { cmd: "ao open [target]", desc: "Open session(s) in terminal" }, + { cmd: "ao init [project]", desc: "Initialize config file" }, +]; + +function getZoneCounts(sessions: DashboardSession[]) { + const counts = { merge: 0, respond: 0, review: 0, pending: 0, working: 0, done: 0 }; + for (const s of sessions) { + const level = getAttentionLevel(s); + counts[level]++; + } + return counts; +} + +export default function OrchestratorScreen({ navigation }: Props) { + const { sessions, stats, loading, error, refresh } = useSessions(); + + const orchestratorSession = sessions.find((s) => s.id.endsWith("-orchestrator")); + const workerSessions = sessions.filter((s) => !s.id.endsWith("-orchestrator")); + const zones = getZoneCounts(workerSessions); + const activeSessions = workerSessions.filter((s) => !isTerminal(s)); + + if (loading && sessions.length === 0) { + return ( + + + + ); + } + + if (error && sessions.length === 0) { + return ( + + {error} + + Retry + + + ); + } + + return ( + + {/* Orchestrator Status */} + + Orchestrator + {orchestratorSession ? ( + navigation.navigate("SessionDetail", { sessionId: orchestratorSession.id })} + > + + + Running + + {orchestratorSession.id} + Tap to view terminal + + ) : ( + + + + Not running + + Start with: ao start <project> + + )} + + + {/* Zone Overview */} + + Session Zones + + + + + + + + + + + {/* Stats */} + + Overview + + + + + + + {/* CLI Reference */} + + CLI Commands + Quick reference for managing sessions from terminal. + {CLI_COMMANDS.map((c, i) => ( + + {c.cmd} + {c.desc} + + ))} + + + ); +} + +function ZoneBadge({ label, count, color }: { label: string; count: number; color: string }) { + return ( + + {count} + {label} + + ); +} + +function StatRow({ label, value }: { label: string; value: string }) { + return ( + + {label} + {value} + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: "#0d1117", + }, + content: { + padding: 14, + paddingBottom: 32, + gap: 12, + }, + center: { + flex: 1, + alignItems: "center", + justifyContent: "center", + padding: 24, + backgroundColor: "#0d1117", + }, + section: { + backgroundColor: "#161b22", + borderRadius: 10, + padding: 16, + }, + sectionTitle: { + color: "#8b949e", + fontSize: 11, + fontWeight: "700", + letterSpacing: 0.8, + textTransform: "uppercase", + marginBottom: 12, + }, + hint: { + color: "#8b949e", + fontSize: 13, + lineHeight: 18, + marginBottom: 12, + }, + // Orchestrator card + orchestratorCard: { + backgroundColor: "#0d1117", + borderWidth: 1, + borderColor: "#30363d", + borderRadius: 8, + padding: 14, + }, + orchestratorRow: { + flexDirection: "row", + alignItems: "center", + gap: 8, + marginBottom: 6, + }, + dot: { + width: 10, + height: 10, + borderRadius: 5, + }, + orchestratorText: { + color: "#3fb950", + fontSize: 15, + fontWeight: "600", + }, + orchestratorId: { + color: "#8b949e", + fontSize: 12, + fontFamily: "monospace", + marginBottom: 4, + }, + orchestratorHint: { + color: "#6e7681", + fontSize: 12, + }, + // Zones grid + zonesGrid: { + flexDirection: "row", + flexWrap: "wrap", + gap: 8, + }, + zoneBadge: { + backgroundColor: "#0d1117", + borderWidth: 1, + borderColor: "#30363d", + borderRadius: 8, + paddingVertical: 10, + paddingHorizontal: 14, + alignItems: "center", + minWidth: 90, + flex: 1, + }, + zoneCount: { + fontSize: 22, + fontWeight: "700", + }, + zoneLabel: { + color: "#8b949e", + fontSize: 11, + fontWeight: "600", + marginTop: 2, + }, + // Stats + statRow: { + flexDirection: "row", + justifyContent: "space-between", + paddingVertical: 6, + }, + statLabel: { + color: "#8b949e", + fontSize: 13, + }, + statValue: { + color: "#e6edf3", + fontSize: 13, + fontWeight: "600", + }, + // CLI commands + cmdRow: { + backgroundColor: "#0d1117", + borderRadius: 6, + padding: 10, + marginBottom: 6, + borderWidth: 1, + borderColor: "#30363d", + }, + cmdText: { + color: "#58a6ff", + fontSize: 12, + fontFamily: "monospace", + marginBottom: 4, + }, + cmdDesc: { + color: "#8b949e", + fontSize: 12, + }, + // Error + errorText: { + color: "#f85149", + fontSize: 14, + textAlign: "center", + marginBottom: 16, + }, + retryButton: { + backgroundColor: "#21262d", + borderWidth: 1, + borderColor: "#30363d", + borderRadius: 6, + paddingHorizontal: 16, + paddingVertical: 8, + }, + retryText: { + color: "#e6edf3", + fontSize: 14, + }, +});