Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs-site/src/content/docs/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ Tokens stay in `~/.opencodex/auth.json`; `/api/oauth/accounts` returns masked me

### Cockpit Tools Antigravity import

For v1, OpenCodex imports only a **Cockpit Tools Antigravity** JSON export for the `google-antigravity` provider. In the Providers dashboard, choose the local JSON file from that provider's Accounts tab. The dashboard does not show the file contents or credential values; it reports only imported, updated, failed, and unsupported counts. Other Cockpit providers are rejected in v1.
For v1, OpenCodex imports only a **Cockpit Tools Antigravity** JSON export for the `google-antigravity` provider. In the Providers dashboard, open that provider's Accounts tab and choose **Log in** or **Add account**: the dialog offers browser login or a Cockpit Tools import from a local JSON file, the clipboard, or pasted text. The dashboard does not show the file contents or credential values; it reports only imported, updated, failed, and unsupported counts. Other Cockpit providers are rejected in v1.

The CLI accepts the export from a file or standard input only — never paste it into a command argument:

Expand Down
2 changes: 1 addition & 1 deletion docs-site/src/content/docs/guides/web-dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ is visible) and never forces an upstream refresh.
| **Startup safety** | Show whether injected Codex routing survives a restart, with separate service and launcher-shim health plus exact repair commands. |
| **Windows tray** | Install a per-user login tray for one-click proxy start, stop, restart, dashboard access, and status. The tray is a controller, not a proxy restart service. |
| **Codex autostart** | Allow an already-installed Codex launcher shim to run `ocx ensure`. This toggle does not install a shim or background service. |
| **Providers** | Add, edit, set the default (enabled providers only), enable/disable, and remove providers; manage OAuth account pools and API-key pools where supported. Removing the current default switches to the first remaining enabled provider when one exists; otherwise deletion is refused and the current default is kept. Provider Settings can disable live model discovery for endpoints with missing, slow, or oversized `/models` catalogs. For Claude (Anthropic) OAuth pools, each logged-in account shows its own 5-hour and weekly rate-limit bars (usage is per credential); a failed probe keeps the last-known bars and marks them unavailable until the next successful refresh. Other OAuth providers with a logged-in account (Google Antigravity, xAI, Kiro, Cursor and the rest) show an **Account pool** card. Its switch controls the pre-dispatch account preference; a 429 still fails over either way. The strategy is marked **Saved, not live** and cannot be changed there until `pool.kernel` is on, and a strategy set only from the CLI, such as Kiro's `least-loaded`, is shown by name and kept when you toggle the pool. The Provider Overview shown when no provider is selected carries a **Refresh all quotas** control that forces one server-side re-read of every configured provider; a provider whose upstream probe fails keeps its last-good row, so the status line reports that the check completed rather than claiming every value is fresh, and each row's own age stays the per-provider freshness signal. |
| **Providers** | Add, edit, set the default (enabled providers only), enable/disable, and remove providers; manage OAuth account pools and API-key pools where supported. Removing the current default switches to the first remaining enabled provider when one exists; otherwise deletion is refused and the current default is kept. Provider Settings can disable live model discovery for endpoints with missing, slow, or oversized `/models` catalogs. OAuth account lists show each account as a card (or a compact row) with search, quota filters, sorting, a login/hidden/alias title switch, and one cached quota refresh per tab. Accounts without quota readings display as unknown and do not count as available; estimated remaining tokens are omitted. Logout remains next to the signed-in status; removing an account asks for confirmation in a dialog. The list rereads cached quota every 30 seconds while the dashboard is visible. For Claude (Anthropic) OAuth pools, each logged-in account shows its own 5-hour and weekly rate-limit bars (usage is per credential); a failed probe keeps the last-known bars and marks them unavailable until the next successful refresh. Other OAuth providers with a logged-in account (Google Antigravity, xAI, Kiro, Cursor and the rest) show an **Account pool** card. Its switch controls the pre-dispatch account preference; a 429 still fails over either way. The strategy is marked **Saved, not live** and cannot be changed there until `pool.kernel` is on, and a strategy set only from the CLI, such as Kiro's `least-loaded`, is shown by name and kept when you toggle the pool. The Provider Overview shown when no provider is selected carries a **Refresh all quotas** control that forces one server-side re-read of every configured provider; a provider whose upstream probe fails keeps its last-good row, so the status line reports that the check completed rather than claiming every value is fresh, and each row's own age stays the per-provider freshness signal. |
| **Add provider** | One search above the tabs reaches all four at once — Accounts, Free, Local, Paid. While a query is live the results are grouped by tab with a count each, and the selected tab stays put rather than jumping. Press ArrowDown in the search box to focus the first available result action, skipping disabled buttons; if no action is available, focus stays in the search box. Local runtimes (Ollama, vLLM, LM Studio, LiteLLM) have their own tab, and a long provider note clamps to two lines with the full text one click away. Claude Code CLI (subscription) sits under Paid with a Subscription CLI badge, and selecting it warns that it bills the signed-in Claude subscription through `claude -p` and never uses an API key (use `anthropic-apikey` for key billing). |
| **Codex Auth** | Add ChatGPT/Codex pool accounts, select the next-session account, refresh 5h / weekly / 30d quotas, enable or disable quota auto-switch, set its 1–100% threshold, and configure transient-failure failover. |
| **Subagents** | Feature up to five bare native or namespaced routed models in the `spawn_agent` override list. |
Expand Down
2 changes: 1 addition & 1 deletion docs-site/src/content/docs/reference/management-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -593,7 +593,7 @@ Labels are read on login/import or refresh, never inferred from quota percentage
| `POST /api/oauth/login/cancel` | Cancel a public in-progress OAuth flow | 400 unknown provider |
| `GET /api/oauth/status` | Poll one provider's OAuth flow | 400 unknown provider |
| `POST /api/oauth/logout` | Remove the selected provider credential | 400 unknown provider; `oauth_mutation_busy` |
| `GET /api/oauth/accounts` | List masked accounts; Anthropic and generic OAuth account rows include their `paused` state. Kiro rows include `autoSelectable` and a closed `skipReason` when excluded from automatic selection; an active singleton may still send. Quota remains opt-in. | 400 invalid provider |
| `GET /api/oauth/accounts` | List masked accounts; Anthropic and generic OAuth account rows include their `paused` state. Kiro rows include `autoSelectable` and a closed `skipReason` when excluded from automatic selection; an active singleton may still send. Quota remains opt-in (`quota=1`); `refresh=1` forces upstream probes, and adding `accountId=<id>` forces only that account while the rest are served from cache. An id that matches no account forces every account. | 400 invalid provider |
| `DELETE /api/oauth/accounts` | Remove one account | 400 invalid provider/id; 404 account missing; `oauth_mutation_busy` |
| `PUT /api/oauth/accounts/active` | Select the active OAuth account | 400 invalid provider/account; 404 account missing; 409 account paused; `oauth_mutation_busy` |
| `PUT /api/oauth/accounts/pause` | Pause or resume one Anthropic or generic OAuth account. Body `{ provider, accountId, paused }`; pausing the active account selects the next usable account when available. Pause is durable and independent of pool enablement; resume preserves health and credentials. | 400 unsupported provider or invalid body; 404 account missing; `oauth_mutation_busy` |
Expand Down
Binary file added gui/public/provider-icons/cockpit-tools.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion gui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -472,8 +472,10 @@ export default function App() {
navigateToPage(group.pages[0]);
setNavOpen(false);
}}
title={t(tkey)}
aria-label={t(tkey)}
aria-current={active ? "page" : undefined}>
<Icon /> {t(tkey)}
<Icon /> <span className="nav-item-text">{t(tkey)}</span>
</button>
</div>
);
Expand Down
123 changes: 123 additions & 0 deletions gui/src/components/AccountAuthChoiceModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { useEffect, useRef } from "react";
import { IconX } from "../icons";
import { useT } from "../i18n/shared";
import { ProviderIcon } from "./provider-workspace/ProviderRail";
import CockpitToolsCard from "./provider-workspace/CockpitToolsCard";

