Skip to content
Draft
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
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,17 @@ account exclusion, affinity expiry, or 401/403 and 429 recovery can rebind them.
selection order when one of them — usually your Codex Desktop login — should only be reached for
once the others are drained.

Command Code supports an opt-in OAuth account pool
(`config.commandCodeAccountPool.enabled`):
sticky session affinity, 429 failover, and new-session quota / round-robin / fill-first
picking with per-account 5h + weekly usage bars in the dashboard. The GUI's account
panel shows an **Add account** button that opens the login and also accepts a pasted
redirect URL / authorization code / raw API key (Command Code).
Pool settings are explicit under `commandCodeAccountPool`:
`enabled`, `autoSwitchThreshold` (0–100), `strategy` (`quota`, `round-robin`, or
`fill-first`), and `stickyLimit` (1–100). Command Code also supports
`accountPriorities` (-100–100 per account) and `activeAccountPinned`.

### For agents

```bash
Expand Down
70 changes: 70 additions & 0 deletions gui/src/components/provider-workspace/ProviderAuthPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ export default function ProviderAuthPanel({
const [addingKey, setAddingKey] = useState(false);
const [newKey, setNewKey] = useState("");
const [keyBusy, setKeyBusy] = useState(false);
const [manualCode, setManualCode] = useState("");
const [manualCodeBusy, setManualCodeBusy] = useState(false);
const [manualCodeMsg, setManualCodeMsg] = useState("");
const [manualCodeOk, setManualCodeOk] = useState(true);
const [importBusy, setImportBusy] = useState(false);
const [importStatus, setImportStatus] = useState<"idle" | "invalid" | "failed" | "complete">("idle");
const [importResult, setImportResult] = useState<CockpitImportResult | null>(null);
Expand Down Expand Up @@ -195,6 +199,25 @@ export default function ProviderAuthPanel({
}
};

const submitManualCode = async () => {
const input = manualCode.trim();
if (!input || manualCodeBusy || !authHandlers.onSubmitManualCode) return;
setManualCodeBusy(true);
setManualCodeMsg("");
try {
await authHandlers.onSubmitManualCode(item.name, input);
setManualCode("");
setManualCodeOk(true);
setManualCodeMsg(t("prov.pasteOk"));
} catch (error) {
setManualCodeOk(false);
const message = error instanceof Error && error.message.trim() ? error.message : t("prov.networkError");
setManualCodeMsg(t("prov.pasteFail", { error: message }));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} finally {
setManualCodeBusy(false);
}
};

Comment thread
coderabbitai[bot] marked this conversation as resolved.
const importCockpitFile = async (file: File | undefined) => {
if (!file || importBusy) return;
setImportBusy(true);
Expand Down Expand Up @@ -326,6 +349,53 @@ export default function ProviderAuthPanel({
</div>
)}
<LoginUrlBlock url={hintForThis.url ?? ""} />
{authHandlers.onSubmitManualCode && (
<div className="pwi-auth-paste">
<div className="muted text-label">
{item.name === "command-code"
? t("prov.pasteCommandCodeHint")
: t("prov.pasteRedirectHint")}
</div>
<div style={{ display: "flex", gap: 8 }}>
<input
type={item.name === "command-code" ? "password" : "text"}
autoComplete="off"
spellCheck={false}
value={manualCode}
onChange={e => { setManualCode(e.target.value); setManualCodeMsg(""); }}
onKeyDown={e => {
if (e.key === "Enter" && manualCode.trim()) {
e.preventDefault();
void submitManualCode();
}
}}
placeholder={item.name === "command-code" ? t("prov.pasteCommandCodePlaceholder") : t("prov.pasteRedirect")}
aria-label={item.name === "command-code" ? t("prov.pasteCommandCodePlaceholder") : t("prov.pasteRedirect")}
disabled={manualCodeBusy}
className="input text-label"
style={{ flex: 1 }}
/>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
<button
className="btn btn-ghost"
type="button"
disabled={manualCodeBusy || !manualCode.trim()}
onClick={() => void submitManualCode()}
>
{manualCodeBusy ? t("prov.pasteSubmitting") : t("prov.pasteSubmit")}
</button>
</div>
{manualCodeMsg && (
<div
role={manualCodeOk ? "status" : "alert"}
aria-atomic="true"
className="text-label"
style={{ color: manualCodeOk ? "var(--accent-hover)" : "var(--amber)" }}
>
{manualCodeMsg}
</div>
)}
</div>
)}
{authHandlers.onCancelLogin && (
<button type="button" className="btn btn-ghost btn-sm" onClick={() => void authHandlers.onCancelLogin?.(item.name)}>
{t("common.cancel")}
Expand Down
2 changes: 2 additions & 0 deletions gui/src/components/provider-workspace/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ export type AccountLoadState = "idle" | "loading" | "ready" | "error";
export interface ProviderAuthHandlers {
onLogin: (provider: string, addAccount?: boolean) => void | Promise<void>;
onCancelLogin?: (provider: string) => void;
/** Submit a pasted redirect URL / authorization code / raw API key to an in-progress login. */
onSubmitManualCode?: (provider: string, input: string) => void | Promise<void>;
onLogout: (provider: string) => void | Promise<void>;
onReauth: (provider: string, accountId?: string) => void | Promise<void>;
onSwitchAccount: (provider: string, account: OAuthAccountRow) => void | Promise<void>;
Expand Down
4 changes: 3 additions & 1 deletion gui/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,9 @@ export const de: Record<TKey, string> = {
"prov.aliasSaveFailed": "Alias konnte nicht gespeichert werden",
"prov.accountId": "ID",
"prov.pasteRedirect": "Redirect-URL oder Code einfügen",
"prov.pasteRedirectHint": "Zeigt der Browser einen localhost-Fehler, kopiere die vollständige URL aus der Adressleiste und füge sie hier ein (oder den Autorisierungscode).",
"prov.pasteRedirectHint": "Falls der Browser einen localhost-Fehler zeigt, kopieren Sie die vollständige URL aus der Adressleiste und fügen Sie sie hier ein (oder fügen Sie den Autorisierungscode ein).",
"prov.pasteCommandCodePlaceholder": "Command-Code-API-Schlüssel oder Redirect-URL einfügen",
"prov.pasteCommandCodeHint": "Fügen Sie einen Command-Code-API-Schlüssel (user_… aus ~/.commandcode/auth.json) ein, um ihn als weiteres Konto hinzuzufügen. Alternativ können Sie die Redirect-URL aus dem Browser einfügen.",
"prov.pasteSubmit": "Senden",
"prov.pasteSubmitting": "Wird gesendet…",
"prov.pasteOk": "Code gesendet — Anmeldung wird abgeschlossen…",
Expand Down
2 changes: 2 additions & 0 deletions gui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,8 @@ export const en = {
"prov.accountId": "ID",
"prov.pasteRedirect": "Paste redirect URL or code",
"prov.pasteRedirectHint": "If the browser shows a localhost error, copy the full URL from its address bar and paste it here (or paste the authorization code).",
"prov.pasteCommandCodePlaceholder": "Paste Command Code API key or redirect URL",
"prov.pasteCommandCodeHint": "Paste a Command Code API key (user_… from ~/.commandcode/auth.json) to add it as another account, or the redirect URL from the browser.",
"prov.pasteSubmit": "Submit",
"prov.pasteSubmitting": "Submitting…",
"prov.pasteOk": "Code submitted — finishing login…",
Expand Down
4 changes: 3 additions & 1 deletion gui/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,9 @@ export const ja: Record<TKey, string> = {
"prov.copyCode": "コードをコピー",
"prov.codeCopied": "コードをコピーしました",
"prov.pasteRedirect": "リダイレクト URL またはコードを貼り付け",
"prov.pasteRedirectHint": "ブラウザに localhost エラーが表示された場合、アドレスバーから URL 全体をコピーしてここに貼り付けてください(または認可コードを貼り付け)。",
"prov.pasteRedirectHint": "ブラウザに localhost エラーが表示された場合は、アドレスバーの完全な URL をコピーしてここに貼り付けてください(認可コードも可)。",
"prov.pasteCommandCodePlaceholder": "Command Code API キーまたはリダイレクト URL を貼り付け",
"prov.pasteCommandCodeHint": "Command Code API キー(~/.commandcode/auth.json の user_…)を貼り付けると別のアカウントとして追加されます。またはブラウザのリダイレクト URL を貼り付けます。",
"prov.pasteSubmit": "送信",
"prov.pasteSubmitting": "送信中…",
"prov.pasteOk": "コードを送信しました — ログインを完了しています…",
Expand Down
4 changes: 3 additions & 1 deletion gui/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,9 @@ export const ko: Record<TKey, string> = {
"prov.aliasSaveFailed": "별칭을 저장하지 못했습니다",
"prov.accountId": "ID",
"prov.pasteRedirect": "리다이렉트 URL 또는 코드 붙여넣기",
"prov.pasteRedirectHint": "브라우저에 localhost 오류가 표시되면, 주소창의 전체 URL을 복사해 여기에 붙여넣으세요(또는 인증 코드 붙여넣기).",
"prov.pasteRedirectHint": "브라우저에 localhost 오류가 표시되면 주소 표시줄의 전체 URL을 복사하여 여기에 붙여넣으세요(또는 인증 코드를 붙여넣으세요).",
"prov.pasteCommandCodePlaceholder": "Command Code API 키 또는 리다이렉트 URL 붙여넣기",
"prov.pasteCommandCodeHint": "Command Code API 키(~/.commandcode/auth.json의 user_…)를 붙여넣어 다른 계정으로 추가하거나 브라우저의 리다이렉트 URL을 붙여넣으세요.",
"prov.pasteSubmit": "제출",
"prov.pasteSubmitting": "제출 중…",
"prov.pasteOk": "코드를 제출했습니다 — 로그인 완료 중…",
Expand Down
4 changes: 3 additions & 1 deletion gui/src/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,9 @@ export const ru: Record<TKey, string> = {
"prov.aliasSaveFailed": "Не удалось сохранить псевдоним",
"prov.accountId": "ID",
"prov.pasteRedirect": "Вставьте URL перенаправления или код",
"prov.pasteRedirectHint": "Если браузер показывает ошибку localhost, скопируйте полный URL из его адресной строки и вставьте сюда (или вставьте код авторизации).",
"prov.pasteRedirectHint": "Если браузер показывает ошибку localhost, скопируйте полный URL из адресной строки и вставьте его сюда (или вставьте код авторизации).",
"prov.pasteCommandCodePlaceholder": "Вставьте API-ключ Command Code или URL перенаправления",
"prov.pasteCommandCodeHint": "Вставьте API-ключ Command Code (user_… из ~/.commandcode/auth.json), чтобы добавить его как ещё один аккаунт, или URL перенаправления из браузера.",
"prov.pasteSubmit": "Отправить",
"prov.pasteSubmitting": "Отправка…",
"prov.pasteOk": "Код отправлен — завершаем вход…",
Expand Down
4 changes: 3 additions & 1 deletion gui/src/i18n/tr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,9 @@ export const tr: Record<TKey, string> = {
"prov.aliasSaveFailed": "Takma ad kaydedilemedi",
"prov.accountId": "ID",
"prov.pasteRedirect": "Yönlendirme URL'sini veya kodu yapıştırın",
"prov.pasteRedirectHint": "Tarayıcı localhost hatası gösterirse adres çubuğundaki URL'yi kopyalayıp buraya yapıştırın.",
"prov.pasteRedirectHint": "Tarayıcı bir localhost hatası gösteriyorsa, adres çubuğundaki tam URL'yi kopyalayıp buraya yapıştırın (veya yetkilendirme kodunu yapıştırın).",
"prov.pasteCommandCodePlaceholder": "Command Code API anahtarını veya yönlendirme URL'sini yapıştırın",
"prov.pasteCommandCodeHint": "Başka bir hesap olarak eklemek için bir Command Code API anahtarı (user_…, ~/.commandcode/auth.json) veya tarayıcıdan yönlendirme URL'sini yapıştırın.",
"prov.pasteSubmit": "Gönder",
"prov.pasteSubmitting": "Gönderiliyor…",
"prov.pasteOk": "Kod gönderildi — giriş tamamlanıyor…",
Expand Down
4 changes: 3 additions & 1 deletion gui/src/i18n/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,9 @@ export const zhTW: Record<TKey, string> = {
"prov.aliasSaveFailed": "無法儲存別名",
"prov.accountId": "ID",
"prov.pasteRedirect": "貼上重定向 URL 或授權碼",
"prov.pasteRedirectHint": "如果瀏覽器顯示 localhost 錯誤,請複製位址列中的完整 URL 並貼上到此處(或貼上授權碼)。",
"prov.pasteRedirectHint": "如果瀏覽器顯示 localhost 錯誤,請複製網址列的完整 URL 並貼到此處(或貼上授權碼)。",
"prov.pasteCommandCodePlaceholder": "貼上 Command Code API 金鑰或重新導向 URL",
"prov.pasteCommandCodeHint": "貼上 Command Code API 金鑰(~/.commandcode/auth.json 中的 user_…)以新增為另一個帳戶,或貼上瀏覽器中的重新導向 URL。",
"prov.pasteSubmit": "提交",
"prov.pasteSubmitting": "提交中…",
"prov.pasteOk": "已提交授權碼 — 正在完成登入…",
Expand Down
4 changes: 3 additions & 1 deletion gui/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,9 @@ export const zh: Record<TKey, string> = {
"prov.aliasSaveFailed": "无法保存别名",
"prov.accountId": "ID",
"prov.pasteRedirect": "粘贴重定向 URL 或授权码",
"prov.pasteRedirectHint": "如果浏览器显示 localhost 错误,请复制地址栏中的完整 URL 并粘贴到此处(或粘贴授权码)。",
"prov.pasteRedirectHint": "如果浏览器显示 localhost 错误,请复制地址栏中的完整 URL 并粘贴到这里(或粘贴授权码)。",
"prov.pasteCommandCodePlaceholder": "粘贴 Command Code API 密钥或重定向 URL",
"prov.pasteCommandCodeHint": "粘贴 Command Code API 密钥(~/.commandcode/auth.json 中的 user_…)以添加为另一个账户,或粘贴浏览器中的重定向 URL。",
"prov.pasteSubmit": "提交",
"prov.pasteSubmitting": "提交中…",
"prov.pasteOk": "已提交代码 — 正在完成登录…",
Expand Down
3 changes: 2 additions & 1 deletion gui/src/pages/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ export default function Providers({ apiBase }: { apiBase: string }) {

const bumpModelsRefresh = () => setModelsRefreshToken(n => n + 1);

const { cancelLoginOAuth, loginOAuth, logoutOAuth } = useProvidersOAuth({
const { cancelLoginOAuth, loginOAuth, logoutOAuth, submitManualCode } = useProvidersOAuth({
apiBase, t, aliveRef, accountSets, setAccountSets,
setBusy, setStatus, setLoginInfo, setOauthStatus, notify,
fetchConfig, fetchOauth, fetchAccountSets, fetchProviderQuotas, bumpModelsRefresh,
Expand Down Expand Up @@ -367,6 +367,7 @@ export default function Providers({ apiBase }: { apiBase: string }) {
authHandlers={{
onLogin: requestLoginOAuth,
onCancelLogin: cancelLoginOAuth,
onSubmitManualCode: submitManualCode,
onLogout: logoutOAuth,
onReauth: (provider, accountId) => loginOAuth(provider, true, accountId),
onSwitchAccount: switchAccount,
Expand Down
21 changes: 20 additions & 1 deletion gui/src/pages/use-providers-oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,5 +211,24 @@ export function useProvidersOAuth({
}
};

return { cancelLoginOAuth, loginOAuth, logoutOAuth };
const submitManualCode = async (provider: string, input: string) => {
try {
const res = await fetch(`${apiBase}/api/oauth/login/code`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ provider, input }),
});
if (!aliveRef.current) return;
if (!res.ok) {
const data = await res.json().catch(() => ({})) as { error?: string };
throw new Error(data.error || res.statusText);
}
} catch (error) {
if (aliveRef.current) {
throw error instanceof Error ? error : new Error(String(error));
}
}
};

return { cancelLoginOAuth, loginOAuth, logoutOAuth, submitManualCode };
}
1 change: 1 addition & 0 deletions gui/src/styles/provider-workspace-settings.css
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
.pwi-auth-wait-title { font-size: var(--text-control); font-weight: 600; color: var(--text); }
.pwi-auth-wait-copy { font-size: var(--text-label); color: var(--muted); line-height: 1.45; }
.pwi-device-code-wrap { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 12px; margin: 6px 0; border: 1px solid var(--border); border-radius: 10px; background: var(--surface); }
.pwi-auth-paste { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; }
.pwi-device-code { font-size: 20px; font-weight: 800; letter-spacing: .14em; color: var(--text); user-select: all; }
.pwi-auth-row-copy { min-width: 0; display: flex; flex-direction: column; align-items: flex-start; gap: 2px; }
.pwi-auth-row-secondary { max-width: 100%; overflow: hidden; text-overflow: ellipsis; font-size: var(--text-label); color: var(--muted); }
Expand Down
Loading
Loading