const settingsBase = document.body.dataset.basePath;
async function settingsRpc(method, params) {
const response = await fetch(settingsBase + "/api/rpc", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ method, params: params || {} }),
});
const envelope = await response.json();
if (!response.ok || envelope.error) {
throw new Error(envelope.error?.message || envelope.error || "Request failed");
}
return envelope.result;
}
function setCodexTheme(theme) {
const value = ["light", "dark", "system"].includes(theme) ? theme : "dark";
localStorage.setItem("codex-web-theme", value);
document.documentElement.classList.toggle("light", value === "light");
document.documentElement.classList.toggle(
"dark",
value === "dark" || (value === "system" && matchMedia("(prefers-color-scheme: dark)").matches),
);
}
function openCodexModal(id) {
const modal = document.getElementById(id);
if (!modal) return;
modal.classList.remove("hidden");
modal.classList.add("grid");
}
function closeCodexModal(id) {
const modal = document.getElementById(id);
modal?.classList.add("hidden");
modal?.classList.remove("grid");
}
function accountMessage(message, error = false) {
const node = document.querySelector("[data-account-message]");
if (!node) return;
node.textContent = message;
node.classList.remove("hidden", "text-destructive", "text-muted-foreground");
node.classList.add(error ? "text-destructive" : "text-muted-foreground");
}
async function updateAccountProfiles(payload) {
const response = await fetch(settingsBase + "/api/account-profiles", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const result = await response.json();
if (!response.ok || result.error) throw new Error(result.error || "Could not update account profiles");
location.reload();
}
setCodexTheme(localStorage.getItem("codex-web-theme") || "dark");
globalThis.codexWebSettings = { setTheme: setCodexTheme, openModal: openCodexModal, closeModal: closeCodexModal };
const settingsModal = document.querySelector("#settings-modal");
document.querySelector("#settings-button")?.addEventListener("click", () => openCodexModal("settings-modal"));
document.querySelectorAll("[data-close-modal]").forEach((button) => {
button.addEventListener("click", () => closeCodexModal(button.dataset.closeModal));
});
settingsModal?.addEventListener("click", (event) => {
if (event.target === settingsModal) closeCodexModal("settings-modal");
});
const settingsTheme = document.querySelector("#settings-theme");
if (settingsTheme) {
settingsTheme.value = localStorage.getItem("codex-web-theme") || "dark";
settingsTheme.addEventListener("change", () => setCodexTheme(settingsTheme.value));
}
document.querySelector("[data-account-login]")?.addEventListener("click", async () => {
try {
const result = await settingsRpc("account/login/start", { type: "chatgptDeviceCode" });
accountMessage(`Enter ${result.userCode} in the ChatGPT window. This page will update when sign-in completes.`);
window.open(result.verificationUrl, "_blank", "noopener,noreferrer");
const poll = setInterval(async () => {
try {
if ((await settingsRpc("account/read", {})).account) {
clearInterval(poll);
location.reload();
}
} catch {}
}, 1500);
setTimeout(() => clearInterval(poll), 120000);
} catch (error) {
accountMessage(error.message, true);
}
});
document.querySelector("[data-account-api-key]")?.addEventListener("click", async () => {
const apiKey = window.prompt("OpenAI API key");
if (!apiKey) return;
try {
await settingsRpc("account/login/start", { type: "apiKey", apiKey });
accountMessage("API key saved for the active profile.");
} catch (error) {
accountMessage(error.message, true);
}
});
document.querySelector("[data-account-logout]")?.addEventListener("click", async () => {
if (!confirm("Sign out of the active Codex account?")) return;
try {
await settingsRpc("account/logout", {});
accountMessage("Signed out. You can now connect another account.");
} catch (error) {
accountMessage(error.message, true);
}
});
document.querySelector("[data-account-profile-form]")?.addEventListener("submit", (event) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
void updateAccountProfiles({
action: "add",
name: data.get("name"),
label: data.get("label"),
codexHome: data.get("codexHome"),
proxy: data.get("proxy") || null,
useSshTunnel: data.get("useSshTunnel") === "on",
}).catch((error) => accountMessage(error.message, true));
});
settingsModal?.addEventListener("click", (event) => {
const move = event.target.closest("[data-account-move]");
if (move) {
void updateAccountProfiles({ action: "move", name: move.dataset.accountMove, direction: move.dataset.direction })
.catch((error) => accountMessage(error.message, true));
return;
}
const remove = event.target.closest("[data-account-remove]");
if (remove && confirm(`Remove the ${remove.dataset.accountRemove} profile?`)) {
void updateAccountProfiles({ action: "remove", name: remove.dataset.accountRemove })
.catch((error) => accountMessage(error.message, true));
}
});
settingsModal?.addEventListener("click", async (event) => {
const button = event.target.closest("[data-mcp-login]");
if (!button) return;
const loginWindow = window.open("", "_blank");
button.disabled = true;
button.textContent = "Opening…";
try {
const threadId = document.body.dataset.threadId || null;
const result = await settingsRpc("mcpServer/oauth/login", { name: button.dataset.mcpLogin, threadId });
const authorizationUrl = result.authorizationUrl || result.authorization_url;
if (!authorizationUrl) throw new Error("MCP login did not return an authorization URL");
if (loginWindow) loginWindow.location.replace(authorizationUrl);
else location.assign(authorizationUrl);
button.textContent = "Waiting…";
} catch (error) {
loginWindow?.close();
button.disabled = false;
button.textContent = "Retry";
accountMessage(error.message, true);
}
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && !settingsModal?.classList.contains("hidden")) closeCodexModal("settings-modal");
});