Skip to content
Merged
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
6 changes: 4 additions & 2 deletions gui/src/components/AddProviderModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ type Preset = CatalogPreset;

export default function AddProviderModal({
apiBase, existingNames, onClose, onAdded, initialTier, initialCustom = false,
accountRows, accountStatus, accountBusy, onAccountLogin, onAccountCancelLogin, onAccountLogout, onOpen,
accountRows, accountStatus, accountBusy, onAccountLogin, onAccountCancelLogin, onAccountLogout, onAccountManage, onOpen,
}: {
apiBase: string;
existingNames: string[];
Expand All @@ -39,9 +39,10 @@ export default function AddProviderModal({
accountRows?: AccountLoginRow[];
accountStatus?: Record<string, AccountLoginStatus>;
accountBusy?: string | null;
onAccountLogin?: (provider: string) => void;
onAccountLogin?: (provider: string, addAccount?: boolean) => void;
onAccountCancelLogin?: (provider: string) => void;
onAccountLogout?: (provider: string) => void;
onAccountManage?: (provider: string) => void;
onOpen?: () => void;
}) {
const t = useT();
Expand Down Expand Up @@ -249,6 +250,7 @@ export default function AddProviderModal({
onLogin={onAccountLogin}
onCancelLogin={onAccountCancelLogin}
onLogout={onAccountLogout}
onManage={onAccountManage}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
/>
) : form && (
preset.auth === "oauth" && form.authMode === "oauth" ? (
Expand Down
33 changes: 31 additions & 2 deletions gui/src/components/provider-catalog/ProviderCatalog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export default function ProviderCatalog({
onLogin,
onCancelLogin,
onLogout,
onManage,
}: {
presets: CatalogPreset[];
usageRank?: Record<string, number>;
Expand All @@ -52,9 +53,11 @@ export default function ProviderCatalog({
accountRows?: AccountLoginRow[];
accountStatus?: Record<string, AccountLoginStatus>;
busyProvider?: string | null;
onLogin?: (provider: string) => void;
onLogin?: (provider: string, addAccount?: boolean) => void;
onCancelLogin?: (provider: string) => void;
onLogout?: (provider: string) => void;
/** Jump to the provider's Accounts surface in the workspace. */
onManage?: (provider: string) => void;
}) {
const t = useT();
const [tier, setTier] = useState<CatalogTier>(initialTier);
Expand Down Expand Up @@ -172,7 +175,33 @@ export default function ProviderCatalog({
)}
</>
) : loggedIn ? (
onLogout && <button type="button" className="btn btn-ghost" onClick={() => onLogout(row.id)}>{t("modal.accountLogout")}</button>
<>
{onManage && (
<button type="button" className="btn btn-ghost" onClick={() => onManage(row.id)}>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Hide Manage when no provider workspace exists

Deleting an OAuth provider removes its config row but deliberately leaves its stored credential, so Add Provider → Accounts still considers that provider logged in. In that state this new Manage button closes the modal and selects a provider absent from config.providers; ProviderWorkspaceShell cannot resolve a detail item and only shows the overview. Only offer Manage for configured providers, or recreate/fetch the provider row before navigating.

AGENTS.md reference: gui/AGENTS.md:L10-L10

Useful? React with 👍 / 👎.

{t("modal.accountManage")}
</button>
)}
{onLogin && (
<button
type="button"
className="btn btn-ghost"
disabled={busy}
onClick={() => { if (!busy) onLogin(row.id, true); }}
>
{busy ? t("prov.waitingBrowser") : t("modal.accountAdd")}
</button>
)}
{busy && onCancelLogin && (
<button type="button" className="btn btn-ghost" onClick={() => onCancelLogin(row.id)}>
{t("common.cancel")}
</button>
)}
{onLogout && !busy && (
<button type="button" className="btn btn-ghost" onClick={() => onLogout(row.id)}>
{t("modal.accountLogout")}
</button>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
)}
</>
) : busy ? (
onCancelLogin && <button type="button" className="btn btn-ghost" onClick={() => onCancelLogin(row.id)}>{t("common.cancel")}</button>
) : (
Expand Down
27 changes: 27 additions & 0 deletions gui/src/components/provider-workspace/ProviderDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export default function ProviderDetails({
oauth,
accounts,
accountLoadState,
accountsFocusToken = 0,
accountsFocusProvider = null,
switchingAccountId,
keys,
busyProvider,
Expand Down Expand Up @@ -71,6 +73,10 @@ export default function ProviderDetails({
oauth?: { loggedIn: boolean; email?: string; error?: string; needsReauth?: boolean };
accounts?: OAuthAccountRow[];
accountLoadState?: AccountLoadState;
/** When this token increases for accountsFocusProvider, switch to the Accounts tab. */
accountsFocusToken?: number;
/** Provider that owns the current accountsFocusToken; other providers ignore it. */
accountsFocusProvider?: string | null;
switchingAccountId?: string | null;
keys?: ApiKeyRow[];
busyProvider?: string | null;
Expand All @@ -91,13 +97,18 @@ export default function ProviderDetails({
const [pendingLeave, setPendingLeave] = useState<Tab | "deselect" | null>(null);
const [leaveSaving, setLeaveSaving] = useState(false);
const settingsSaveRef = useRef<(() => Promise<boolean>) | null>(null);
// Seed 0 so a mount-time token from revealProviderAccounts stays pending until
// authSurface exists; seeding with the prop would treat it as already seen.
const [seenAccountsFocusToken, setSeenAccountsFocusToken] = useState(0);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const registerSettingsSave = useCallback((save: (() => Promise<boolean>) | null) => {
settingsSaveRef.current = save;
}, []);
const isDisabled = item.disabled === true;
const free = useMemo(() => isFreeProvider(item), [item]);
const local = useMemo(() => isLocalProvider(item), [item]);
const authSurface = useMemo(() => providerAuthSurface(item), [item]);
// Global counter from Providers — only honor it for the reveal target.
const scopedAccountsFocusToken = accountsFocusProvider === item.name ? accountsFocusToken : 0;
const connectionIdentity = JSON.stringify([
codexController?.activeId ?? "",
accounts?.find(account => account.active)?.id ?? "",
Expand All @@ -122,6 +133,22 @@ export default function ProviderDetails({
setTab(next);
}, [tab, settingsDirty]);

// Adjust related state when accountsFocusToken changes during render (not in an
// effect) so the Accounts tab is selected without a one-frame stale paint.
// https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes
// Hold a non-zero token until authSurface exists so mount-time focus from
// revealProviderAccounts is not marked seen before Accounts can open.
if (scopedAccountsFocusToken !== seenAccountsFocusToken && !(scopedAccountsFocusToken && !authSurface)) {
setSeenAccountsFocusToken(scopedAccountsFocusToken);
if (scopedAccountsFocusToken && authSurface) {
if (settingsDirty && tab === "settings") {
setPendingLeave("accounts");
} else {
setTab("accounts");
}
}
}

const requestDeselect = useCallback(() => {
if (settingsDirty && tab === "settings") {
setPendingLeave("deselect");
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ export const de: Record<TKey, string> = {
"prov.loginCancelled": "{provider}-Login abgebrochen",
"prov.loginTimeout": "{provider}-Login abgelaufen — Browser geschlossen oder nicht beendet. Erneut versuchen.",
"prov.loginOk": "Bei {provider} angemeldet. Führe {cmd} aus (oder es gilt live), um seine Modelle aufzulisten.",
"prov.loginSameAccount": "Immer noch dasselbe {provider}-Konto — wechsle im Browser das Konto und versuche „Konto hinzufügen“ erneut.",
"oauthTos.highTitle": "{provider}: Risiko bei Abo-OAuth",
"oauthTos.elevatedTitle": "{provider}: inoffizielle OAuth-Brücke",
"oauthTos.anthropicBody": "Die direkte Wiederverwendung von Claude-Abo-OAuth-Tokens über einen Drittanbieter-Proxy wie OpenCodex ist keine von Anthropic unterstützte Integration und kann zu Zugriffsbeschränkungen führen. Unterstützte Agent-SDK-Integrationen, die Claude-Abos verwenden, sind davon getrennt.",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ export const en = {
"prov.loginCancelled": "{provider} login cancelled",
"prov.loginTimeout": "{provider} login timed out — browser closed or never finished. Try again.",
"prov.loginOk": "Logged in to {provider}. Run {cmd} (or it applies live) to list its models.",
"prov.loginSameAccount": "Still the same {provider} account — switch accounts in the browser, then try Add account again.",
"oauthTos.highTitle": "{provider}: subscription OAuth risk",
"oauthTos.elevatedTitle": "{provider}: unofficial OAuth bridge",
"oauthTos.anthropicBody": "Directly reusing Claude subscription OAuth tokens through a third-party proxy such as OpenCodex is not a supported Anthropic integration and may lead to access restrictions. Supported Agent SDK integrations that use Claude subscriptions are separate.",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ export const ja: Record<TKey, string> = {
"prov.loginCancelled": "{provider} ログインはキャンセルされました",
"prov.loginTimeout": "{provider} ログインがタイムアウトしました — ブラウザが閉じたか完了しませんでした。もう一度お試しください。",
"prov.loginOk": "{provider} にログインしました。{cmd} を実行(またはライブで適用)してモデルを一覧表示します。",
"prov.loginSameAccount": "同じ {provider} アカウントのままです。ブラウザでアカウントを切り替えてから、もう一度アカウント追加を試してください。",
"oauthTos.highTitle": "{provider}: サブスクリプション OAuth リスク",
"oauthTos.elevatedTitle": "{provider}: 非公式 OAuth ブリッジ",
"oauthTos.anthropicBody": "OpenCodex のような第三者プロキシ経由で Claude サブスクリプションの OAuth トークンを直接再利用することは、Anthropic がサポートする統合ではなく、アクセス制限につながる可能性があります。Claude サブスクリプションを使用するサポートされた Agent SDK 統合は別物です。",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ export const ko: Record<TKey, string> = {
"prov.loginCancelled": "{provider} 로그인이 취소되었습니다",
"prov.loginTimeout": "{provider} 로그인 시간 초과 — 브라우저를 닫았거나 완료되지 않았습니다. 다시 시도하세요.",
"prov.loginOk": "{provider} 에 로그인했습니다. 모델을 표시하려면 {cmd} 를 실행하세요(또는 실시간 적용됩니다).",
"prov.loginSameAccount": "같은 {provider} 계정입니다. 브라우저에서 계정을 전환한 뒤 계정 추가를 다시 시도하세요.",
"oauthTos.highTitle": "{provider}: 구독 OAuth 위험",
"oauthTos.elevatedTitle": "{provider}: 비공식 OAuth 브리지",
"oauthTos.anthropicBody": "Claude 구독 OAuth 토큰을 OpenCodex 같은 타사 프록시에서 직접 재사용하는 방식은 Anthropic이 지원하는 통합이 아니며 접근이 제한될 수 있습니다. Claude 구독을 사용하는 공식 Agent SDK 통합은 별도입니다.",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ export const ru: Record<TKey, string> = {
"prov.loginCancelled": "Вход в {provider} отменён",
"prov.loginTimeout": "Время ожидания входа в {provider} истекло — браузер был закрыт или вход не был завершён. Попробуйте ещё раз.",
"prov.loginOk": "Выполнен вход в {provider}. Выполните {cmd} (или изменения применятся на лету), чтобы его модели появились в списке.",
"prov.loginSameAccount": "Это всё ещё тот же аккаунт {provider} — переключите аккаунт в браузере и снова нажмите «Добавить аккаунт».",
"oauthTos.highTitle": "{provider}: риск OAuth по подписке",
"oauthTos.elevatedTitle": "{provider}: неофициальный OAuth-мост",
"oauthTos.anthropicBody": "Прямое повторное использование OAuth-токенов подписки Claude через сторонний прокси, такой как OpenCodex, не является поддерживаемой интеграцией Anthropic и может привести к ограничению доступа. Поддерживаемые интеграции Agent SDK, использующие подписки Claude, — это отдельный механизм.",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ export const zh: Record<TKey, string> = {
"prov.loginCancelled": "{provider} 登录已取消",
"prov.loginTimeout": "{provider} 登录超时 — 浏览器已关闭或未完成。请重试。",
"prov.loginOk": "已登录到 {provider}。运行 {cmd}(或实时生效)以列出其模型。",
"prov.loginSameAccount": "仍是同一个 {provider} 账户 — 请在浏览器中切换账户后再次尝试添加账户。",
"oauthTos.highTitle": "{provider}:订阅 OAuth 风险",
"oauthTos.elevatedTitle": "{provider}:非官方 OAuth 桥接",
"oauthTos.anthropicBody": "通过 OpenCodex 等第三方代理直接复用 Claude 订阅 OAuth 令牌,并非 Anthropic 支持的集成方式,可能导致访问受限。可使用 Claude 订阅的受支持 Agent SDK 集成属于另一种方式。",
Expand Down
Loading
Loading