typeduck-codex-web 0.5.0

A standalone browser interface and Codex runtime
Documentation
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");
});