From 239673bc8bfae3676b679d4e253cd244e417e89c Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Thu, 12 Feb 2026 09:29:30 -0500 Subject: [PATCH 1/7] feat: check for sandbox snapshot on login and trigger setup if missing Adds useSetupSandbox hook that runs once after Privy authentication. Checks GET /api/sandboxes for existing snapshots, and if none exist, fires POST /api/sandboxes/setup in the background to provision a GitHub repo + snapshot before the user starts using the terminal. Co-Authored-By: Claude Opus 4.6 --- app/components/TerminalPage.tsx | 2 ++ app/hooks/useSetupSandbox.ts | 41 +++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 app/hooks/useSetupSandbox.ts diff --git a/app/components/TerminalPage.tsx b/app/components/TerminalPage.tsx index 1f1a426a..19536e38 100644 --- a/app/components/TerminalPage.tsx +++ b/app/components/TerminalPage.tsx @@ -4,6 +4,7 @@ import { useEffect, useState, ReactNode } from "react"; import { usePrivy } from "@privy-io/react-auth"; import TerminalComponent from "./Terminal"; import { TerminalData } from "./TerminalData"; +import { useSetupSandbox } from "../hooks/useSetupSandbox"; export default function TerminalPage({ agentEndpoint, @@ -14,6 +15,7 @@ export default function TerminalPage({ }) { const [mounted, setMounted] = useState(false); const { ready, authenticated, login, getAccessToken } = usePrivy(); + useSetupSandbox({ getAccessToken, authenticated }); useEffect(() => { setMounted(true); diff --git a/app/hooks/useSetupSandbox.ts b/app/hooks/useSetupSandbox.ts new file mode 100644 index 00000000..e88626f7 --- /dev/null +++ b/app/hooks/useSetupSandbox.ts @@ -0,0 +1,41 @@ +import { useEffect, useRef } from "react"; + +const RECOUP_API_URL = + process.env.NEXT_PUBLIC_RECOUP_API_URL || "https://recoup-api.vercel.app"; + +export function useSetupSandbox({ + getAccessToken, + authenticated, +}: { + getAccessToken: () => Promise; + authenticated: boolean; +}) { + const hasRun = useRef(false); + + useEffect(() => { + if (!authenticated || hasRun.current) return; + hasRun.current = true; + + (async () => { + try { + const token = await getAccessToken(); + if (!token) return; + + const headers = { Authorization: `Bearer ${token}` }; + + const res = await fetch(`${RECOUP_API_URL}/api/sandboxes`, { headers }); + if (!res.ok) return; + + const data = await res.json(); + if (data.sandboxes && data.sandboxes.length > 0) return; + + fetch(`${RECOUP_API_URL}/api/sandboxes/setup`, { + method: "POST", + headers, + }); + } catch { + // Silent — background provisioning only + } + })(); + }, [authenticated, getAccessToken]); +} From 7a38ae7eaa0767876a88e77927d45128e27bd16f Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Thu, 12 Feb 2026 09:31:41 -0500 Subject: [PATCH 2/7] refactor: use usePrivy directly inside useSetupSandbox hook Co-Authored-By: Claude Opus 4.6 --- app/components/TerminalPage.tsx | 2 +- app/hooks/useSetupSandbox.ts | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/app/components/TerminalPage.tsx b/app/components/TerminalPage.tsx index 19536e38..cbebe3e5 100644 --- a/app/components/TerminalPage.tsx +++ b/app/components/TerminalPage.tsx @@ -15,7 +15,7 @@ export default function TerminalPage({ }) { const [mounted, setMounted] = useState(false); const { ready, authenticated, login, getAccessToken } = usePrivy(); - useSetupSandbox({ getAccessToken, authenticated }); + useSetupSandbox(); useEffect(() => { setMounted(true); diff --git a/app/hooks/useSetupSandbox.ts b/app/hooks/useSetupSandbox.ts index e88626f7..2bd0caf0 100644 --- a/app/hooks/useSetupSandbox.ts +++ b/app/hooks/useSetupSandbox.ts @@ -1,15 +1,11 @@ import { useEffect, useRef } from "react"; +import { usePrivy } from "@privy-io/react-auth"; const RECOUP_API_URL = process.env.NEXT_PUBLIC_RECOUP_API_URL || "https://recoup-api.vercel.app"; -export function useSetupSandbox({ - getAccessToken, - authenticated, -}: { - getAccessToken: () => Promise; - authenticated: boolean; -}) { +export function useSetupSandbox() { + const { authenticated, getAccessToken } = usePrivy(); const hasRun = useRef(false); useEffect(() => { From c237636fee153954dd5da73aaa20e6f9f3c884d9 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Thu, 12 Feb 2026 09:32:26 -0500 Subject: [PATCH 3/7] fix: use NEXT_PUBLIC_VERCEL_ENV for Recoup API URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Matches the existing pattern in lib/recoup-api/ — uses production URL in prod and test URL otherwise. Co-Authored-By: Claude Opus 4.6 --- app/hooks/useSetupSandbox.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/hooks/useSetupSandbox.ts b/app/hooks/useSetupSandbox.ts index 2bd0caf0..11eacbc6 100644 --- a/app/hooks/useSetupSandbox.ts +++ b/app/hooks/useSetupSandbox.ts @@ -1,8 +1,10 @@ import { useEffect, useRef } from "react"; import { usePrivy } from "@privy-io/react-auth"; -const RECOUP_API_URL = - process.env.NEXT_PUBLIC_RECOUP_API_URL || "https://recoup-api.vercel.app"; +const IS_PROD = process.env.NEXT_PUBLIC_VERCEL_ENV === "production"; +const RECOUP_API_URL = IS_PROD + ? "https://recoup-api.vercel.app" + : "https://test-recoup-api.vercel.app"; export function useSetupSandbox() { const { authenticated, getAccessToken } = usePrivy(); From e1b1cbf6a9ebb371f41bc2673f4b66162e5d7792 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Thu, 12 Feb 2026 09:33:29 -0500 Subject: [PATCH 4/7] refactor: extract RECOUP_API_URL into lib/consts.ts Deduplicates the env-based URL logic from three files into a single shared constant. Co-Authored-By: Claude Opus 4.6 --- app/hooks/useSetupSandbox.ts | 6 +----- lib/consts.ts | 5 +++++ lib/recoup-api/createSandbox.ts | 5 +---- lib/recoup-api/updateAccountSnapshot.ts | 5 +---- 4 files changed, 8 insertions(+), 13 deletions(-) create mode 100644 lib/consts.ts diff --git a/app/hooks/useSetupSandbox.ts b/app/hooks/useSetupSandbox.ts index 11eacbc6..bd391897 100644 --- a/app/hooks/useSetupSandbox.ts +++ b/app/hooks/useSetupSandbox.ts @@ -1,10 +1,6 @@ import { useEffect, useRef } from "react"; import { usePrivy } from "@privy-io/react-auth"; - -const IS_PROD = process.env.NEXT_PUBLIC_VERCEL_ENV === "production"; -const RECOUP_API_URL = IS_PROD - ? "https://recoup-api.vercel.app" - : "https://test-recoup-api.vercel.app"; +import { RECOUP_API_URL } from "@/lib/consts"; export function useSetupSandbox() { const { authenticated, getAccessToken } = usePrivy(); diff --git a/lib/consts.ts b/lib/consts.ts new file mode 100644 index 00000000..c667cf3f --- /dev/null +++ b/lib/consts.ts @@ -0,0 +1,5 @@ +const IS_PROD = process.env.NEXT_PUBLIC_VERCEL_ENV === "production"; + +export const RECOUP_API_URL = IS_PROD + ? "https://recoup-api.vercel.app" + : "https://test-recoup-api.vercel.app"; diff --git a/lib/recoup-api/createSandbox.ts b/lib/recoup-api/createSandbox.ts index 24e13208..56ec3b35 100644 --- a/lib/recoup-api/createSandbox.ts +++ b/lib/recoup-api/createSandbox.ts @@ -1,7 +1,4 @@ -const IS_PROD = process.env.NEXT_PUBLIC_VERCEL_ENV === "production"; -const RECOUP_API_URL = IS_PROD - ? "https://recoup-api.vercel.app" - : "https://test-recoup-api.vercel.app"; +import { RECOUP_API_URL } from "@/lib/consts"; export async function createSandbox( bearerToken: string, diff --git a/lib/recoup-api/updateAccountSnapshot.ts b/lib/recoup-api/updateAccountSnapshot.ts index 19712984..b2bb7626 100644 --- a/lib/recoup-api/updateAccountSnapshot.ts +++ b/lib/recoup-api/updateAccountSnapshot.ts @@ -1,7 +1,4 @@ -const IS_PROD = process.env.NEXT_PUBLIC_VERCEL_ENV === "production"; -const RECOUP_API_URL = IS_PROD - ? "https://recoup-api.vercel.app" - : "https://test-recoup-api.vercel.app"; +import { RECOUP_API_URL } from "@/lib/consts"; export async function updateAccountSnapshot( bearerToken: string, From 481bff4c12d18ca2f6478bd683aba63a47814822 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Thu, 12 Feb 2026 09:34:50 -0500 Subject: [PATCH 5/7] fix: check for snapshot_id and github_repo before skipping setup Co-Authored-By: Claude Opus 4.6 --- app/hooks/useSetupSandbox.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/hooks/useSetupSandbox.ts b/app/hooks/useSetupSandbox.ts index bd391897..9351a878 100644 --- a/app/hooks/useSetupSandbox.ts +++ b/app/hooks/useSetupSandbox.ts @@ -21,7 +21,8 @@ export function useSetupSandbox() { if (!res.ok) return; const data = await res.json(); - if (data.sandboxes && data.sandboxes.length > 0) return; + const sandbox = data.sandboxes?.[0]; + if (sandbox?.snapshot_id && sandbox?.github_repo) return; fetch(`${RECOUP_API_URL}/api/sandboxes/setup`, { method: "POST", From 46ceeab5d04d0434bc46868f7a6048b6b41a2c8d Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Thu, 12 Feb 2026 09:36:07 -0500 Subject: [PATCH 6/7] fix: read snapshot_id and github_repo from top-level response These are top-level fields on the GET /api/sandboxes response, not nested inside the sandboxes array. Co-Authored-By: Claude Opus 4.6 --- app/hooks/useSetupSandbox.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/hooks/useSetupSandbox.ts b/app/hooks/useSetupSandbox.ts index 9351a878..7ee87901 100644 --- a/app/hooks/useSetupSandbox.ts +++ b/app/hooks/useSetupSandbox.ts @@ -21,8 +21,7 @@ export function useSetupSandbox() { if (!res.ok) return; const data = await res.json(); - const sandbox = data.sandboxes?.[0]; - if (sandbox?.snapshot_id && sandbox?.github_repo) return; + if (data.snapshot_id && data.github_repo) return; fetch(`${RECOUP_API_URL}/api/sandboxes/setup`, { method: "POST", From 5d0152f66013dca841147766d237b1190222a5ef Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Thu, 12 Feb 2026 09:37:20 -0500 Subject: [PATCH 7/7] refactor: extract fetch calls into getSandboxes and setupSandbox libs Co-Authored-By: Claude Opus 4.6 --- app/hooks/useSetupSandbox.ts | 16 +++++----------- lib/recoup-api/getSandboxes.ts | 11 +++++++++++ lib/recoup-api/setupSandbox.ts | 8 ++++++++ 3 files changed, 24 insertions(+), 11 deletions(-) create mode 100644 lib/recoup-api/getSandboxes.ts create mode 100644 lib/recoup-api/setupSandbox.ts diff --git a/app/hooks/useSetupSandbox.ts b/app/hooks/useSetupSandbox.ts index 7ee87901..af498b72 100644 --- a/app/hooks/useSetupSandbox.ts +++ b/app/hooks/useSetupSandbox.ts @@ -1,6 +1,7 @@ import { useEffect, useRef } from "react"; import { usePrivy } from "@privy-io/react-auth"; -import { RECOUP_API_URL } from "@/lib/consts"; +import { getSandboxes } from "@/lib/recoup-api/getSandboxes"; +import { setupSandbox } from "@/lib/recoup-api/setupSandbox"; export function useSetupSandbox() { const { authenticated, getAccessToken } = usePrivy(); @@ -15,18 +16,11 @@ export function useSetupSandbox() { const token = await getAccessToken(); if (!token) return; - const headers = { Authorization: `Bearer ${token}` }; - - const res = await fetch(`${RECOUP_API_URL}/api/sandboxes`, { headers }); - if (!res.ok) return; - - const data = await res.json(); + const data = await getSandboxes(token); + if (!data) return; if (data.snapshot_id && data.github_repo) return; - fetch(`${RECOUP_API_URL}/api/sandboxes/setup`, { - method: "POST", - headers, - }); + setupSandbox(token); } catch { // Silent — background provisioning only } diff --git a/lib/recoup-api/getSandboxes.ts b/lib/recoup-api/getSandboxes.ts new file mode 100644 index 00000000..fe7569c7 --- /dev/null +++ b/lib/recoup-api/getSandboxes.ts @@ -0,0 +1,11 @@ +import { RECOUP_API_URL } from "@/lib/consts"; + +export async function getSandboxes(bearerToken: string) { + const response = await fetch(`${RECOUP_API_URL}/api/sandboxes`, { + headers: { Authorization: `Bearer ${bearerToken}` }, + }); + + if (!response.ok) return null; + + return response.json(); +} diff --git a/lib/recoup-api/setupSandbox.ts b/lib/recoup-api/setupSandbox.ts new file mode 100644 index 00000000..7f193eda --- /dev/null +++ b/lib/recoup-api/setupSandbox.ts @@ -0,0 +1,8 @@ +import { RECOUP_API_URL } from "@/lib/consts"; + +export function setupSandbox(bearerToken: string) { + fetch(`${RECOUP_API_URL}/api/sandboxes/setup`, { + method: "POST", + headers: { Authorization: `Bearer ${bearerToken}` }, + }); +}