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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
108 changes: 107 additions & 1 deletion gui/src/components/provider-workspace/ProviderAuthPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,12 +187,44 @@ 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);
const [reserveQuotaSlots, setReserveQuotaSlots] = useState(false);
const importFileRef = useRef<HTMLInputElement>(null);
const deviceCodeCopy = useCopyFeedback<string>();
const manualFlowKey = loginHint?.provider === item.name
? `${loginHint.provider}\0${loginHint.url ?? ""}\0${loginHint.deviceCode ?? ""}\0${loginHint.instructions ?? ""}\0${loginHint.attemptId ?? ""}`
: "";
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const [seenManualFlowKey, setSeenManualFlowKey] = useState(manualFlowKey);
const manualFlowKeyRef = useRef(manualFlowKey);
const mountedRef = useRef(false);

const resetManualCode = () => {
setManualCode("");
setManualCodeMsg("");
setManualCodeOk(true);
};

if (manualFlowKey !== seenManualFlowKey) {
setSeenManualFlowKey(manualFlowKey);
setManualCode("");
setManualCodeBusy(false);
setManualCodeMsg("");
setManualCodeOk(true);
}

useEffect(() => {
mountedRef.current = true;
manualFlowKeyRef.current = manualFlowKey;
return () => {
mountedRef.current = false;
};
}, [manualFlowKey]);

