diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..bec2627e --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +# Copy to .env (or .env.secrets if using direnv) for local development. + +# Optional: set this to use a non-default Codex config location. +# By default, Codex uses the system user config (for example ~/.codex). +# CODEX_HOME=~/.codex + +# Optional: custom Codex CLI binary path. +# GAMBIT_CODEX_BIN=codex diff --git a/README.md b/README.md index 1c3f7ca8..62ef89e0 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,18 @@ open http://localhost:8000/debug Use the CLI to run decks locally, stream output, and capture traces/state. +### Environment setup + +For local development, copy `.env.example` to `.env` if you want local +overrides: + +```bash +cp .env.example .env +``` + +Gambit uses your system Codex login/config by default. If you need a custom +location, set `CODEX_HOME` explicitly. + Run with npx (no install): ``` diff --git a/simulator-ui/src/WorkbenchDrawer.tsx b/simulator-ui/src/WorkbenchDrawer.tsx index 4e63f227..3dd0dc25 100644 --- a/simulator-ui/src/WorkbenchDrawer.tsx +++ b/simulator-ui/src/WorkbenchDrawer.tsx @@ -96,34 +96,33 @@ export default function WorkbenchDrawer(props: WorkbenchDrawerProps) { const [chatHistoryError, setChatHistoryError] = useState(null); const [copiedStatePath, setCopiedStatePath] = useState(false); const [copiedCodexLoginCommand, setCopiedCodexLoginCommand] = useState(false); - const [codexTrustPending, setCodexTrustPending] = useState(false); - const [codexTrustError, setCodexTrustError] = useState(null); - const [codexTrustSuccess, setCodexTrustSuccess] = useState( - null, + const [codexLoginRecheckPending, setCodexLoginRecheckPending] = useState( + false, ); - const [codexWorkspaceWriteEnabled, setCodexWorkspaceWriteEnabled] = useState< - boolean | null - >(null); + const [showCodexLoginRecheck, setShowCodexLoginRecheck] = useState(false); + const [codexAutoRecheckActive, setCodexAutoRecheckActive] = useState(false); const [codexWorkspaceLoggedIn, setCodexWorkspaceLoggedIn] = useState< boolean | null >(null); + const [codexLoginError, setCodexLoginError] = useState(null); const [codexLoginStatusText, setCodexLoginStatusText] = useState< string | null >(null); - const [codexTrustedPath, setCodexTrustedPath] = useState(null); - const [codexTrustOverlayDismissed, setCodexTrustOverlayDismissed] = useState( + const [codexLoginOverlayDismissed, setCodexLoginOverlayDismissed] = useState( false, ); const initializedChipTrackingRef = useRef(false); const seenRatingChipIdsRef = useRef(new Set()); const seenFlagChipIdsRef = useRef(new Set()); - const showCodexTrustOverlay = (codexWorkspaceWriteEnabled === false || - codexWorkspaceLoggedIn === false) && - !codexTrustOverlayDismissed || Boolean(codexTrustError); + const showCodexLoginOverlay = codexWorkspaceLoggedIn === false && + !codexLoginOverlayDismissed || Boolean(codexLoginError); const workspaceIdForTrust = (sessionId ?? run.id) || undefined; - const codexLoginCommand = codexTrustedPath - ? `CODEX_HOME="${codexTrustedPath}/.codex" codex login` - : 'CODEX_HOME="/.codex" codex login'; + const codexStatusEndpoint = workspaceIdForTrust + ? `/api/codex/trust-workspace?workspaceId=${ + encodeURIComponent(workspaceIdForTrust) + }` + : "/api/codex/trust-workspace"; + const codexLoginCommand = "codex login"; const resolvedStatePath = useMemo(() => { if (statePath) return statePath; const meta = sessionDetail?.meta; @@ -353,27 +352,21 @@ export default function WorkbenchDrawer(props: WorkbenchDrawerProps) { initializedChipTrackingRef.current = false; seenRatingChipIdsRef.current.clear(); seenFlagChipIdsRef.current.clear(); - setCodexTrustPending(false); - setCodexTrustError(null); - setCodexTrustSuccess(null); - setCodexWorkspaceWriteEnabled(null); setCodexWorkspaceLoggedIn(null); + setCodexLoginError(null); setCodexLoginStatusText(null); - setCodexTrustedPath(null); setCopiedCodexLoginCommand(false); - setCodexTrustOverlayDismissed(false); + setCodexLoginRecheckPending(false); + setShowCodexLoginRecheck(false); + setCodexAutoRecheckActive(false); + setCodexLoginOverlayDismissed(false); }, [sessionId]); useEffect(() => { if (!open) return; - if (!workspaceIdForTrust) return; let canceled = false; - setCodexTrustError(null); - fetch( - `/api/codex/trust-workspace?workspaceId=${ - encodeURIComponent(workspaceIdForTrust) - }`, - ) + setCodexLoginError(null); + fetch(codexStatusEndpoint) .then(async (response) => { const payload = await response.json() as { ok?: boolean; @@ -381,30 +374,28 @@ export default function WorkbenchDrawer(props: WorkbenchDrawerProps) { writeEnabled?: boolean; codexLoggedIn?: boolean; codexLoginStatus?: string; - trustedPath?: string; error?: string; }; if (!response.ok || payload.ok === false) { throw new Error(payload.error || response.statusText); } if (canceled) return; - setCodexWorkspaceWriteEnabled(payload.writeEnabled === true); setCodexWorkspaceLoggedIn(payload.codexLoggedIn === true); + if (payload.codexLoggedIn === true) { + setShowCodexLoginRecheck(false); + setCodexAutoRecheckActive(false); + } setCodexLoginStatusText( typeof payload.codexLoginStatus === "string" ? payload.codexLoginStatus : null, ); - setCodexTrustedPath( - typeof payload.trustedPath === "string" ? payload.trustedPath : null, - ); }) .catch((err) => { if (canceled) return; - setCodexWorkspaceWriteEnabled(null); setCodexWorkspaceLoggedIn(null); setCodexLoginStatusText(null); - setCodexTrustError(err instanceof Error ? err.message : String(err)); + setCodexLoginError(err instanceof Error ? err.message : String(err)); }) .finally(() => { if (canceled) return; @@ -412,7 +403,17 @@ export default function WorkbenchDrawer(props: WorkbenchDrawerProps) { return () => { canceled = true; }; - }, [open, workspaceIdForTrust]); + }, [codexStatusEndpoint, open]); + + useEffect(() => { + if (!showCodexLoginOverlay) return; + if (codexWorkspaceLoggedIn !== false) return; + if (showCodexLoginRecheck) return; + const timeout = window.setTimeout(() => { + setShowCodexLoginRecheck(true); + }, 5000); + return () => window.clearTimeout(timeout); + }, [codexWorkspaceLoggedIn, showCodexLoginOverlay, showCodexLoginRecheck]); useEffect(() => { if (loading) return; @@ -570,103 +571,69 @@ export default function WorkbenchDrawer(props: WorkbenchDrawerProps) { const handleCopyCodexLoginCommand = useCallback(() => { navigator.clipboard?.writeText(codexLoginCommand); setCopiedCodexLoginCommand(true); + setShowCodexLoginRecheck(true); + setCodexAutoRecheckActive(true); window.setTimeout(() => setCopiedCodexLoginCommand(false), 1200); }, [codexLoginCommand]); - useEffect(() => { - if (!open) return; - if (!onClose) return; - const handler = (event: KeyboardEvent) => { - if (event.key === "Escape") { - onClose(); - } - }; - window.addEventListener("keydown", handler); - return () => window.removeEventListener("keydown", handler); - }, [onClose, open]); - const trustWorkspaceInCodex = useCallback(async () => { - setCodexTrustPending(true); - setCodexTrustError(null); - setCodexTrustSuccess(null); + const handleRecheckCodexLogin = useCallback(async () => { + setCodexLoginRecheckPending(true); + setCodexLoginError(null); try { - const statusResponse = await fetch( - `/api/codex/trust-workspace?workspaceId=${ - encodeURIComponent(workspaceIdForTrust ?? "") - }`, - ); - const statusPayload = await statusResponse.json() as { - ok?: boolean; - trusted?: boolean; - writeEnabled?: boolean; - codexLoggedIn?: boolean; - codexLoginStatus?: string; - trustedPath?: string; - error?: string; - }; - if (!statusResponse.ok || statusPayload.ok === false) { - throw new Error(statusPayload.error || statusResponse.statusText); - } - if ( - statusPayload.writeEnabled === true && - statusPayload.codexLoggedIn === true - ) { - setCodexWorkspaceWriteEnabled(true); - setCodexWorkspaceLoggedIn(true); - setCodexTrustSuccess( - "Workspace is already configured for Codex writes.", - ); - setCodexTrustOverlayDismissed(true); - return; - } - setCodexWorkspaceWriteEnabled(statusPayload.writeEnabled === true); - setCodexWorkspaceLoggedIn(statusPayload.codexLoggedIn === true); - setCodexLoginStatusText( - typeof statusPayload.codexLoginStatus === "string" - ? statusPayload.codexLoginStatus - : null, - ); - setCodexTrustedPath( - typeof statusPayload.trustedPath === "string" - ? statusPayload.trustedPath - : null, - ); - - const response = await fetch("/api/codex/trust-workspace", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ workspaceId: workspaceIdForTrust }), - }); + const response = await fetch(codexStatusEndpoint); const payload = await response.json() as { ok?: boolean; - error?: string; - trustedPath?: string; - writeEnabled?: boolean; codexLoggedIn?: boolean; codexLoginStatus?: string; + error?: string; }; if (!response.ok || payload.ok === false) { throw new Error(payload.error || response.statusText); } - const trustedPath = typeof payload.trustedPath === "string" - ? payload.trustedPath - : "workspace"; - setCodexTrustSuccess(`Codex write enabled for: ${trustedPath}`); - setCodexWorkspaceWriteEnabled(payload.writeEnabled === true); setCodexWorkspaceLoggedIn(payload.codexLoggedIn === true); + if (payload.codexLoggedIn === true) { + setShowCodexLoginRecheck(false); + setCodexAutoRecheckActive(false); + } setCodexLoginStatusText( typeof payload.codexLoginStatus === "string" ? payload.codexLoginStatus : null, ); - setCodexTrustedPath( - typeof payload.trustedPath === "string" ? payload.trustedPath : null, - ); - setCodexTrustOverlayDismissed(payload.codexLoggedIn === true); } catch (err) { - setCodexTrustError(err instanceof Error ? err.message : String(err)); + setCodexWorkspaceLoggedIn(null); + setCodexLoginStatusText(null); + setCodexLoginError(err instanceof Error ? err.message : String(err)); } finally { - setCodexTrustPending(false); + setCodexLoginRecheckPending(false); } - }, [workspaceIdForTrust]); + }, [codexStatusEndpoint]); + useEffect(() => { + if (!codexAutoRecheckActive) return; + if (!showCodexLoginOverlay) return; + if (codexWorkspaceLoggedIn !== false) return; + const interval = window.setInterval(() => { + if (codexLoginRecheckPending) return; + void handleRecheckCodexLogin(); + }, 2000); + return () => window.clearInterval(interval); + }, [ + codexAutoRecheckActive, + codexLoginRecheckPending, + codexWorkspaceLoggedIn, + handleRecheckCodexLogin, + showCodexLoginOverlay, + ]); + useEffect(() => { + if (!open) return; + if (!onClose) return; + const handler = (event: KeyboardEvent) => { + if (event.key === "Escape") { + onClose(); + } + }; + window.addEventListener("keydown", handler); + return () => window.removeEventListener("keydown", handler); + }, [onClose, open]); if (!open) return null; return (