154 lines
5.3 KiB
TypeScript
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;
|
|
}
|