agent-orchestrator/packages/mobile/src/context/BackendContext.tsx

154 lines
5.3 KiB
TypeScript

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<void>;
/** 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<void>;
fetchSessions: () => Promise<SessionsResponse>;
fetchSession: (id: string) => Promise<DashboardSession>;
sendMessage: (id: string, message: string) => Promise<void>;
killSession: (id: string) => Promise<void>;
restoreSession: (id: string) => Promise<void>;
mergePR: (prNumber: number) => Promise<void>;
spawnSession: (projectId: string, issueId?: string) => Promise<DashboardSession>;
}
const BackendContext = createContext<BackendContextValue | null>(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<SessionsResponse> => {
const res = await apiFetch("/api/sessions");
return res.json() as Promise<SessionsResponse>;
}, [apiFetch]);
const fetchSession = useCallback(async (id: string): Promise<DashboardSession> => {
const res = await apiFetch(`/api/sessions/${encodeURIComponent(id)}`);
return res.json() as Promise<DashboardSession>;
}, [apiFetch]);
const sendMessage = useCallback(async (id: string, message: string): Promise<void> => {
await apiFetch(`/api/sessions/${encodeURIComponent(id)}/message`, {
method: "POST",
body: JSON.stringify({ message }),
});
}, [apiFetch]);
const killSession = useCallback(async (id: string): Promise<void> => {
await apiFetch(`/api/sessions/${encodeURIComponent(id)}/kill`, { method: "POST" });
}, [apiFetch]);
const restoreSession = useCallback(async (id: string): Promise<void> => {
await apiFetch(`/api/sessions/${encodeURIComponent(id)}/restore`, { method: "POST" });
}, [apiFetch]);
const mergePR = useCallback(async (prNumber: number): Promise<void> => {
await apiFetch(`/api/prs/${prNumber}/merge`, { method: "POST" });
}, [apiFetch]);
const spawnSession = useCallback(async (projectId: string, issueId?: string): Promise<DashboardSession> => {
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 (
<BackendContext.Provider
value={{
backendUrl,
setBackendUrl,
terminalWsUrl,
terminalWsOverride,
setTerminalWsOverride,
fetchSessions,
fetchSession,
sendMessage,
killSession,
restoreSession,
mergePR,
spawnSession,
}}
>
{children}
</BackendContext.Provider>
);
}
export function useBackend(): BackendContextValue {
const ctx = useContext(BackendContext);
if (!ctx) throw new Error("useBackend must be used inside BackendProvider");
return ctx;
}