diff --git a/simulator-ui/src/WorkbenchDrawer.tsx b/simulator-ui/src/WorkbenchDrawer.tsx index a2949a97..4e63f227 100644 --- a/simulator-ui/src/WorkbenchDrawer.tsx +++ b/simulator-ui/src/WorkbenchDrawer.tsx @@ -95,9 +95,35 @@ export default function WorkbenchDrawer(props: WorkbenchDrawerProps) { const [chatHistoryLoading, setChatHistoryLoading] = useState(false); 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 [codexWorkspaceWriteEnabled, setCodexWorkspaceWriteEnabled] = useState< + boolean | null + >(null); + const [codexWorkspaceLoggedIn, setCodexWorkspaceLoggedIn] = useState< + boolean | null + >(null); + const [codexLoginStatusText, setCodexLoginStatusText] = useState< + string | null + >(null); + const [codexTrustedPath, setCodexTrustedPath] = useState(null); + const [codexTrustOverlayDismissed, setCodexTrustOverlayDismissed] = 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 workspaceIdForTrust = (sessionId ?? run.id) || undefined; + const codexLoginCommand = codexTrustedPath + ? `CODEX_HOME="${codexTrustedPath}/.codex" codex login` + : 'CODEX_HOME="/.codex" codex login'; const resolvedStatePath = useMemo(() => { if (statePath) return statePath; const meta = sessionDetail?.meta; @@ -327,8 +353,67 @@ 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); + setCodexLoginStatusText(null); + setCodexTrustedPath(null); + setCopiedCodexLoginCommand(false); + setCodexTrustOverlayDismissed(false); }, [sessionId]); + useEffect(() => { + if (!open) return; + if (!workspaceIdForTrust) return; + let canceled = false; + setCodexTrustError(null); + fetch( + `/api/codex/trust-workspace?workspaceId=${ + encodeURIComponent(workspaceIdForTrust) + }`, + ) + .then(async (response) => { + const payload = await response.json() as { + ok?: boolean; + trusted?: boolean; + 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); + 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)); + }) + .finally(() => { + if (canceled) return; + }); + return () => { + canceled = true; + }; + }, [open, workspaceIdForTrust]); + useEffect(() => { if (loading) return; const currentRatingChipIds = new Set( @@ -482,6 +567,11 @@ export default function WorkbenchDrawer(props: WorkbenchDrawerProps) { window.setTimeout(() => setCopiedStatePath(false), 1200); }; }, [resolvedStatePath]); + const handleCopyCodexLoginCommand = useCallback(() => { + navigator.clipboard?.writeText(codexLoginCommand); + setCopiedCodexLoginCommand(true); + window.setTimeout(() => setCopiedCodexLoginCommand(false), 1200); + }, [codexLoginCommand]); useEffect(() => { if (!open) return; if (!onClose) return; @@ -493,6 +583,90 @@ export default function WorkbenchDrawer(props: WorkbenchDrawerProps) { window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }, [onClose, open]); + const trustWorkspaceInCodex = useCallback(async () => { + setCodexTrustPending(true); + setCodexTrustError(null); + setCodexTrustSuccess(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 payload = await response.json() as { + ok?: boolean; + error?: string; + trustedPath?: string; + writeEnabled?: boolean; + codexLoggedIn?: boolean; + codexLoginStatus?: 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); + 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)); + } finally { + setCodexTrustPending(false); + } + }, [workspaceIdForTrust]); if (!open) return null; return (