From 4c4fada522551b8bc7c38a96ea23c4a2512fd189 Mon Sep 17 00:00:00 2001 From: Ashish Huddar Date: Wed, 25 Mar 2026 12:00:59 +0530 Subject: [PATCH 01/90] chore: remove React Native/Expo mobile app The web dashboard now has full PWA and mobile-responsive support, making the separate native mobile app redundant. - Delete packages/mobile/ directory - Remove mobile exclusion from pnpm-workspace.yaml - Remove mobile ignore from eslint.config.js - Update SETUP.md example project --- SETUP.md | 8 +- eslint.config.js | 1 - packages/mobile/.gitignore | 34 - packages/mobile/app.json | 55 -- packages/mobile/assets/adaptive-icon.png | Bin 23856 -> 0 bytes packages/mobile/assets/favicon.png | Bin 23856 -> 0 bytes packages/mobile/assets/icon.png | Bin 23856 -> 0 bytes packages/mobile/assets/splash.png | Bin 23856 -> 0 bytes packages/mobile/babel.config.js | 6 - packages/mobile/eas.json | 21 - packages/mobile/index.js | 4 - packages/mobile/metro.config.js | 10 - packages/mobile/package.json | 33 - packages/mobile/src/App.tsx | 51 -- .../mobile/src/components/AttentionBadge.tsx | 39 - .../mobile/src/components/SessionCard.tsx | 143 ---- packages/mobile/src/components/StatBar.tsx | 51 -- .../mobile/src/context/BackendContext.tsx | 153 ---- packages/mobile/src/hooks/useSession.ts | 93 --- .../src/hooks/useSessionNotifications.ts | 63 -- packages/mobile/src/hooks/useSessions.ts | 88 --- .../mobile/src/navigation/RootNavigator.tsx | 90 --- .../src/notifications/backgroundTask.ts | 83 -- packages/mobile/src/notifications/index.ts | 86 --- .../mobile/src/screens/CommandsScreen.tsx | 75 -- packages/mobile/src/screens/HomeScreen.tsx | 176 ----- .../mobile/src/screens/OrchestratorScreen.tsx | 370 --------- .../src/screens/SessionDetailScreen.tsx | 729 ------------------ .../mobile/src/screens/SettingsScreen.tsx | 443 ----------- .../mobile/src/screens/SpawnSessionScreen.tsx | 173 ----- .../mobile/src/screens/TerminalScreen.tsx | 122 --- packages/mobile/src/terminal/terminal-html.ts | 178 ----- packages/mobile/src/types/index.ts | 222 ------ packages/mobile/tsconfig.json | 17 - pnpm-workspace.yaml | 1 - 35 files changed, 4 insertions(+), 3614 deletions(-) delete mode 100644 packages/mobile/.gitignore delete mode 100644 packages/mobile/app.json delete mode 100644 packages/mobile/assets/adaptive-icon.png delete mode 100644 packages/mobile/assets/favicon.png delete mode 100644 packages/mobile/assets/icon.png delete mode 100644 packages/mobile/assets/splash.png delete mode 100644 packages/mobile/babel.config.js delete mode 100644 packages/mobile/eas.json delete mode 100644 packages/mobile/index.js delete mode 100644 packages/mobile/metro.config.js delete mode 100644 packages/mobile/package.json delete mode 100644 packages/mobile/src/App.tsx delete mode 100644 packages/mobile/src/components/AttentionBadge.tsx delete mode 100644 packages/mobile/src/components/SessionCard.tsx delete mode 100644 packages/mobile/src/components/StatBar.tsx delete mode 100644 packages/mobile/src/context/BackendContext.tsx delete mode 100644 packages/mobile/src/hooks/useSession.ts delete mode 100644 packages/mobile/src/hooks/useSessionNotifications.ts delete mode 100644 packages/mobile/src/hooks/useSessions.ts delete mode 100644 packages/mobile/src/navigation/RootNavigator.tsx delete mode 100644 packages/mobile/src/notifications/backgroundTask.ts delete mode 100644 packages/mobile/src/notifications/index.ts delete mode 100644 packages/mobile/src/screens/CommandsScreen.tsx delete mode 100644 packages/mobile/src/screens/HomeScreen.tsx delete mode 100644 packages/mobile/src/screens/OrchestratorScreen.tsx delete mode 100644 packages/mobile/src/screens/SessionDetailScreen.tsx delete mode 100644 packages/mobile/src/screens/SettingsScreen.tsx delete mode 100644 packages/mobile/src/screens/SpawnSessionScreen.tsx delete mode 100644 packages/mobile/src/screens/TerminalScreen.tsx delete mode 100644 packages/mobile/src/terminal/terminal-html.ts delete mode 100644 packages/mobile/src/types/index.ts delete mode 100644 packages/mobile/tsconfig.json diff --git a/SETUP.md b/SETUP.md index e745b5daa..a34dc9bfb 100644 --- a/SETUP.md +++ b/SETUP.md @@ -618,10 +618,10 @@ projects: path: ~/backend sessionPrefix: api - mobile: - repo: org/mobile - path: ~/mobile - sessionPrefix: mob + docs: + repo: org/docs + path: ~/docs + sessionPrefix: doc ``` See [examples/multi-project.yaml](./examples/multi-project.yaml) for full example. diff --git a/eslint.config.js b/eslint.config.js index cc1167afc..c30ff0f15 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -15,7 +15,6 @@ export default tseslint.config( "packages/web/postcss.config.mjs", "test-clipboard*.mjs", "test-clipboard*.sh", - "packages/mobile/**", ], }, diff --git a/packages/mobile/.gitignore b/packages/mobile/.gitignore deleted file mode 100644 index bf107f1b0..000000000 --- a/packages/mobile/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -# Expo -.expo/ -dist/ -web-build/ - -# Native build folders (generated by `expo prebuild`) -android/ -ios/ - -# Native -*.orig.* -*.jks -*.p8 -*.p12 -*.key -*.mobileprovision - -# Metro -.metro-health-check* - -# debug -npm-debug.* -yarn-debug.* -yarn-error.* - -# macOS -.DS_Store -*.pem - -# local env files -.env*.local - -# npm (project uses pnpm) -package-lock.json diff --git a/packages/mobile/app.json b/packages/mobile/app.json deleted file mode 100644 index ad1e56dab..000000000 --- a/packages/mobile/app.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "expo": { - "name": "Agent Orchestrator", - "slug": "ao-mobile", - "version": "1.0.0", - "orientation": "portrait", - "icon": "./assets/icon.png", - "userInterfaceStyle": "dark", - "splash": { - "image": "./assets/splash.png", - "resizeMode": "contain", - "backgroundColor": "#0d1117" - }, - "assetBundlePatterns": [ - "**/*" - ], - "ios": { - "supportsTablet": true, - "bundleIdentifier": "com.composio.ao-mobile", - "infoPlist": { - "UIBackgroundModes": [ - "fetch", - "processing", - "remote-notification" - ] - } - }, - "android": { - "adaptiveIcon": { - "foregroundImage": "./assets/adaptive-icon.png", - "backgroundColor": "#0d1117" - }, - "package": "com.composio.aomobile", - "usesCleartextTraffic": true - }, - "web": { - "favicon": "./assets/favicon.png" - }, - "plugins": [ - "expo-background-task", - [ - "expo-notifications", - { - "color": "#f85149", - "defaultChannel": "ao-respond" - } - ] - ], - "extra": { - "eas": { - "projectId": "de59ca61-8fd4-47bb-bdea-6e1a50ade3df" - } - } - } -} diff --git a/packages/mobile/assets/adaptive-icon.png b/packages/mobile/assets/adaptive-icon.png deleted file mode 100644 index 9e252931309f28d881063e9a1426f328089d10e1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23856 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 AZvX%Q diff --git a/packages/mobile/assets/favicon.png b/packages/mobile/assets/favicon.png deleted file mode 100644 index 9e252931309f28d881063e9a1426f328089d10e1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23856 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 AZvX%Q diff --git a/packages/mobile/assets/icon.png b/packages/mobile/assets/icon.png deleted file mode 100644 index 9e252931309f28d881063e9a1426f328089d10e1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23856 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 AZvX%Q diff --git a/packages/mobile/assets/splash.png b/packages/mobile/assets/splash.png deleted file mode 100644 index 9e252931309f28d881063e9a1426f328089d10e1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23856 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 AZvX%Q diff --git a/packages/mobile/babel.config.js b/packages/mobile/babel.config.js deleted file mode 100644 index 73ebf58e3..000000000 --- a/packages/mobile/babel.config.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = function (api) { - api.cache(true); - return { - presets: ["babel-preset-expo"], - }; -}; diff --git a/packages/mobile/eas.json b/packages/mobile/eas.json deleted file mode 100644 index 59b609d6b..000000000 --- a/packages/mobile/eas.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "cli": { - "version": ">= 12.0.0" - }, - "build": { - "development": { - "developmentClient": true, - "distribution": "internal" - }, - "preview": { - "distribution": "internal", - "ios": { - "simulator": false - } - }, - "production": {} - }, - "submit": { - "production": {} - } -} diff --git a/packages/mobile/index.js b/packages/mobile/index.js deleted file mode 100644 index 000be560b..000000000 --- a/packages/mobile/index.js +++ /dev/null @@ -1,4 +0,0 @@ -import { registerRootComponent } from "expo"; -import App from "./src/App"; - -registerRootComponent(App); diff --git a/packages/mobile/metro.config.js b/packages/mobile/metro.config.js deleted file mode 100644 index c75ab9503..000000000 --- a/packages/mobile/metro.config.js +++ /dev/null @@ -1,10 +0,0 @@ -const { getDefaultConfig } = require("expo/metro-config"); - -const config = getDefaultConfig(__dirname); - -// Disable package exports resolution — some packages (math-intrinsics, expo-application) -// have invalid or incomplete `exports` fields that cause Metro bundling failures. -// Falling back to classic file-based resolution fixes these. -config.resolver.unstable_enablePackageExports = false; - -module.exports = config; diff --git a/packages/mobile/package.json b/packages/mobile/package.json deleted file mode 100644 index 37911a4d6..000000000 --- a/packages/mobile/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "@composio/ao-mobile", - "version": "0.1.0", - "private": true, - "main": "index.js", - "scripts": { - "start": "expo start", - "android": "expo run:android", - "ios": "expo run:ios", - "web": "expo start --web" - }, - "dependencies": { - "@react-native-async-storage/async-storage": "2.1.2", - "@react-navigation/native": "^6.1.18", - "@react-navigation/native-stack": "^6.11.0", - "expo": "~53.0.0", - "expo-application": "~6.1.5", - "expo-background-task": "~0.2.8", - "expo-notifications": "~0.31.5", - "expo-status-bar": "~2.2.3", - "expo-task-manager": "~13.1.6", - "react": "19.0.0", - "react-native": "0.79.6", - "react-native-safe-area-context": "5.4.0", - "react-native-screens": "~4.11.1", - "react-native-webview": "13.13.5" - }, - "devDependencies": { - "@babel/core": "^7.24.0", - "@types/react": "~19.0.10", - "typescript": "~5.8.3" - } -} diff --git a/packages/mobile/src/App.tsx b/packages/mobile/src/App.tsx deleted file mode 100644 index c44f3c870..000000000 --- a/packages/mobile/src/App.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React, { useEffect } from "react"; -import { SafeAreaProvider } from "react-native-safe-area-context"; -import * as Notifications from "expo-notifications"; -import { BackendProvider } from "./context/BackendContext"; -import RootNavigator from "./navigation/RootNavigator"; -import { navigationRef } from "./navigation/RootNavigator"; -import { setupNotifications } from "./notifications"; -import { registerBackgroundTask } from "./notifications/backgroundTask"; - -/** Navigate to a session when a notification is tapped. */ -function handleNotificationResponse(response: Notifications.NotificationResponse) { - const data = response.notification.request.content.data as - | { sessionId?: string } - | undefined; - const sessionId = data?.sessionId; - if (!sessionId) return; - - // Wait for navigation to be ready (cold-start case) - const tryNavigate = () => { - if (navigationRef.isReady()) { - navigationRef.navigate("SessionDetail", { sessionId }); - } else { - setTimeout(tryNavigate, 100); - } - }; - tryNavigate(); -} - -export default function App() { - useEffect(() => { - void setupNotifications(); - void registerBackgroundTask(); - - // Handle notification tapped while app was killed or in background - void Notifications.getLastNotificationResponseAsync().then((response) => { - if (response) handleNotificationResponse(response); - }); - - // Handle notification tapped while app is running - const sub = Notifications.addNotificationResponseReceivedListener(handleNotificationResponse); - return () => sub.remove(); - }, []); - - return ( - - - - - - ); -} diff --git a/packages/mobile/src/components/AttentionBadge.tsx b/packages/mobile/src/components/AttentionBadge.tsx deleted file mode 100644 index 34e9b3e26..000000000 --- a/packages/mobile/src/components/AttentionBadge.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React from "react"; -import { View, Text, StyleSheet } from "react-native"; -import { ATTENTION_COLORS, type AttentionLevel } from "../types"; - -interface Props { - level: AttentionLevel; -} - -const LABELS: Record = { - merge: "MERGE", - respond: "RESPOND", - review: "REVIEW", - pending: "PENDING", - working: "WORKING", - done: "DONE", -}; - -export default function AttentionBadge({ level }: Props) { - const color = ATTENTION_COLORS[level]; - return ( - - {LABELS[level]} - - ); -} - -const styles = StyleSheet.create({ - badge: { - borderWidth: 1, - borderRadius: 4, - paddingHorizontal: 6, - paddingVertical: 2, - }, - label: { - fontSize: 10, - fontWeight: "700", - letterSpacing: 0.5, - }, -}); diff --git a/packages/mobile/src/components/SessionCard.tsx b/packages/mobile/src/components/SessionCard.tsx deleted file mode 100644 index dc1d84338..000000000 --- a/packages/mobile/src/components/SessionCard.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import React from "react"; -import { View, Text, TouchableOpacity, StyleSheet } from "react-native"; -import { - getAttentionLevel, - relativeTime, - ATTENTION_COLORS, - type DashboardSession, -} from "../types"; -import AttentionBadge from "./AttentionBadge"; - -interface Props { - session: DashboardSession; - onPress: () => void; -} - -export default function SessionCard({ session, onPress }: Props) { - const level = getAttentionLevel(session); - const color = ATTENTION_COLORS[level]; - const time = relativeTime(session.lastActivityAt); - - return ( - - - - {session.id} - - - - - {session.issueLabel || session.issueTitle ? ( - - {session.issueLabel ? `${session.issueLabel}: ` : ""} - {session.issueTitle ?? ""} - - ) : null} - - {session.summary && !session.summaryIsFallback ? ( - - {session.summary} - - ) : null} - - - {session.branch ? ( - - {session.branch} - - ) : null} - {time} - - - {session.pr ? ( - - PR #{session.pr.number} - {session.pr.ciStatus !== "none" && ( - - {session.pr.ciStatus.toUpperCase()} - - )} - - ) : null} - - ); -} - -const styles = StyleSheet.create({ - card: { - backgroundColor: "#161b22", - borderLeftWidth: 3, - borderRadius: 8, - padding: 14, - marginHorizontal: 12, - marginVertical: 5, - }, - header: { - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - marginBottom: 6, - }, - id: { - color: "#8b949e", - fontSize: 12, - fontFamily: "monospace", - flex: 1, - marginRight: 8, - }, - issue: { - color: "#e6edf3", - fontSize: 14, - fontWeight: "600", - marginBottom: 4, - }, - summary: { - color: "#8b949e", - fontSize: 13, - lineHeight: 18, - marginBottom: 6, - }, - footer: { - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - }, - branch: { - color: "#58a6ff", - fontSize: 12, - fontFamily: "monospace", - flex: 1, - }, - time: { - color: "#8b949e", - fontSize: 12, - }, - prRow: { - flexDirection: "row", - alignItems: "center", - marginTop: 6, - gap: 8, - }, - prLabel: { - color: "#3fb950", - fontSize: 12, - fontWeight: "600", - }, - ciStatus: { - fontSize: 11, - fontWeight: "700", - letterSpacing: 0.5, - }, -}); diff --git a/packages/mobile/src/components/StatBar.tsx b/packages/mobile/src/components/StatBar.tsx deleted file mode 100644 index d237598fb..000000000 --- a/packages/mobile/src/components/StatBar.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from "react"; -import { View, Text, StyleSheet } from "react-native"; -import type { DashboardStats } from "../types"; - -interface Props { - stats: DashboardStats; -} - -export default function StatBar({ stats }: Props) { - return ( - - - - - - - ); -} - -function StatItem({ label, value, color }: { label: string; value: number; color: string }) { - return ( - - {value} - {label} - - ); -} - -const styles = StyleSheet.create({ - container: { - flexDirection: "row", - justifyContent: "space-around", - backgroundColor: "#161b22", - paddingVertical: 12, - paddingHorizontal: 16, - borderBottomWidth: 1, - borderBottomColor: "#30363d", - }, - item: { - alignItems: "center", - }, - value: { - fontSize: 20, - fontWeight: "700", - }, - label: { - fontSize: 11, - color: "#8b949e", - marginTop: 2, - }, -}); diff --git a/packages/mobile/src/context/BackendContext.tsx b/packages/mobile/src/context/BackendContext.tsx deleted file mode 100644 index 2ec784949..000000000 --- a/packages/mobile/src/context/BackendContext.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import React, { createContext, useContext, useState, useCallback, useEffect } from "react"; -import AsyncStorage from "@react-native-async-storage/async-storage"; -import type { DashboardSession, SessionsResponse } from "../types"; - -const STORAGE_KEY = "@ao_backend_url"; -const TERMINAL_WS_OVERRIDE_KEY = "@ao_terminal_ws_url"; -const DEFAULT_URL = "http://192.168.1.1:3000"; - -interface BackendContextValue { - backendUrl: string; - setBackendUrl: (url: string) => Promise; - /** WebSocket URL for the terminal server. Auto-derived unless manually overridden. */ - terminalWsUrl: string; - /** Manual override for terminal WS URL (for ngrok / different host). Empty = auto-derive. */ - terminalWsOverride: string; - setTerminalWsOverride: (url: string) => Promise; - fetchSessions: () => Promise; - fetchSession: (id: string) => Promise; - sendMessage: (id: string, message: string) => Promise; - killSession: (id: string) => Promise; - restoreSession: (id: string) => Promise; - mergePR: (prNumber: number) => Promise; - spawnSession: (projectId: string, issueId?: string) => Promise; -} - -const BackendContext = createContext(null); - -function deriveTerminalWsUrl(backendUrl: string): string { - try { - const url = new URL(backendUrl); - return `ws://${url.hostname}:14801`; - } catch { - return "ws://192.168.1.1:14801"; - } -} - -/** Convert an ngrok https URL to a wss URL for WebSocket connections */ -function normalizeWsUrl(url: string): string { - return url.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://"); -} - -export function BackendProvider({ children }: { children: React.ReactNode }) { - const [backendUrl, setBackendUrlState] = useState(DEFAULT_URL); - const [terminalWsOverride, setTerminalWsOverrideState] = useState(""); - - useEffect(() => { - Promise.all([ - AsyncStorage.getItem(STORAGE_KEY), - AsyncStorage.getItem(TERMINAL_WS_OVERRIDE_KEY), - ]).then(([storedBackend, storedWs]) => { - if (storedBackend) setBackendUrlState(storedBackend); - if (storedWs) setTerminalWsOverrideState(storedWs); - }); - }, []); - - const setBackendUrl = useCallback(async (url: string) => { - const trimmed = url.trim().replace(/\/$/, ""); - setBackendUrlState(trimmed); - await AsyncStorage.setItem(STORAGE_KEY, trimmed); - }, []); - - const setTerminalWsOverride = useCallback(async (url: string) => { - const trimmed = url.trim().replace(/\/$/, ""); - setTerminalWsOverrideState(trimmed); - await AsyncStorage.setItem(TERMINAL_WS_OVERRIDE_KEY, trimmed); - }, []); - - // Use override if set, otherwise derive from backendUrl - const terminalWsUrl = terminalWsOverride - ? normalizeWsUrl(terminalWsOverride) - : deriveTerminalWsUrl(backendUrl); - - const apiFetch = useCallback( - async (path: string, options?: RequestInit) => { - const url = `${backendUrl}${path}`; - const res = await fetch(url, { - ...options, - headers: { "Content-Type": "application/json", ...(options?.headers ?? {}) }, - }); - if (!res.ok) { - const text = await res.text().catch(() => res.statusText); - throw new Error(`${res.status}: ${text}`); - } - return res; - }, - [backendUrl], - ); - - const fetchSessions = useCallback(async (): Promise => { - const res = await apiFetch("/api/sessions"); - return res.json() as Promise; - }, [apiFetch]); - - const fetchSession = useCallback(async (id: string): Promise => { - const res = await apiFetch(`/api/sessions/${encodeURIComponent(id)}`); - return res.json() as Promise; - }, [apiFetch]); - - const sendMessage = useCallback(async (id: string, message: string): Promise => { - await apiFetch(`/api/sessions/${encodeURIComponent(id)}/message`, { - method: "POST", - body: JSON.stringify({ message }), - }); - }, [apiFetch]); - - const killSession = useCallback(async (id: string): Promise => { - await apiFetch(`/api/sessions/${encodeURIComponent(id)}/kill`, { method: "POST" }); - }, [apiFetch]); - - const restoreSession = useCallback(async (id: string): Promise => { - await apiFetch(`/api/sessions/${encodeURIComponent(id)}/restore`, { method: "POST" }); - }, [apiFetch]); - - const mergePR = useCallback(async (prNumber: number): Promise => { - await apiFetch(`/api/prs/${prNumber}/merge`, { method: "POST" }); - }, [apiFetch]); - - const spawnSession = useCallback(async (projectId: string, issueId?: string): Promise => { - const res = await apiFetch("/api/spawn", { - method: "POST", - body: JSON.stringify({ projectId, ...(issueId ? { issueId } : {}) }), - }); - const data = (await res.json()) as { session: DashboardSession }; - return data.session; - }, [apiFetch]); - - return ( - - {children} - - ); -} - -export function useBackend(): BackendContextValue { - const ctx = useContext(BackendContext); - if (!ctx) throw new Error("useBackend must be used inside BackendProvider"); - return ctx; -} diff --git a/packages/mobile/src/hooks/useSession.ts b/packages/mobile/src/hooks/useSession.ts deleted file mode 100644 index 7bdf901ca..000000000 --- a/packages/mobile/src/hooks/useSession.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { useState, useEffect, useRef, useCallback } from "react"; -import { AppState, type AppStateStatus } from "react-native"; -import { useBackend } from "../context/BackendContext"; -import type { DashboardSession } from "../types"; - -const POLL_INTERVAL = 5_000; - -interface UseSessionOptions { - /** Set to false to disable polling. Defaults to true. */ - enabled?: boolean; -} - -interface UseSessionResult { - session: DashboardSession | null; - loading: boolean; - error: string | null; - refresh: () => void; -} - -export function useSession(id: string, options?: UseSessionOptions): UseSessionResult { - const enabled = options?.enabled ?? true; - const { fetchSession } = useBackend(); - const [session, setSession] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - const intervalRef = useRef | null>(null); - // Generation counter — incremented on cleanup to invalidate in-flight fetches - // from a previous effect run (e.g. when backend URL changes). - const fetchGenRef = useRef(0); - - const doFetch = useCallback(async () => { - const gen = fetchGenRef.current; - try { - const data = await fetchSession(id); - if (gen !== fetchGenRef.current) return; - setSession(data); - setError(null); - } catch (err) { - if (gen !== fetchGenRef.current) return; - setError(err instanceof Error ? err.message : "Failed to load session"); - } finally { - if (gen === fetchGenRef.current) setLoading(false); - } - }, [fetchSession, id]); - - const startPolling = useCallback(() => { - doFetch(); - intervalRef.current = setInterval(doFetch, POLL_INTERVAL); - }, [doFetch]); - - const stopPolling = useCallback(() => { - if (intervalRef.current) { - clearInterval(intervalRef.current); - intervalRef.current = null; - } - }, []); - - useEffect(() => { - if (!enabled) { - setSession(null); - setLoading(false); - return; - } - - startPolling(); - - const handleAppState = (nextState: AppStateStatus) => { - if (nextState === "active") { - stopPolling(); - startPolling(); - } else { - stopPolling(); - } - }; - - const sub = AppState.addEventListener("change", handleAppState); - - return () => { - fetchGenRef.current++; // Invalidate in-flight fetches from this effect run - stopPolling(); - sub.remove(); - }; - }, [enabled, startPolling, stopPolling]); - - const refresh = useCallback(() => { - if (!enabled) return; - setLoading(true); - doFetch(); - }, [enabled, doFetch]); - - return { session, loading, error, refresh }; -} diff --git a/packages/mobile/src/hooks/useSessionNotifications.ts b/packages/mobile/src/hooks/useSessionNotifications.ts deleted file mode 100644 index 29aea2956..000000000 --- a/packages/mobile/src/hooks/useSessionNotifications.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { useEffect, useRef } from "react"; -import { AppState } from "react-native"; -import AsyncStorage from "@react-native-async-storage/async-storage"; -import { getAttentionLevel, type AttentionLevel, type DashboardSession } from "../types"; -import { scheduleNotification } from "../notifications"; -import { NOTIFY_STATE_KEY } from "../notifications/backgroundTask"; - -/** - * Minimum time (ms) before re-notifying for the same session. - * Prevents spam when an agent oscillates between working <-> waiting_input. - */ -const COOLDOWN_MS = 10 * 60 * 1000; // 10 minutes - -export function useSessionNotifications(sessions: DashboardSession[]): void { - const prevLevels = useRef>({}); - // Tracks when we last sent a notification per session - const lastNotifiedAt = useRef>({}); - // Skip the very first render — don't notify for state that existed before app opened - const isFirstRender = useRef(true); - - useEffect(() => { - if (sessions.length === 0) return; - - const nextLevels: Record = {}; - const now = Date.now(); - // Only notify when app is in background — no point interrupting the user - // if they're actively looking at the session list - const isBackground = AppState.currentState !== "active"; - - for (const session of sessions) { - const level = getAttentionLevel(session); - nextLevels[session.id] = level; - - if (!isFirstRender.current) { - const prev = prevLevels.current[session.id]; - const lastNotified = lastNotifiedAt.current[session.id] ?? 0; - const cooldownExpired = now - lastNotified > COOLDOWN_MS; - - if (level === "respond" && (prev !== "respond" || cooldownExpired)) { - if (isBackground || prev !== "respond") { - void scheduleNotification(session, "respond").catch(() => {}); - lastNotifiedAt.current[session.id] = now; - } - } else if (level === "merge" && (prev !== "merge" || cooldownExpired)) { - if (isBackground || prev !== "merge") { - void scheduleNotification(session, "merge").catch(() => {}); - lastNotifiedAt.current[session.id] = now; - } - } else if (level === "review" && (prev !== "review" || cooldownExpired)) { - if (isBackground || prev !== "review") { - void scheduleNotification(session, "review").catch(() => {}); - lastNotifiedAt.current[session.id] = now; - } - } - } - } - - prevLevels.current = nextLevels; - isFirstRender.current = false; - - void AsyncStorage.setItem(NOTIFY_STATE_KEY, JSON.stringify(nextLevels)); - }, [sessions]); -} diff --git a/packages/mobile/src/hooks/useSessions.ts b/packages/mobile/src/hooks/useSessions.ts deleted file mode 100644 index e7891ecdc..000000000 --- a/packages/mobile/src/hooks/useSessions.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { useState, useEffect, useRef, useCallback } from "react"; -import { AppState, type AppStateStatus } from "react-native"; -import { useBackend } from "../context/BackendContext"; -import type { DashboardSession, DashboardStats } from "../types"; - -const POLL_INTERVAL = 5_000; - -interface UseSessionsResult { - sessions: DashboardSession[]; - stats: DashboardStats | null; - orchestratorId: string | null; - loading: boolean; - error: string | null; - refresh: () => void; -} - -export function useSessions(): UseSessionsResult { - const { fetchSessions } = useBackend(); - const [sessions, setSessions] = useState([]); - const [stats, setStats] = useState(null); - const [orchestratorId, setOrchestratorId] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - const intervalRef = useRef | null>(null); - // Generation counter — incremented on cleanup to invalidate in-flight fetches - // from a previous effect run (e.g. when backend URL changes). - const fetchGenRef = useRef(0); - - const doFetch = useCallback(async () => { - const gen = fetchGenRef.current; - try { - const data = await fetchSessions(); - if (gen !== fetchGenRef.current) return; - setSessions(data.sessions ?? []); - setStats(data.stats ?? null); - setOrchestratorId(data.orchestratorId ?? null); - setError(null); - } catch (err) { - if (gen !== fetchGenRef.current) return; - setError(err instanceof Error ? err.message : "Failed to load sessions"); - } finally { - if (gen === fetchGenRef.current) setLoading(false); - } - }, [fetchSessions]); - - const startPolling = useCallback(() => { - doFetch(); - intervalRef.current = setInterval(doFetch, POLL_INTERVAL); - }, [doFetch]); - - const stopPolling = useCallback(() => { - if (intervalRef.current) { - clearInterval(intervalRef.current); - intervalRef.current = null; - } - }, []); - - useEffect(() => { - startPolling(); - - const handleAppState = (nextState: AppStateStatus) => { - if (nextState === "active") { - // Resumed — refresh immediately and restart polling - stopPolling(); - startPolling(); - } else { - // Backgrounded — stop polling to save battery - stopPolling(); - } - }; - - const sub = AppState.addEventListener("change", handleAppState); - - return () => { - fetchGenRef.current++; // Invalidate in-flight fetches from this effect run - stopPolling(); - sub.remove(); - }; - }, [startPolling, stopPolling]); - - const refresh = useCallback(() => { - setLoading(true); - doFetch(); - }, [doFetch]); - - return { sessions, stats, orchestratorId, loading, error, refresh }; -} diff --git a/packages/mobile/src/navigation/RootNavigator.tsx b/packages/mobile/src/navigation/RootNavigator.tsx deleted file mode 100644 index 9939907e4..000000000 --- a/packages/mobile/src/navigation/RootNavigator.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React from "react"; -import { NavigationContainer, DarkTheme, createNavigationContainerRef } from "@react-navigation/native"; -import { createNativeStackNavigator } from "@react-navigation/native-stack"; -import HomeScreen from "../screens/HomeScreen"; -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"; -import CommandsScreen from "../screens/CommandsScreen"; - -export type RootStackParamList = { - Home: undefined; - SessionDetail: { sessionId: string }; - Terminal: { sessionId: string; terminalWsUrl: string }; - Settings: undefined; - SpawnSession: undefined; - Orchestrator: undefined; - Commands: undefined; -}; - -export const navigationRef = createNavigationContainerRef(); - -const Stack = createNativeStackNavigator(); - -const AoDarkTheme = { - ...DarkTheme, - colors: { - ...DarkTheme.colors, - background: "#0d1117", - card: "#161b22", - text: "#e6edf3", - border: "#30363d", - primary: "#58a6ff", - }, -}; - -export default function RootNavigator() { - return ( - - - - - - - - - - - - ); -} diff --git a/packages/mobile/src/notifications/backgroundTask.ts b/packages/mobile/src/notifications/backgroundTask.ts deleted file mode 100644 index 2e2132e60..000000000 --- a/packages/mobile/src/notifications/backgroundTask.ts +++ /dev/null @@ -1,83 +0,0 @@ -import * as TaskManager from "expo-task-manager"; -import * as BackgroundTask from "expo-background-task"; -import AsyncStorage from "@react-native-async-storage/async-storage"; -import { getAttentionLevel, type AttentionLevel, type DashboardSession, type SessionsResponse } from "../types"; -import { scheduleNotification } from "./index"; - -export const TASK_ID = "ao-session-check"; -const BACKEND_URL_KEY = "@ao_backend_url"; -export const NOTIFY_STATE_KEY = "@ao_notify_state"; -const NOTIFY_TIMESTAMPS_KEY = "@ao_notify_timestamps"; -const COOLDOWN_MS = 10 * 60 * 1000; // 10 minutes - -/** Define the background task globally — must be at module top level. */ -TaskManager.defineTask(TASK_ID, async () => { - try { - const backendUrl = await AsyncStorage.getItem(BACKEND_URL_KEY); - if (!backendUrl) return BackgroundTask.BackgroundTaskResult.Success; - - const res = await fetch(`${backendUrl}/api/sessions`, { - headers: { "Content-Type": "application/json" }, - }); - if (!res.ok) return BackgroundTask.BackgroundTaskResult.Failed; - - const data = (await res.json()) as SessionsResponse; - const sessions: DashboardSession[] = data.sessions ?? []; - - const [prevRaw, tsRaw] = await Promise.all([ - AsyncStorage.getItem(NOTIFY_STATE_KEY), - AsyncStorage.getItem(NOTIFY_TIMESTAMPS_KEY), - ]); - const prevState: Record = prevRaw - ? (JSON.parse(prevRaw) as Record) - : {}; - const timestamps: Record = tsRaw - ? (JSON.parse(tsRaw) as Record) - : {}; - - const nextState: Record = {}; - const now = Date.now(); - - for (const session of sessions) { - const level = getAttentionLevel(session); - nextState[session.id] = level; - - const prev = prevState[session.id]; - const lastNotified = timestamps[session.id] ?? 0; - const cooldownExpired = now - lastNotified > COOLDOWN_MS; - - if (level === "respond" && (prev !== "respond" || cooldownExpired)) { - await scheduleNotification(session, "respond"); - timestamps[session.id] = now; - } else if (level === "merge" && (prev !== "merge" || cooldownExpired)) { - await scheduleNotification(session, "merge"); - timestamps[session.id] = now; - } else if (level === "review" && (prev !== "review" || cooldownExpired)) { - await scheduleNotification(session, "review"); - timestamps[session.id] = now; - } - } - - await Promise.all([ - AsyncStorage.setItem(NOTIFY_STATE_KEY, JSON.stringify(nextState)), - AsyncStorage.setItem(NOTIFY_TIMESTAMPS_KEY, JSON.stringify(timestamps)), - ]); - return BackgroundTask.BackgroundTaskResult.Success; - } catch { - return BackgroundTask.BackgroundTaskResult.Failed; - } -}); - -/** Register the background task. Safe to call multiple times. */ -export async function registerBackgroundTask(): Promise { - try { - const isRegistered = await TaskManager.isTaskRegisteredAsync(TASK_ID); - if (isRegistered) return; - - await BackgroundTask.registerTaskAsync(TASK_ID, { - minimumInterval: 15 * 60, // 15 minutes (OS minimum) - }); - } catch { - // Background tasks are not supported in Expo Go — ignore silently - } -} diff --git a/packages/mobile/src/notifications/index.ts b/packages/mobile/src/notifications/index.ts deleted file mode 100644 index 9808399ad..000000000 --- a/packages/mobile/src/notifications/index.ts +++ /dev/null @@ -1,86 +0,0 @@ -import * as Notifications from "expo-notifications"; -import { Platform } from "react-native"; -import type { DashboardSession } from "../types"; - -const ANDROID_CHANNEL_ID = "ao-respond"; - -/** Configure how notifications are presented when the app is in foreground */ -Notifications.setNotificationHandler({ - handleNotification: async () => ({ - shouldShowAlert: true, - shouldPlaySound: true, - shouldSetBadge: false, - shouldShowBanner: true, - shouldShowList: true, - }), -}); - -/** Create Android channel + request permission. Call once on app startup. */ -export async function setupNotifications(): Promise { - // Create Android notification channel - if (Platform.OS === "android") { - await Notifications.setNotificationChannelAsync(ANDROID_CHANNEL_ID, { - name: "Agent Input Required", - importance: Notifications.AndroidImportance.HIGH, - vibrationPattern: [0, 250, 250, 250], - lightColor: "#f85149", - }); - } - - const { status } = await Notifications.requestPermissionsAsync({ - ios: { - allowAlert: true, - allowBadge: false, - allowSound: true, - }, - }); - - return status === "granted"; -} - -/** Fire an immediate local notification for a session attention transition. */ -export async function scheduleNotification( - session: DashboardSession, - level: "respond" | "merge" | "review", -): Promise { - const sessionLabel = - session.issueLabel ?? - session.id; - - const body = - session.issueTitle ?? - (session.summary && !session.summaryIsFallback ? session.summary : null) ?? - session.activity ?? - session.status; - - const titles: Record = { - respond: "Agent needs your input", - merge: "PR ready to merge", - review: "Session needs review", - }; - - const bodies: Record = { - respond: `${sessionLabel}: ${body}`, - merge: `${sessionLabel}${session.pr ? `: PR #${session.pr.number}` : ""}`, - review: `${sessionLabel}: ${session.pr?.ciStatus === "failing" ? "CI failing" : session.pr?.reviewDecision === "changes_requested" ? "Changes requested" : body}`, - }; - - const content: Notifications.NotificationContentInput = { - title: titles[level], - body: bodies[level], - data: { sessionId: session.id }, - sound: true, - ...(Platform.OS === "android" && { channelId: ANDROID_CHANNEL_ID }), - }; - - // Use timeInterval trigger instead of null — trigger: null fails silently on Android in background tasks. - // SDK 53 requires explicit `type` field on trigger objects. - await Notifications.scheduleNotificationAsync({ - content, - trigger: { - type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL, - seconds: 1, - channelId: Platform.OS === "android" ? ANDROID_CHANNEL_ID : undefined, - }, - }); -} diff --git a/packages/mobile/src/screens/CommandsScreen.tsx b/packages/mobile/src/screens/CommandsScreen.tsx deleted file mode 100644 index b584c78d6..000000000 --- a/packages/mobile/src/screens/CommandsScreen.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React from "react"; -import { View, Text, StyleSheet, ScrollView } from "react-native"; - -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" }, -]; - -export default function CommandsScreen() { - return ( - - - Quick reference for managing sessions from terminal. - {CLI_COMMANDS.map((c, i) => ( - - {c.cmd} - {c.desc} - - ))} - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: "#0d1117", - }, - content: { - padding: 14, - paddingBottom: 32, - }, - section: { - backgroundColor: "#161b22", - borderRadius: 10, - padding: 16, - }, - hint: { - color: "#8b949e", - fontSize: 13, - lineHeight: 18, - marginBottom: 12, - }, - 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, - }, -}); diff --git a/packages/mobile/src/screens/HomeScreen.tsx b/packages/mobile/src/screens/HomeScreen.tsx deleted file mode 100644 index d1015d14f..000000000 --- a/packages/mobile/src/screens/HomeScreen.tsx +++ /dev/null @@ -1,176 +0,0 @@ -import React from "react"; -import { - View, - Text, - FlatList, - StyleSheet, - RefreshControl, - 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 { useSessionNotifications } from "../hooks/useSessionNotifications"; -import SessionCard from "../components/SessionCard"; -import StatBar from "../components/StatBar"; -import { getAttentionLevel, type DashboardSession } from "../types"; - -type Props = NativeStackScreenProps; - -const ATTENTION_ORDER = ["respond", "merge", "review", "pending", "working", "done"] as const; - -function sortSessions(sessions: DashboardSession[]): DashboardSession[] { - return [...sessions].sort((a, b) => { - const la = getAttentionLevel(a); - const lb = getAttentionLevel(b); - const ia = ATTENTION_ORDER.indexOf(la); - const ib = ATTENTION_ORDER.indexOf(lb); - if (ia !== ib) return ia - ib; - return new Date(b.lastActivityAt).getTime() - new Date(a.lastActivityAt).getTime(); - }); -} - -export default function HomeScreen({ navigation }: Props) { - const { sessions, stats, loading, error, refresh } = useSessions(); - useSessionNotifications(sessions); - - React.useLayoutEffect(() => { - navigation.setOptions({ - headerRight: () => ( - - navigation.navigate("SpawnSession")} - style={{ flexDirection: "row", alignItems: "center", gap: 2 }} - > - + - Session - - navigation.navigate("Orchestrator")}> - {"\uD83E\uDD16"} - - navigation.navigate("Settings")} - style={{ paddingRight: 4 }} - > - {"\u2699\uFE0F"} - - - ), - }); - }, [navigation]); - - const sorted = sortSessions(sessions); - - if (loading && sessions.length === 0) { - return ( - - - Connecting... - - ); - } - - if (error && sessions.length === 0) { - return ( - - {error} - - Retry - - navigation.navigate("Settings")} - > - Configure Backend URL - - - ); - } - - return ( - - {stats && } - item.id} - renderItem={({ item }) => ( - navigation.navigate("SessionDetail", { sessionId: item.id })} - /> - )} - refreshControl={ - - } - contentContainerStyle={ - sorted.length === 0 ? styles.emptyContainer : styles.listContent - } - ListEmptyComponent={ - - No sessions - Sessions will appear here when agents are running - - } - /> - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: "#0d1117", - }, - center: { - flex: 1, - alignItems: "center", - justifyContent: "center", - padding: 24, - }, - emptyContainer: { - flex: 1, - }, - listContent: { - paddingVertical: 8, - }, - loadingText: { - color: "#8b949e", - marginTop: 12, - fontSize: 14, - }, - 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, - }, - emptyText: { - color: "#e6edf3", - fontSize: 16, - fontWeight: "600", - marginBottom: 8, - }, - emptySubtext: { - color: "#8b949e", - fontSize: 13, - textAlign: "center", - }, -}); diff --git a/packages/mobile/src/screens/OrchestratorScreen.tsx b/packages/mobile/src/screens/OrchestratorScreen.tsx deleted file mode 100644 index 8d386a221..000000000 --- a/packages/mobile/src/screens/OrchestratorScreen.tsx +++ /dev/null @@ -1,370 +0,0 @@ -import React, { useState, useCallback } from "react"; -import { - View, - Text, - StyleSheet, - ScrollView, - TouchableOpacity, - ActivityIndicator, - TextInput, - Alert, -} from "react-native"; -import type { NativeStackScreenProps } from "@react-navigation/native-stack"; -import type { RootStackParamList } from "../navigation/RootNavigator"; -import { useSessions } from "../hooks/useSessions"; -import { useSession } from "../hooks/useSession"; -import { useBackend } from "../context/BackendContext"; -import AttentionBadge from "../components/AttentionBadge"; -import { - getAttentionLevel, - relativeTime, - ATTENTION_COLORS, - type DashboardSession, -} from "../types"; - -type Props = NativeStackScreenProps; - -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, orchestratorId, loading, error, refresh } = useSessions(); - const { sendMessage, terminalWsUrl } = useBackend(); - const { session: orchSession } = useSession(orchestratorId ?? "", { enabled: !!orchestratorId }); - const [message, setMessage] = useState(""); - const [sending, setSending] = useState(false); - - React.useLayoutEffect(() => { - navigation.setOptions({ - headerRight: () => ( - navigation.navigate("Commands")}> - Commands - - ), - }); - }, [navigation]); - - const zones = getZoneCounts(sessions); - - const handleSend = useCallback(async () => { - if (!message.trim() || !orchestratorId) return; - setSending(true); - try { - await sendMessage(orchestratorId, message.trim()); - setMessage(""); - } catch (err) { - Alert.alert("Error", err instanceof Error ? err.message : "Failed to send message"); - } finally { - setSending(false); - } - }, [message, sendMessage, orchestratorId]); - - if (loading && sessions.length === 0) { - return ( - - - - ); - } - - if (error && sessions.length === 0) { - return ( - - {error} - - Retry - - - ); - } - - const orchLevel = orchSession ? getAttentionLevel(orchSession) : null; - const orchColor = orchLevel ? ATTENTION_COLORS[orchLevel] : "#8b949e"; - - return ( - - {/* Orchestrator Session Details */} - - Orchestrator - {orchestratorId && orchSession ? ( - - - - - Running - - - - {orchestratorId} - - {orchSession.status} - {orchSession.activity ? ` · ${orchSession.activity}` : ""} - - {orchSession.summary && !orchSession.summaryIsFallback && ( - {orchSession.summary} - )} - - Last activity: {relativeTime(orchSession.lastActivityAt)} - - - {/* Actions */} - - navigation.navigate("Terminal", { sessionId: orchestratorId, terminalWsUrl })} - > - Open Terminal - - - - {/* Send message */} - - - - {sending ? ( - - ) : ( - Send - )} - - - - ) : ( - - - - Not running - - Start with: ao start <project> - - )} - - - {/* Zone Overview */} - - Session Zones - - - - - - - - - - - - ); -} - -function ZoneBadge({ label, count, color }: { label: string; count: number; color: string }) { - return ( - - {count} - {label} - - ); -} - -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, - }, - // Orchestrator detail card - orchDetailCard: { - backgroundColor: "#0d1117", - borderWidth: 1, - borderColor: "#30363d", - borderLeftWidth: 3, - borderRadius: 8, - padding: 14, - }, - orchHeaderRow: { - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - marginBottom: 6, - }, - orchStatusRow: { - flexDirection: "row", - alignItems: "center", - gap: 8, - }, - dot: { - width: 10, - height: 10, - borderRadius: 5, - }, - orchRunning: { - color: "#3fb950", - fontSize: 15, - fontWeight: "600", - }, - orchId: { - color: "#8b949e", - fontSize: 12, - fontFamily: "monospace", - marginBottom: 4, - }, - orchStatus: { - color: "#8b949e", - fontSize: 13, - marginBottom: 4, - }, - orchSummary: { - color: "#e6edf3", - fontSize: 13, - lineHeight: 18, - marginBottom: 4, - }, - orchTimingRow: { - marginBottom: 10, - }, - orchTiming: { - color: "#6e7681", - fontSize: 12, - }, - orchHint: { - color: "#6e7681", - fontSize: 12, - marginTop: 4, - }, - orchActions: { - marginBottom: 10, - }, - terminalButton: { - backgroundColor: "#21262d", - borderWidth: 1, - borderColor: "#30363d", - borderRadius: 8, - padding: 12, - alignItems: "center", - }, - terminalButtonText: { - color: "#e6edf3", - fontSize: 14, - fontWeight: "600", - }, - orchMessageRow: { - flexDirection: "row", - alignItems: "center", - gap: 8, - }, - orchMessageInput: { - flex: 1, - backgroundColor: "#161b22", - borderWidth: 1, - borderColor: "#30363d", - borderRadius: 8, - paddingHorizontal: 12, - paddingVertical: 8, - color: "#e6edf3", - fontSize: 14, - }, - sendButton: { - backgroundColor: "#238636", - borderRadius: 8, - paddingHorizontal: 14, - height: 38, - alignItems: "center", - justifyContent: "center", - }, - sendButtonDisabled: { - backgroundColor: "#21262d", - }, - sendButtonText: { - color: "#fff", - fontSize: 14, - fontWeight: "700", - }, - // 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, - }, - // 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, - }, -}); diff --git a/packages/mobile/src/screens/SessionDetailScreen.tsx b/packages/mobile/src/screens/SessionDetailScreen.tsx deleted file mode 100644 index c18e760b9..000000000 --- a/packages/mobile/src/screens/SessionDetailScreen.tsx +++ /dev/null @@ -1,729 +0,0 @@ -import React, { useState, useCallback, useRef, useEffect } from "react"; -import { - View, - Text, - StyleSheet, - ScrollView, - TextInput, - TouchableOpacity, - Alert, - ActivityIndicator, - Keyboard, - Platform, - Linking, -} from "react-native"; -import type { NativeStackScreenProps } from "@react-navigation/native-stack"; -import type { RootStackParamList } from "../navigation/RootNavigator"; -import { useSession } from "../hooks/useSession"; -import { useBackend } from "../context/BackendContext"; -import AttentionBadge from "../components/AttentionBadge"; -import { - getAttentionLevel, - relativeTime, - isRestorable, - isTerminal, - isPRRateLimited, - ATTENTION_COLORS, - type DashboardCICheck, - type DashboardUnresolvedComment, - type DashboardPR, -} from "../types"; - -type Props = NativeStackScreenProps; - -type ActionButtonState = "idle" | "sending" | "sent" | "error"; - -export default function SessionDetailScreen({ route, navigation }: Props) { - const { sessionId } = route.params; - const { session, loading, error, refresh } = useSession(sessionId); - const { sendMessage, killSession, restoreSession, mergePR, terminalWsUrl } = useBackend(); - const [message, setMessage] = useState(""); - const [sending, setSending] = useState(false); - const [merging, setMerging] = useState(false); - const [ciFixState, setCiFixState] = useState("idle"); - const [commentFixStates, setCommentFixStates] = useState>({}); - const scrollRef = useRef(null); - const [keyboardHeight, setKeyboardHeight] = useState(0); - - useEffect(() => { - const showEvent = Platform.OS === "ios" ? "keyboardWillShow" : "keyboardDidShow"; - const hideEvent = Platform.OS === "ios" ? "keyboardWillHide" : "keyboardDidHide"; - - const showSub = Keyboard.addListener(showEvent, (e) => { - setKeyboardHeight(e.endCoordinates.height); - }); - const hideSub = Keyboard.addListener(hideEvent, () => { - setKeyboardHeight(0); - }); - - return () => { - showSub.remove(); - hideSub.remove(); - }; - }, []); - - const handleSend = useCallback(async () => { - if (!message.trim()) return; - setSending(true); - try { - await sendMessage(sessionId, message.trim()); - setMessage(""); - } catch (err) { - Alert.alert("Error", err instanceof Error ? err.message : "Failed to send message"); - } finally { - setSending(false); - } - }, [message, sendMessage, sessionId]); - - const handleKill = useCallback(() => { - Alert.alert( - "Kill Session", - "This will terminate the agent process. Are you sure?", - [ - { text: "Cancel", style: "cancel" }, - { - text: "Kill", - style: "destructive", - onPress: async () => { - try { - await killSession(sessionId); - refresh(); - } catch (err) { - Alert.alert("Error", err instanceof Error ? err.message : "Failed to kill session"); - } - }, - }, - ], - ); - }, [killSession, sessionId, refresh]); - - const handleRestore = useCallback(async () => { - try { - await restoreSession(sessionId); - refresh(); - } catch (err) { - Alert.alert("Error", err instanceof Error ? err.message : "Failed to restore session"); - } - }, [restoreSession, sessionId, refresh]); - - const handleOpenTerminal = useCallback(() => { - navigation.navigate("Terminal", { sessionId, terminalWsUrl }); - }, [navigation, sessionId, terminalWsUrl]); - - const handleMergePR = useCallback(async (prNumber: number) => { - setMerging(true); - try { - await mergePR(prNumber); - Alert.alert("Merged", `PR #${prNumber} merged successfully.`); - refresh(); - } catch (err) { - Alert.alert("Merge failed", err instanceof Error ? err.message : "Failed to merge PR"); - } finally { - setMerging(false); - } - }, [mergePR, refresh]); - - const handleAskFixCI = useCallback(async (pr: DashboardPR) => { - setCiFixState("sending"); - try { - await sendMessage(sessionId, `Please fix the failing CI checks on ${pr.url}`); - setCiFixState("sent"); - setTimeout(() => setCiFixState("idle"), 3000); - } catch { - setCiFixState("error"); - setTimeout(() => setCiFixState("idle"), 3000); - } - }, [sendMessage, sessionId]); - - const handleAskFixComment = useCallback(async (comment: DashboardUnresolvedComment) => { - const key = comment.url; - setCommentFixStates((prev) => ({ ...prev, [key]: "sending" })); - try { - const msg = `Please address this review comment:\n\nFile: ${comment.path}\nComment: ${comment.body}\n\nComment URL: ${comment.url}\n\nAfter fixing, mark the comment as resolved at ${comment.url}`; - await sendMessage(sessionId, msg); - setCommentFixStates((prev) => ({ ...prev, [key]: "sent" })); - setTimeout(() => setCommentFixStates((prev) => ({ ...prev, [key]: "idle" })), 3000); - } catch { - setCommentFixStates((prev) => ({ ...prev, [key]: "error" })); - setTimeout(() => setCommentFixStates((prev) => ({ ...prev, [key]: "idle" })), 3000); - } - }, [sendMessage, sessionId]); - - if (loading && !session) { - return ( - - - - ); - } - - if (error && !session) { - return ( - - {error} - - Retry - - - ); - } - - if (!session) return null; - - const level = getAttentionLevel(session); - const color = ATTENTION_COLORS[level]; - const canRestore = isRestorable(session); - const isDone = isTerminal(session); - const pr = session.pr; - const isReadyToMerge = pr?.mergeability.mergeable && pr.state === "open" && !isPRRateLimited(pr); - const failedChecks = pr?.ciChecks.filter((c) => c.status === "failed") ?? []; - const unresolvedComments = pr?.unresolvedComments ?? []; - - return ( - - - {/* Header row */} - - - - {session.id} - - - - - {session.status} - {session.activity ? ` · ${session.activity}` : ""} - - - - {/* Alerts */} - {pr && pr.ciStatus === "failing" && failedChecks.length > 0 && ( - - - - {failedChecks.length} CI check{failedChecks.length > 1 ? "s" : ""} failing - - handleAskFixCI(pr)} - /> - - - )} - - {pr && !pr.mergeability.noConflicts && ( - - Merge conflict - - )} - - {pr && pr.reviewDecision === "changes_requested" && ( - - Changes requested - - )} - - {/* Issue */} - {(session.issueLabel || session.issueTitle) && ( -
- - {session.issueLabel ? `${session.issueLabel}: ` : ""} - {session.issueTitle ?? ""} - -
- )} - - {/* Summary */} - {session.summary && !session.summaryIsFallback && ( -
- {session.summary} -
- )} - - {/* Branch */} - {session.branch && ( -
- {session.branch} -
- )} - - {/* PR */} - {pr && ( -
- Linking.openURL(pr.url)}> - - {pr.title} - - - - - - - {pr.isDraft && } - {pr.mergeability.blockers.length > 0 && ( - - Blockers: - {pr.mergeability.blockers.map((b, i) => ( - - {b} - ))} - - )} -
- )} - - {/* CI Checks */} - {pr && pr.ciChecks.length > 0 && ( -
- {pr.ciChecks.map((check, i) => ( - - ))} -
- )} - - {/* Unresolved Comments */} - {unresolvedComments.length > 0 && ( -
- {unresolvedComments.map((comment, i) => ( - - - {comment.author} - {comment.path} - - {comment.body} - - handleAskFixComment(comment)} - /> - Linking.openURL(comment.url)}> - View - - - - ))} -
- )} - - {/* Timestamps */} -
- - -
- - {/* Actions */} - - {isReadyToMerge && ( - handleMergePR(pr.number)} - disabled={merging} - > - {merging ? ( - - ) : ( - - Merge PR #{pr.number} - - )} - - )} - - - Open Terminal - - - {canRestore && ( - - Restore Session - - )} - - {!isDone && ( - - Kill Session - - )} - -
- - {/* Message input — only show for active sessions */} - {!isDone && ( - 0 ? keyboardHeight + 48 : 10 }]}> - - - {sending ? ( - - ) : ( - Send - )} - - - )} -
- ); -} - -function Section({ title, children }: { title: string; children: React.ReactNode }) { - return ( - - {title} - {children} - - ); -} - -function InfoRow({ label, value, valueColor }: { label: string; value: string; valueColor?: string }) { - return ( - - {label} - {value} - - ); -} - -const CI_STATUS_ICONS: Record = { - passed: { icon: "\u2713", color: "#3fb950" }, - failed: { icon: "\u2717", color: "#f85149" }, - running: { icon: "\u25CF", color: "#e3b341" }, - pending: { icon: "\u25CF", color: "#8b949e" }, - skipped: { icon: "\u2014", color: "#6e7681" }, -}; - -function CICheckRow({ check }: { check: DashboardCICheck }) { - const info = CI_STATUS_ICONS[check.status] ?? CI_STATUS_ICONS.pending; - return ( - Linking.openURL(check.url!) : undefined} - disabled={!check.url} - > - {info.icon} - {check.name} - {check.status} - - ); -} - -function ActionButton({ state, label, onPress }: { state: ActionButtonState; label: string; onPress: () => void }) { - const isDisabled = state === "sending" || state === "sent"; - const bgColor = state === "sent" ? "#1f3a2a" : state === "error" ? "#3d1f20" : "#21262d"; - const borderColor = state === "sent" ? "#3fb950" : state === "error" ? "#f85149" : "#30363d"; - const textColor = state === "sent" ? "#3fb950" : state === "error" ? "#f85149" : "#58a6ff"; - const text = state === "sending" ? "Sending..." : state === "sent" ? "Sent" : state === "error" ? "Failed" : label; - - return ( - - {state === "sending" ? ( - - ) : ( - {text} - )} - - ); -} - -const styles = StyleSheet.create({ - root: { - flex: 1, - backgroundColor: "#0d1117", - }, - container: { - flex: 1, - }, - content: { - padding: 12, - paddingBottom: 24, - }, - center: { - flex: 1, - alignItems: "center", - justifyContent: "center", - padding: 24, - backgroundColor: "#0d1117", - }, - headerCard: { - backgroundColor: "#161b22", - borderLeftWidth: 3, - borderRadius: 8, - padding: 14, - marginBottom: 12, - }, - headerRow: { - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - marginBottom: 6, - }, - sessionId: { - color: "#8b949e", - fontSize: 13, - fontFamily: "monospace", - flex: 1, - marginRight: 8, - }, - status: { - color: "#8b949e", - fontSize: 13, - }, - // Alerts - alertCard: { - backgroundColor: "#3d1f20", - borderLeftWidth: 3, - borderLeftColor: "#f85149", - borderRadius: 8, - padding: 12, - marginBottom: 10, - }, - alertRow: { - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - }, - alertText: { - color: "#f85149", - fontSize: 13, - fontWeight: "600", - flex: 1, - }, - // Sections - section: { - backgroundColor: "#161b22", - borderRadius: 8, - padding: 14, - marginBottom: 10, - }, - sectionTitle: { - color: "#8b949e", - fontSize: 11, - fontWeight: "700", - letterSpacing: 0.8, - textTransform: "uppercase", - marginBottom: 8, - }, - bodyText: { - color: "#e6edf3", - fontSize: 14, - lineHeight: 20, - }, - issueText: { - color: "#e6edf3", - fontSize: 14, - fontWeight: "600", - }, - monoText: { - color: "#58a6ff", - fontSize: 13, - fontFamily: "monospace", - }, - infoRow: { - flexDirection: "row", - justifyContent: "space-between", - paddingVertical: 4, - }, - infoLabel: { - color: "#8b949e", - fontSize: 13, - }, - infoValue: { - color: "#e6edf3", - fontSize: 13, - fontWeight: "500", - }, - blockersLabel: { - color: "#f85149", - fontSize: 12, - fontWeight: "600", - marginBottom: 4, - }, - blockerText: { - color: "#f85149", - fontSize: 12, - marginLeft: 4, - marginBottom: 2, - }, - // CI Checks - ciCheckRow: { - flexDirection: "row", - alignItems: "center", - paddingVertical: 6, - gap: 8, - }, - ciCheckIcon: { - fontSize: 14, - fontWeight: "700", - width: 18, - textAlign: "center", - }, - ciCheckName: { - color: "#e6edf3", - fontSize: 13, - flex: 1, - }, - ciCheckStatus: { - fontSize: 11, - fontWeight: "600", - }, - // Unresolved Comments - commentCard: { - backgroundColor: "#0d1117", - borderRadius: 6, - padding: 10, - marginBottom: 8, - borderWidth: 1, - borderColor: "#30363d", - }, - commentHeader: { - flexDirection: "row", - justifyContent: "space-between", - marginBottom: 6, - }, - commentAuthor: { - color: "#e6edf3", - fontSize: 12, - fontWeight: "600", - }, - commentPath: { - color: "#8b949e", - fontSize: 11, - fontFamily: "monospace", - flex: 1, - textAlign: "right", - marginLeft: 8, - }, - commentBody: { - color: "#8b949e", - fontSize: 13, - lineHeight: 18, - marginBottom: 8, - }, - commentActions: { - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - }, - viewLink: { - color: "#58a6ff", - fontSize: 13, - fontWeight: "600", - }, - // Inline action button (for "Ask to fix", etc.) - inlineActionButton: { - borderWidth: 1, - borderRadius: 6, - paddingHorizontal: 10, - paddingVertical: 6, - minWidth: 80, - alignItems: "center", - }, - inlineActionText: { - fontSize: 12, - fontWeight: "600", - }, - // Main actions - actionsSection: { - gap: 8, - marginTop: 4, - }, - actionButton: { - borderRadius: 8, - padding: 14, - alignItems: "center", - borderWidth: 1, - }, - mergeButton: { - backgroundColor: "#238636", - borderColor: "#2ea043", - }, - terminalButton: { - backgroundColor: "#21262d", - borderColor: "#30363d", - }, - restoreButton: { - backgroundColor: "#1f3a2a", - borderColor: "#3fb950", - }, - killButton: { - backgroundColor: "#21262d", - borderColor: "#30363d", - }, - actionButtonText: { - color: "#e6edf3", - fontSize: 15, - fontWeight: "600", - }, - messageBar: { - flexDirection: "row", - alignItems: "flex-end", - padding: 10, - backgroundColor: "#161b22", - borderTopWidth: 1, - borderTopColor: "#30363d", - gap: 8, - }, - messageInput: { - flex: 1, - backgroundColor: "#0d1117", - borderWidth: 1, - borderColor: "#30363d", - borderRadius: 8, - paddingHorizontal: 12, - paddingVertical: 8, - color: "#e6edf3", - fontSize: 14, - maxHeight: 100, - }, - sendButton: { - backgroundColor: "#238636", - borderRadius: 8, - paddingHorizontal: 16, - height: 40, - alignItems: "center", - justifyContent: "center", - }, - sendButtonDisabled: { - backgroundColor: "#21262d", - }, - sendButtonText: { - color: "#fff", - fontSize: 14, - fontWeight: "700", - }, - button: { - backgroundColor: "#21262d", - borderWidth: 1, - borderColor: "#30363d", - borderRadius: 6, - paddingHorizontal: 16, - paddingVertical: 8, - }, - buttonText: { - color: "#e6edf3", - fontSize: 14, - }, - errorText: { - color: "#f85149", - fontSize: 14, - textAlign: "center", - marginBottom: 16, - }, -}); diff --git a/packages/mobile/src/screens/SettingsScreen.tsx b/packages/mobile/src/screens/SettingsScreen.tsx deleted file mode 100644 index c3c8ade36..000000000 --- a/packages/mobile/src/screens/SettingsScreen.tsx +++ /dev/null @@ -1,443 +0,0 @@ -import React, { useState } from "react"; -import { - View, - Text, - TextInput, - TouchableOpacity, - StyleSheet, - ScrollView, - Alert, - KeyboardAvoidingView, - Platform, -} from "react-native"; -import type { NativeStackScreenProps } from "@react-navigation/native-stack"; -import type { RootStackParamList } from "../navigation/RootNavigator"; -import { useBackend } from "../context/BackendContext"; -import { scheduleNotification } from "../notifications"; -import * as Notifications from "expo-notifications"; - -type Props = NativeStackScreenProps; - -export default function SettingsScreen({ navigation }: Props) { - const { backendUrl, setBackendUrl, terminalWsUrl, terminalWsOverride, setTerminalWsOverride } = useBackend(); - const [input, setInput] = useState(backendUrl); - const [wsInput, setWsInput] = useState(terminalWsOverride); - const [saving, setSaving] = useState(false); - - const handleTestRespondNotification = async () => { - const { status } = await Notifications.getPermissionsAsync(); - if (status !== "granted") { - Alert.alert("Permission denied", "Notification permission is not granted. Enable it in your phone's Settings app."); - return; - } - try { - await scheduleNotification( - { - id: "ao-test-session", - projectId: "test", - status: "needs_input", - activity: "waiting_input", - branch: "feat/test", - issueId: null, - issueUrl: null, - issueLabel: "TEST-1", - issueTitle: "Fix the flaky integration test", - summary: "Waiting for your decision on the approach", - summaryIsFallback: false, - createdAt: new Date().toISOString(), - lastActivityAt: new Date().toISOString(), - pr: null, - metadata: {}, - }, - "respond", - ); - Alert.alert("Sent", "A test 'respond' notification was fired. Check your notification shade."); - } catch (err) { - Alert.alert("Failed", err instanceof Error ? err.message : "Could not schedule notification."); - } - }; - - const handleTestMergeNotification = async () => { - const { status } = await Notifications.getPermissionsAsync(); - if (status !== "granted") { - Alert.alert("Permission denied", "Notification permission is not granted. Enable it in your phone's Settings app."); - return; - } - try { - await scheduleNotification( - { - id: "ao-test-session", - projectId: "test", - status: "mergeable", - activity: "idle", - branch: "feat/test", - issueId: null, - issueUrl: null, - issueLabel: "TEST-1", - issueTitle: "Add user authentication flow", - summary: null, - summaryIsFallback: false, - createdAt: new Date().toISOString(), - lastActivityAt: new Date().toISOString(), - pr: { - number: 42, - url: "", - title: "Add auth flow", - owner: "", - repo: "", - branch: "feat/test", - baseBranch: "main", - isDraft: false, - state: "open", - additions: 120, - deletions: 8, - ciStatus: "passing", - ciChecks: [], - reviewDecision: "approved", - mergeability: { mergeable: true, ciPassing: true, approved: true, noConflicts: true, blockers: [] }, - unresolvedThreads: 0, - }, - metadata: {}, - }, - "merge", - ); - Alert.alert("Sent", "A test 'merge' notification was fired. Check your notification shade."); - } catch (err) { - Alert.alert("Failed", err instanceof Error ? err.message : "Could not schedule notification."); - } - }; - - const handleTestReviewNotification = async () => { - const { status } = await Notifications.getPermissionsAsync(); - if (status !== "granted") { - Alert.alert("Permission denied", "Notification permission is not granted. Enable it in your phone's Settings app."); - return; - } - try { - await scheduleNotification( - { - id: "ao-test-session", - projectId: "test", - status: "review_pending", - activity: "idle", - branch: "feat/test", - issueId: null, - issueUrl: null, - issueLabel: "TEST-1", - issueTitle: "Refactor database connection pool", - summary: "Changes requested on PR", - summaryIsFallback: false, - createdAt: new Date().toISOString(), - lastActivityAt: new Date().toISOString(), - pr: { - number: 99, - url: "", - title: "Refactor DB pool", - owner: "", - repo: "", - branch: "feat/test", - baseBranch: "main", - isDraft: false, - state: "open", - additions: 85, - deletions: 42, - ciStatus: "passing", - ciChecks: [], - reviewDecision: "changes_requested", - mergeability: { mergeable: true, ciPassing: true, approved: false, noConflicts: true, blockers: ["Changes requested"] }, - unresolvedThreads: 2, - }, - metadata: {}, - }, - "review", - ); - Alert.alert("Sent", "A test 'review' notification was fired. Check your notification shade."); - } catch (err) { - Alert.alert("Failed", err instanceof Error ? err.message : "Could not schedule notification."); - } - }; - - const handleSave = async () => { - const trimmed = input.trim(); - if (!trimmed) { - Alert.alert("Invalid URL", "Please enter a valid backend URL."); - return; - } - if (!trimmed.startsWith("http://") && !trimmed.startsWith("https://")) { - Alert.alert("Invalid URL", "URL must start with http:// or https://"); - return; - } - setSaving(true); - try { - await setBackendUrl(trimmed); - await setTerminalWsOverride(wsInput.trim()); - Alert.alert("Saved", "Settings updated.", [ - { text: "OK", onPress: () => navigation.goBack() }, - ]); - } finally { - setSaving(false); - } - }; - - return ( - - - - Backend URL - - Enter the URL where your AO dashboard is running. - - Dashboard API URL - - - Terminal WebSocket URL{" "} - (leave blank to auto-derive) - - - - {saving ? "Saving..." : "Save"} - - - - - Active URLs - - - - - {__DEV__ && ( - - Test Notifications - Fire a test notification to verify permissions are working. Tap the notification to navigate to the session. - - Test "Agent needs input" (respond) - - - Test "PR ready to merge" (merge) - - - Test "Session needs review" (review) - - - )} - - - Setup Guide — Tailscale (Recommended) - - - - - - Alternative: Local Wi-Fi - - - - Alternative: ngrok - - - - - - - ); -} - -function SettingsInfoRow({ label, value, note }: { label: string; value: string; note?: string }) { - return ( - - - {label} - {note ? {note} : null} - - - {value} - - - ); -} - -function Step({ n, text }: { n: string; text: string }) { - return ( - - - {n} - - {text} - - ); -} - -const styles = StyleSheet.create({ - root: { - flex: 1, - backgroundColor: "#0d1117", - }, - container: { - flex: 1, - }, - content: { - padding: 14, - paddingBottom: 32, - gap: 12, - }, - section: { - backgroundColor: "#161b22", - borderRadius: 10, - padding: 16, - }, - sectionTitle: { - color: "#8b949e", - fontSize: 11, - fontWeight: "700", - letterSpacing: 0.8, - textTransform: "uppercase", - marginBottom: 12, - }, - sectionDivider: { - color: "#8b949e", - fontSize: 11, - fontWeight: "700", - letterSpacing: 0.8, - textTransform: "uppercase", - marginTop: 12, - marginBottom: 12, - paddingTop: 12, - borderTopWidth: 1, - borderTopColor: "#30363d", - }, - hint: { - color: "#8b949e", - fontSize: 13, - lineHeight: 18, - marginBottom: 12, - }, - input: { - backgroundColor: "#0d1117", - borderWidth: 1, - borderColor: "#30363d", - borderRadius: 8, - paddingHorizontal: 12, - paddingVertical: 10, - color: "#e6edf3", - fontSize: 14, - fontFamily: "monospace", - marginBottom: 12, - }, - saveButton: { - backgroundColor: "#238636", - borderRadius: 8, - paddingVertical: 12, - alignItems: "center", - }, - saveButtonDisabled: { - backgroundColor: "#21262d", - }, - saveButtonText: { - color: "#fff", - fontSize: 15, - fontWeight: "600", - }, - testButton: { - borderWidth: 1, - borderRadius: 8, - paddingVertical: 12, - alignItems: "center", - }, - testButtonText: { - fontSize: 14, - fontWeight: "600", - }, - infoRow: { - flexDirection: "row", - justifyContent: "space-between", - paddingVertical: 6, - }, - infoLabel: { - color: "#8b949e", - fontSize: 13, - width: 100, - }, - infoValue: { - color: "#58a6ff", - fontSize: 13, - fontFamily: "monospace", - flex: 1, - textAlign: "right", - }, - infoLabelRow: { - flexDirection: "column", - width: 100, - }, - infoNote: { - color: "#8b949e", - fontSize: 10, - marginTop: 1, - }, - fieldLabel: { - color: "#8b949e", - fontSize: 12, - fontWeight: "600", - marginBottom: 6, - }, - fieldLabelMuted: { - color: "#6e7681", - fontWeight: "400", - }, - step: { - flexDirection: "row", - alignItems: "flex-start", - marginBottom: 12, - gap: 10, - }, - stepBadge: { - backgroundColor: "#21262d", - borderRadius: 10, - width: 20, - height: 20, - alignItems: "center", - justifyContent: "center", - flexShrink: 0, - }, - stepN: { - color: "#58a6ff", - fontSize: 11, - fontWeight: "700", - }, - stepText: { - color: "#8b949e", - fontSize: 13, - lineHeight: 18, - flex: 1, - }, -}); diff --git a/packages/mobile/src/screens/SpawnSessionScreen.tsx b/packages/mobile/src/screens/SpawnSessionScreen.tsx deleted file mode 100644 index 0d52a696c..000000000 --- a/packages/mobile/src/screens/SpawnSessionScreen.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import React, { useState, useCallback } from "react"; -import { - View, - Text, - TextInput, - TouchableOpacity, - StyleSheet, - Alert, - KeyboardAvoidingView, - Platform, - ScrollView, -} from "react-native"; -import type { NativeStackScreenProps } from "@react-navigation/native-stack"; -import type { RootStackParamList } from "../navigation/RootNavigator"; -import { useBackend } from "../context/BackendContext"; - -type Props = NativeStackScreenProps; - -export default function SpawnSessionScreen({ navigation }: Props) { - const { spawnSession } = useBackend(); - const [projectId, setProjectId] = useState(""); - const [issueId, setIssueId] = useState(""); - const [spawning, setSpawning] = useState(false); - - const handleSpawn = useCallback(async () => { - const trimmedProject = projectId.trim(); - if (!trimmedProject) { - Alert.alert("Invalid", "Project ID is required."); - return; - } - if (!/^[a-zA-Z0-9_-]+$/.test(trimmedProject)) { - Alert.alert("Invalid", "Project ID must be alphanumeric (hyphens and underscores allowed)."); - return; - } - const trimmedIssue = issueId.trim(); - if (trimmedIssue && !/^[a-zA-Z0-9_-]+$/.test(trimmedIssue)) { - Alert.alert("Invalid", "Issue ID must be alphanumeric (hyphens and underscores allowed)."); - return; - } - - setSpawning(true); - try { - const session = await spawnSession(trimmedProject, trimmedIssue || undefined); - navigation.replace("SessionDetail", { sessionId: session.id }); - } catch (err) { - Alert.alert("Spawn failed", err instanceof Error ? err.message : "Failed to spawn session"); - } finally { - setSpawning(false); - } - }, [projectId, issueId, spawnSession, navigation]); - - return ( - - - - Spawn New Session - - Create a new agent session. The orchestrator will assign an agent and workspace. - - - Project ID * - - - - Issue ID{" "} - (optional) - - - - - - {spawning ? "Spawning..." : "Spawn Session"} - - - - - - ); -} - -const styles = StyleSheet.create({ - root: { - flex: 1, - backgroundColor: "#0d1117", - }, - container: { - flex: 1, - }, - content: { - padding: 14, - }, - 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: 16, - }, - fieldLabel: { - color: "#8b949e", - fontSize: 12, - fontWeight: "600", - marginBottom: 6, - }, - fieldLabelMuted: { - color: "#6e7681", - fontWeight: "400", - }, - input: { - backgroundColor: "#0d1117", - borderWidth: 1, - borderColor: "#30363d", - borderRadius: 8, - paddingHorizontal: 12, - paddingVertical: 10, - color: "#e6edf3", - fontSize: 14, - fontFamily: "monospace", - marginBottom: 14, - }, - spawnButton: { - backgroundColor: "#238636", - borderRadius: 8, - paddingVertical: 14, - alignItems: "center", - marginTop: 4, - }, - spawnButtonDisabled: { - backgroundColor: "#21262d", - }, - spawnButtonText: { - color: "#fff", - fontSize: 15, - fontWeight: "600", - }, -}); diff --git a/packages/mobile/src/screens/TerminalScreen.tsx b/packages/mobile/src/screens/TerminalScreen.tsx deleted file mode 100644 index 88d46e0b9..000000000 --- a/packages/mobile/src/screens/TerminalScreen.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import React, { useRef, useCallback, useState } from "react"; -import { View, StyleSheet, StatusBar, Text } from "react-native"; -import { WebView, type WebViewMessageEvent } from "react-native-webview"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import type { NativeStackScreenProps } from "@react-navigation/native-stack"; -import type { RootStackParamList } from "../navigation/RootNavigator"; - -import { TERMINAL_HTML } from "../terminal/terminal-html"; - -type Props = NativeStackScreenProps; - -type ConnectionStatus = "connecting" | "connected" | "error" | "disconnected"; - -const STATUS_COLORS: Record = { - connecting: "#e3b341", - connected: "#3fb950", - error: "#f85149", - disconnected: "#f85149", -}; - -export default function TerminalScreen({ route }: Props) { - const { sessionId, terminalWsUrl } = route.params; - const webViewRef = useRef(null); - const insets = useSafeAreaInsets(); - const [status, setStatus] = useState("connecting"); - - // Inject WS URL and session ID BEFORE xterm.js init runs - const injectedJS = ` - window.AO_WS_URL = ${JSON.stringify(terminalWsUrl)}; - window.AO_SESSION_ID = ${JSON.stringify(sessionId)}; - true; // required return value - `; - - const handleMessage = useCallback((event: WebViewMessageEvent) => { - try { - const msg = JSON.parse(event.nativeEvent.data) as { - type: string; - state?: ConnectionStatus; - }; - if (msg.type === "status" && msg.state) { - setStatus(msg.state); - } - } catch { - // ignore unparseable messages - } - }, []); - - const handleLoad = useCallback(() => { - // Ask xterm to fit to current viewport - webViewRef.current?.injectJavaScript( - 'window.dispatchEvent(new MessageEvent("message", { data: JSON.stringify({ type: "fit" }) })); true;', - ); - }, []); - - const dotColor = STATUS_COLORS[status]; - - return ( - - - - {/* Status indicator in top-right */} - - - - {status} - - - - {}} - /> - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: "#0d1117", - }, - webView: { - flex: 1, - backgroundColor: "#0d1117", - }, - statusDot: { - position: "absolute", - top: 8, - right: 12, - flexDirection: "row", - alignItems: "center", - zIndex: 10, - gap: 4, - }, - dot: { - width: 8, - height: 8, - borderRadius: 4, - }, - statusText: { - fontSize: 11, - fontWeight: "600", - }, -}); diff --git a/packages/mobile/src/terminal/terminal-html.ts b/packages/mobile/src/terminal/terminal-html.ts deleted file mode 100644 index 62f162462..000000000 --- a/packages/mobile/src/terminal/terminal-html.ts +++ /dev/null @@ -1,178 +0,0 @@ -/** - * Standalone xterm.js terminal page, loaded into WebView. - * WS URL and session ID are injected via injectedJavaScriptBeforeContentLoaded - * which sets window.AO_WS_URL and window.AO_SESSION_ID before the script runs. - */ -export const TERMINAL_HTML = ` - - - - -Terminal -