export interface AccountAuthChoiceModalProps {
provider: string;
providerLabel: string;
apiBase: string;
isOpen: boolean;
isBusy?: boolean;
onClose: () => void;
onContinueOAuth: () => void;
onImportSuccess?: () => void;
}

export default function AccountAuthChoiceModal({
provider,
providerLabel,
apiBase,
isOpen,
isBusy = false,
onClose,
onContinueOAuth,
onImportSuccess,
}: AccountAuthChoiceModalProps) {
const t = useT();
const dialog = useRef<HTMLDivElement>(null);
const primary = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (!isOpen) return;
const previous = document.activeElement as HTMLElement | null;
primary.current?.focus();
// Deferred: React is still removing the dialog during cleanup and resets focus to the
// body afterwards, which would discard a synchronous restoration.
return () => { requestAnimationFrame(() => { if (previous?.isConnected && typeof previous.focus === "function") previous.focus(); }); };
}, [isOpen]);
useEffect(() => {
if (!isOpen) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
e.preventDefault();
onClose();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [isOpen, onClose]);

if (!isOpen) return null;

return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="auth-choice-modal-title"
className="modal-overlay"
onClick={onClose}
onKeyDown={event => {
if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); onClose(); }
if (event.key !== "Tab") return;
const focusables = [...(dialog.current?.querySelectorAll<HTMLElement>("button:not([disabled]), input:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex='-1'])") ?? [])].filter(el => el.offsetParent !== null);
const buttons = focusables as HTMLButtonElement[];
const first = buttons?.[0], last = buttons?.[buttons.length - 1];
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last?.focus(); }
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first?.focus(); }
}}
>
<div className="modal-card auth-choice-modal" ref={dialog} onClick={e => e.stopPropagation()}>
<div className="modal-head">
<div>
<h3 id="auth-choice-modal-title">
{t("pws.authChoiceModalTitle", { provider: providerLabel })}
</h3>
<p className="muted text-label" style={{ marginTop: 2 }}>
{t("pws.authChoiceModalSubtitle")}
</p>
</div>
<button
type="button"
className="btn btn-ghost btn-icon"
aria-label={t("common.close")}
onClick={onClose}
>
<IconX />
</button>
</div>