// Soft &quota=1 enrichment lands after the local account list. Reserve stacked
// bar height briefly so bars don't shove rows when WHAM returns.
Expand Down Expand Up @@ -220,6 +252,7 @@ export default function ProviderAuthPanel({
const surface = providerAuthSurface({ ...item, hasApiKey: item.hasApiKey || keys.length > 0 });
const isOauth = surface === "oauth-accounts";
const isKeyAuth = surface === "api-keys";
const isCommandCodeAuth = item.adapter === "command-code" && item.authMode === "oauth";

if (surface === "codex-accounts") {
return (
Expand Down Expand Up @@ -263,6 +296,31 @@ export default function ProviderAuthPanel({
}
};

const submitManualCode = async () => {
const input = manualCode.trim();
if (!input || manualCodeBusy || !authHandlers.onSubmitManualCode) return;
const submittedFlowKey = manualFlowKeyRef.current;
setManualCodeBusy(true);
setManualCodeMsg("");
try {
const outcome = await authHandlers.onSubmitManualCode(item.name, input);
if (outcome !== "submitted") return;
if (!mountedRef.current || manualFlowKeyRef.current !== submittedFlowKey) return;
setManualCode("");
setManualCodeOk(true);
setManualCodeMsg(t("prov.pasteOk"));
} catch (error) {
if (!mountedRef.current || manualFlowKeyRef.current !== submittedFlowKey) return;
setManualCodeOk(false);
const message = error instanceof Error && error.message.trim() ? error.message : t("prov.networkError");
setManualCodeMsg(t("prov.pasteFail", { error: message }));
} finally {
if (mountedRef.current && manualFlowKeyRef.current === submittedFlowKey) {
setManualCodeBusy(false);
}
}
};

const importCockpitFile = async (file: File | undefined) => {
if (!file || importBusy) return;
setImportBusy(true);
Expand Down Expand Up @@ -400,8 +458,56 @@ export default function ProviderAuthPanel({
</div>
)}
<LoginUrlBlock url={hintForThis.url ?? ""} />
{authHandlers.onSubmitManualCode && (
<div className="pwi-auth-paste">
<div className="muted text-label">
{isCommandCodeAuth
? t("prov.pasteCommandCodeHint")
: t("prov.pasteRedirectHint")}
</div>
<div style={{ display: "flex", gap: 8 }}>
<input
type={isCommandCodeAuth ? "password" : "text"}
autoComplete="off"
spellCheck={false}
maxLength={4096}
value={manualCode}
onChange={e => { setManualCode(e.target.value); setManualCodeMsg(""); }}
onKeyDown={e => {
if (e.key === "Enter" && manualCode.trim()) {
e.preventDefault();
void submitManualCode();
}
}}
placeholder={isCommandCodeAuth ? t("prov.pasteCommandCodePlaceholder") : t("prov.pasteRedirect")}
aria-label={isCommandCodeAuth ? t("prov.pasteCommandCodePlaceholder") : t("prov.pasteRedirect")}
disabled={manualCodeBusy}
className="input text-label"
style={{ flex: 1 }}
/>
<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)}>
<button type="button" className="btn btn-ghost btn-sm" onClick={() => { resetManualCode(); void authHandlers.onCancelLogin?.(item.name); }}>
{t("common.cancel")}
</button>
)}
Expand Down
3 changes: 3 additions & 0 deletions gui/src/components/provider-workspace/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,16 @@ export type LoginHint = {
url?: string;
instructions?: string;
deviceCode?: string;
attemptId?: string;
};

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) => Promise<"submitted" | "cancelled">;
onLogout: (provider: string) => void | Promise<void>;
onReauth: (provider: string, accountId?: string) => void | Promise<void>;
onSwitchAccount: (provider: string, account: OAuthAccountRow) => void | Promise<void>;
Expand Down
12 changes: 11 additions & 1 deletion gui/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,10 +379,20 @@ 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, Autorisierungscode oder Redirect-URL einfügen",
"prov.pasteCommandCodeHint": "Fügen Sie einen Command-Code-API-Schlüssel (user_… aus ~/.commandcode/auth.json), einen Autorisierungscode oder die Redirect-URL aus dem Browser ein, um ihn als weiteres Konto hinzuzufügen.",
"prov.pasteSubmit": "Senden",
"prov.pasteSubmitting": "Wird gesendet…",
"prov.pasteOk": "Code gesendet — Anmeldung wird abgeschlossen…",
"prov.manualErrorEmpty": "Geben Sie eine Redirect-URL, einen Autorisierungscode oder einen API-Schlüssel ein.",
"prov.manualErrorTooLarge": "Eingefügter Inhalt ist zu groß.",
"prov.manualErrorNoLogin": "Kein Login aktiv. Starten Sie „Konto hinzufügen“ erneut.",
"prov.manualErrorStale": "Dieses Einfügen gehört zu einem früheren Login. Starten Sie „Konto hinzufügen“ erneut und fügen Sie die neue URL bzw. den neuen Code ein.",
"prov.manualErrorNoCode": "Kein Autorisierungscode im eingefügten Inhalt gefunden.",
"prov.manualErrorMissingState": "Der Redirect-URL fehlt der State-Parameter. Fügen Sie die vollständige URL dieses Logins ein.",
"prov.manualErrorStateMismatch": "State stimmt nicht überein — fügen Sie die Redirect-URL dieses Logins ein.",
"prov.manualErrorInvalid": "Code konnte nicht gesendet werden. Prüfen Sie den eingefügten Wert und versuchen Sie es erneut.",
"prov.pasteFail": "Code konnte nicht gesendet werden: {error}",
"prov.port": "Port",
"prov.default": "Standard",
Expand Down
10 changes: 10 additions & 0 deletions gui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,9 +403,19 @@ 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, authorization code, or redirect URL",
"prov.pasteCommandCodeHint": "Paste a Command Code API key (user_… from ~/.commandcode/auth.json), an authorization code, or the redirect URL from the browser to add it as another account.",
"prov.pasteSubmit": "Submit",
"prov.pasteSubmitting": "Submitting…",
"prov.pasteOk": "Code submitted — finishing login…",
"prov.manualErrorEmpty": "Enter a redirect URL, authorization code, or API key.",
"prov.manualErrorTooLarge": "Pasted content is too large.",
"prov.manualErrorNoLogin": "No login in progress. Start Add account again.",
"prov.manualErrorStale": "This paste belongs to a previous login. Start Add account again and paste the new URL or code.",
"prov.manualErrorNoCode": "No authorization code found in the pasted content.",
"prov.manualErrorMissingState": "Redirect URL is missing the state parameter. Paste the full URL from this login attempt.",
"prov.manualErrorStateMismatch": "State mismatch — paste the redirect URL from this login attempt.",
"prov.manualErrorInvalid": "Could not submit the code. Check the pasted value and try again.",
"prov.pasteFail": "Could not submit code: {error}",
"prov.port": "Port",
"prov.default": "Default",
Expand Down
10 changes: 10 additions & 0 deletions gui/src/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,9 +390,19 @@ export const fr: Record<TKey, string> = {
"prov.accountId": "ID",
"prov.pasteRedirect": "Coller l’URL de redirection ou le code",
"prov.pasteRedirectHint": "Si le navigateur affiche une erreur localhost, copiez l’URL complète depuis sa barre d’adresse et collez-la ici (ou collez le code d’autorisation).",
"prov.pasteCommandCodePlaceholder": "Coller la clé API Command Code, le code d'autorisation ou l’URL de redirection",
"prov.pasteCommandCodeHint": "Collez une clé API Command Code (user_… depuis ~/.commandcode/auth.json), un code d’autorisation ou l’URL de redirection du navigateur pour l’ajouter comme autre compte.",
"prov.pasteSubmit": "Envoyer",
"prov.pasteSubmitting": "Envoi…",
"prov.pasteOk": "Code envoyé — finalisation de la connexion…",
"prov.manualErrorEmpty": "Saisissez une URL de redirection, un code d'autorisation ou une clé API.",
"prov.manualErrorTooLarge": "Le contenu collé est trop volumineux.",
"prov.manualErrorNoLogin": "Aucune connexion en cours. Relancez Ajouter un compte.",
"prov.manualErrorStale": "Ce collage appartient à une tentative précédente. Relancez Ajouter un compte et collez la nouvelle URL ou le nouveau code.",
"prov.manualErrorNoCode": "Aucun code d'autorisation trouvé dans le contenu collé.",
"prov.manualErrorMissingState": "Le paramètre state manque dans l’URL de redirection. Collez l’URL complète de cette tentative.",
"prov.manualErrorStateMismatch": "État incohérent — collez l’URL de redirection de cette tentative.",
"prov.manualErrorInvalid": "Impossible d’envoyer le code. Vérifiez la valeur collée et réessayez.",
"prov.pasteFail": "Impossible d’envoyer le code : {error}",
"prov.port": "Port",
"prov.default": "Par défaut",
Expand Down
12 changes: 11 additions & 1 deletion gui/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,10 +385,20 @@ 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": "コードを送信しました — ログインを完了しています…",
"prov.manualErrorEmpty": "リダイレクト URL、認可コード、または API キーを入力してください。",
"prov.manualErrorTooLarge": "貼り付けた内容が大きすぎます。",
"prov.manualErrorNoLogin": "ログイン進行中ではありません。Add account からやり直してください。",
"prov.manualErrorStale": "この貼り付けは以前のログインに属します。Add account をやり直して新しい URL またはコードを貼り付けてください。",
"prov.manualErrorNoCode": "貼り付けた内容に認可コードが見つかりません。",
"prov.manualErrorMissingState": "リダイレクト URL に state パラメータがありません。このログインの完全な URL を貼り付けてください。",
"prov.manualErrorStateMismatch": "state が一致しません — このログインの URL を貼り付けてください。",
"prov.manualErrorInvalid": "コードを送信できませんでした。貼り付けた値をご確認のうえ再試行してください。",
"prov.pasteFail": "コードを送信できませんでした: {error}",
"prov.port": "ポート",
"prov.default": "デフォルト",
Expand Down
12 changes: 11 additions & 1 deletion gui/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,10 +388,20 @@ 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": "코드를 제출했습니다 — 로그인 완료 중…",
"prov.manualErrorEmpty": "리다이렉트 URL, 인증 코드 또는 API 키를 입력하세요.",
"prov.manualErrorTooLarge": "붙여넣은 내용이 너무 큽니다.",
"prov.manualErrorNoLogin": "진행 중인 로그인이 없습니다. Add account를 다시 시작하세요.",
"prov.manualErrorStale": "이 붙여넣기는 이전 로그인에 속합니다. Add account를 다시 시작해 새 URL이나 코드를 붙여넣으세요.",
"prov.manualErrorNoCode": "붙여넣은 내용에서 인증 코드를 찾을 수 없습니다.",
"prov.manualErrorMissingState": "리다이렉트 URL에 state 파라미터가 없습니다. 이번 로그인의 전체 URL을 붙여넣으세요.",
"prov.manualErrorStateMismatch": "state가 일치하지 않습니다 — 이번 로그인의 리다이렉트 URL을 붙여넣으세요.",
"prov.manualErrorInvalid": "코드를 제출할 수 없습니다. 붙여넣은 값을 확인하고 다시 시도하세요.",
"prov.pasteFail": "코드 제출 실패: {error}",
"prov.port": "포트",
"prov.default": "기본값",
Expand Down
Loading
Loading