<div className="auth-choice-body">
{/* Method 1: Browser OAuth */}
<div className="auth-choice-card">
<div className="auth-choice-card-info">
<ProviderIcon name={provider} cls="provider-icon provider-icon-sm" />
<div className="auth-choice-card-text">
<span className="auth-choice-card-title">{t("pws.antigravityOauthTitle")}</span>
<span className="auth-choice-card-desc">{t("pws.antigravityOauthDesc")}</span>
</div>
</div>
<button
type="button"
className="btn btn-primary btn-sm"
ref={primary}
disabled={isBusy}
onClick={() => {
onClose();
onContinueOAuth();
}}
>
{isBusy ? t("prov.waitingBrowser") : t("pws.antigravityOauthAction")}
</button>
</div>

{/* Method 2: Cockpit Tools Card */}
<CockpitToolsCard
apiBase={apiBase}
onImportSuccess={onImportSuccess}
/>
</div>
</div>
</div>
);
}
18 changes: 18 additions & 0 deletions gui/src/components/AntigravityChoiceModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useT } from "../i18n/shared";
import AccountAuthChoiceModal, { type AccountAuthChoiceModalProps } from "./AccountAuthChoiceModal";

export type AntigravityChoiceModalProps = Omit<AccountAuthChoiceModalProps, "provider" | "providerLabel"> & {
provider?: string;
providerLabel?: string;
};

export default function AntigravityChoiceModal(props: AntigravityChoiceModalProps) {
const t = useT();
return (
<AccountAuthChoiceModal
provider={props.provider ?? "google-antigravity"}
providerLabel={props.providerLabel ?? t("pws.providerLabelAntigravity")}
{...props}
/>
);
}
Loading
Loading