provider, gui: removing the Codex account in use signs Codex in to another of its accounts first, rather than leaving it signed out (vincentzhang1_55530 on Discord)

Removing a Team account beside a Plus one left Codex at its sign-in screen. ForgetLogin now switches Codex to the next saved account (nextOnForget: the first on, else one whose sign-in still holds, else any other) as Use would, then forgets the removed one; only the last account is still signed out as codex logout does. The accounts list gives the account in use a Remove when there are others. TestCodexSignOutLastAccount and TestCodexLogins fail without the change (SignedInError); codex-signout.test.cjs covers the new Remove in English and Chinese.
This commit is contained in:
yetone committed 2026-10-06 00:37:50 +08:00
1 parent 6d198f7f23
commit 4c35c1b8e6
7 files changed
+87 -28

No files matched your search

+7
View File
@@ -10078,6 +10078,13 @@ function renderAccounts(a, p) {
out.title = t("Signs Codex out of this account, as codex logout does");
out.onclick = () => askSignOutLogin(a, l);
row.append(out);
} else if (a.agent === "codex") {
// Codex is signed in to another of its accounts first, then this
// one goes (ForgetLogin), rather than Codex signed out
const forget = el("button", "text quiet", t("Remove"));
forget.title = t("Codex is signed in to another of its accounts, and magpie forgets this one; the account itself is untouched");
forget.onclick = () => accountAction("login/forget", { agent: a.agent, user: l.user }, t("{user} removed", { user: l.user }));
row.append(forget);
}
} else {
const forget = el("button", "text quiet", t("Remove"));
+3
View File
@@ -620,6 +620,7 @@ const I18N = {
"{name} signed out": "已退出 {name}",
"{agent} is signed in to {user} now: sign it in to another of its accounts first (Use on that account), then remove this one": "{agent} 当前登录的是 {user}:请先在另一个账号上点「使用」把 {agent} 切过去,再移除这个账号",
"Signs Codex out of this account, as codex logout does": "让 Codex 退出这个账号,与 codex logout 相同",
"Codex is signed in to another of its accounts, and magpie forgets this one; the account itself is untouched": "Codex 会先登录到它的另一个账号,magpie 再忘掉这个账号;账号本身不受影响",
"Codex itself is signed out too, as codex logout does.": "Codex 本身也会退出登录,与 codex logout 相同。",
"Sign {agent} out of {user}?": "让 {agent} 退出 {user}?",
"{agent} is signed out, as codex logout does, and magpie forgets the account. Sign in again to use it; an open {agent} may need quitting and opening again. The account itself is left as it is.": "{agent} 会退出登录(与 codex logout 相同),magpie 也不再保存这个账号。要再用需重新登录;已打开的 {agent} 可能要退出后重新打开。账号本身不受影响。",
@@ -4007,6 +4008,7 @@ const I18N = {
"{name} signed out": "{name} からサインアウトしました",
"{agent} is signed in to {user} now: sign it in to another of its accounts first (Use on that account), then remove this one": "{agent} は現在 {user} でサインインしています。先に別のアカウントの「使用」で {agent} を切り替えてから、このアカウントを削除してください",
"Signs Codex out of this account, as codex logout does": "codex logout と同じく、Codex をこのアカウントからサインアウトします",
"Codex is signed in to another of its accounts, and magpie forgets this one; the account itself is untouched": "Codex を別のアカウントにサインインし直してから、magpie はこのアカウントを忘れます。アカウント自体はそのままです",
"Codex itself is signed out too, as codex logout does.": "codex logout と同じく、Codex 自体もサインアウトします。",
"Sign {agent} out of {user}?": "{agent} を {user} からサインアウトしますか?",
"{agent} is signed out, as codex logout does, and magpie forgets the account. Sign in again to use it; an open {agent} may need quitting and opening again. The account itself is left as it is.": "codex logout と同じく {agent} はサインアウトし、magpie はこのアカウントを削除します。再び使うにはサインインし直してください。開いている {agent} は終了して開き直す必要があるかもしれません。アカウント自体はそのままです。",
@@ -7380,6 +7382,7 @@ const I18N = {
"{name} signed out": "{name} abgemeldet",
"{agent} is signed in to {user} now: sign it in to another of its accounts first (Use on that account), then remove this one": "{agent} ist gerade bei {user} angemeldet: Melden Sie {agent} zuerst bei einem anderen seiner Konten an (dort „Verwenden“) und entfernen Sie dann dieses",
"Signs Codex out of this account, as codex logout does": "Meldet Codex von diesem Konto ab, wie codex logout",
"Codex is signed in to another of its accounts, and magpie forgets this one; the account itself is untouched": "Codex wird bei einem anderen seiner Konten angemeldet, und magpie vergisst dieses; das Konto selbst bleibt unberührt",
"Codex itself is signed out too, as codex logout does.": "Codex selbst wird ebenfalls abgemeldet, wie mit codex logout.",
"Sign {agent} out of {user}?": "{agent} von {user} abmelden?",
"{agent} is signed out, as codex logout does, and magpie forgets the account. Sign in again to use it; an open {agent} may need quitting and opening again. The account itself is left as it is.": "{agent} wird abgemeldet, wie mit codex logout, und magpie vergisst das Konto. Zum erneuten Verwenden neu anmelden; ein geöffnetes {agent} muss eventuell beendet und neu geöffnet werden. Das Konto selbst bleibt unverändert.",
+3 -2
View File
@@ -681,8 +681,9 @@ login/forget once per account and the line goes; in English and Chinese.
couldn't be removed while Codex was signed in to it): its row has Sign out,
asked in the app's own dialog; Cancel and Escape post nothing and go back to
the editor, Sign out posts login/forget and says so. With another account
saved the one in use has no Sign out, and a signed_in refusal is said in the
reader's language. No select, no left border, the page doesn't move; in
saved the one in use has no Sign out but Remove, which posts login/forget
(vincentzhang1_55530 on Discord: magpie signs Codex in to another account
first), and a signed_in refusal is said in the reader's language. No select, no left border, the page doesn't move; in
English and Chinese.
`nokey-editor.test.cjs` opens two local Ollama providers saved with no key,
+15 -4
View File
@@ -48,8 +48,8 @@ function serve(lang, logins, posts, refuse) {
}
const words = {
en: { out: "Sign out", cancel: "Cancel", ask: "Sign Codex out of me@example.com?", text: /Codex is signed out, as codex logout does/, done: "Codex signed out of me@example.com", refused: /Codex is signed in to me@example\.com now: sign it in to another of its accounts first/ },
zh: { out: "退出登录", cancel: "取消", ask: "让 Codex 退出 me@example.com?", text: /codex logout/, done: "Codex 已退出 me@example.com", refused: /Codex 当前登录的是 me@example\.com:请先在另一个账号上点「使用」/ },
en: { out: "Sign out", remove: "Remove", cancel: "Cancel", ask: "Sign Codex out of me@example.com?", text: /Codex is signed out, as codex logout does/, done: "Codex signed out of me@example.com", refused: /Codex is signed in to me@example\.com now: sign it in to another of its accounts first/ },
zh: { out: "退出登录", remove: "移除", cancel: "取消", ask: "让 Codex 退出 me@example.com?", text: /codex logout/, done: "Codex 已退出 me@example.com", refused: /Codex 当前登录的是 me@example\.com:请先在另一个账号上点「使用」/ },
};
for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", "webkit"])) {
@@ -112,12 +112,23 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
assert.deepEqual(errors, []);
});
test(`${engine} ${lang}: with another account saved, the one in use has no Sign out`, async (t) => {
const { page, errors } = await open(t, two);
// vincentzhang1_55530 on Discord: the one in use is removed as the
// others are, and Codex is signed in to another first (ForgetLogin)
test(`${engine} ${lang}: with another account saved, the one in use has Remove, not Sign out`, async (t) => {
const { page, errors, posts } = await open(t, two);
for (const user of ["me@example.com", "work@example.com"]) {
const row = page.locator(".editor .acc", { hasText: user });
assert.equal(await row.getByRole("button", { name: w.out, exact: true }).count(), 0, user);
}
const remove = page.locator(".editor .acc", { hasText: "me@example.com" }).getByRole("button", { name: w.remove, exact: true });
assert.equal(await remove.count(), 1, "the one in use has Remove");
assert.ok(await remove.getAttribute("title"));
await remove.click();
await page.waitForFunction(() => document.querySelector("#status")?.textContent.includes("me@example.com"));
assert.deepEqual(posts, [{ agent: "codex", user: "me@example.com" }]);
const missing = await page.evaluate(() => ["Codex is signed in to another of its accounts, and magpie forgets this one; the account itself is untouched"]
.filter((k) => !I18N.zh[k] || !I18N.ja[k] || !I18N.de[k]));
assert.deepEqual(missing, [], "every string has its zh, ja and de");
assert.deepEqual(errors, []);
});
+18 -15
View File
@@ -10,34 +10,37 @@ import (
)
// Codex's only account is signed out when it is removed, as codex logout
// does (mamba on Discord); one with another saved is still refused, with a
// SignedInError the GUI says in the reader's language; and ForgetAccount,
// which takes the one in use last, signs Codex out too.
// does (mamba on Discord); one with another saved signs Codex in to that
// one first (vincentzhang1_55530 on Discord: a Team account removed beside
// a Plus one left Codex at its sign-in screen); and ForgetAccount, which
// takes the one in use last, signs Codex out too.
func TestCodexSignOutLastAccount(t *testing.T) {
home := signIn(t)
auth := filepath.Join(home, ".codex", "auth.json")
rememberLogins(true)
var signedIn *SignedInError
// another saved: the one signed in to is refused, as before
// another saved: the one signed in to goes, and Codex is signed in to
// the other, its own sign-in back in auth.json
codexSignIn(t, home, "work@example.com", "r-work")
rememberLogins(true)
err := ForgetLogin("codex", "work@example.com")
var signedIn *SignedInError
if !errors.As(err, &signedIn) || signedIn.Agent != "codex" || signedIn.User != "work@example.com" {
if err := ForgetLogin("codex", "work@example.com"); err != nil {
t.Fatalf("the one in use with another saved: %v", err)
}
if !strings.Contains(err.Error(), "Codex is signed in to work@example.com now") {
t.Fatalf("message: %v", err)
live, ok := liveLogin("codex")
if !ok || live.User != "me@example.com" {
t.Fatalf("Codex signed in to %q (%v), want me@example.com", live.User, ok)
}
if _, err := os.Stat(auth); err != nil {
t.Fatalf("auth.json went on a refusal: %v", err)
if b, _ := os.ReadFile(auth); strings.Contains(string(b), "r-work") {
t.Fatalf("auth.json still the removed one's: %s", b)
}
rememberLogins(true)
if ls := Logins("codex"); len(ls) != 1 || ls[0].User != "me@example.com" || !ls[0].Active {
t.Fatalf("listed: %+v", ls)
}
// the other forgotten, the one left is signed out
// the one left is signed out
if err := ForgetLogin("codex", "me@example.com"); err != nil {
t.Fatal(err)
}
if err := ForgetLogin("codex", "work@example.com"); err != nil {
t.Fatalf("the only account: %v", err)
}
if _, err := os.Stat(auth); !os.IsNotExist(err) {
+37 -1
View File
@@ -946,7 +946,8 @@ func putClaudeLogin(l savedLogin) error {
// ForgetLogin drops a remembered account. The one an agent is signed in to
// now can't be forgotten while it has another to be signed in to; it would
// only be remembered again (SignedInError). Codex's last one is signed out
// only be remembered again (SignedInError). Codex is signed in to another
// of its accounts first (nextOnForget), and its last one is signed out
// instead, as `codex logout` does (mamba on Discord: a single Codex account
// couldn't be removed at all).
func ForgetLogin(agent, user string) error {
@@ -979,6 +980,18 @@ func ForgetLogin(agent, user string) error {
case "gemini", "antigravity":
return forgetGoogleLogin(agent, user)
}
if agent == "codex" {
// the account Codex is signed in to, with another saved: Codex is
// signed in to that one first, as its Use would, and this one is
// forgotten then, rather than Codex left signed out or the removal
// refused (vincentzhang1_55530 on Discord: a Team account removed
// beside a Plus one, and Codex was at its sign-in screen)
if next := nextOnForget(agent, user); next != "" {
if err := SwitchLogin(agent, next); err != nil {
return err
}
}
}
signedOut, err := forgetLogin(agent, user)
if signedOut {
// gone from the agent too: nothing of it is served any more
@@ -987,6 +1000,29 @@ func ForgetLogin(agent, user string) error {
return err
}
// nextOnForget is the account an agent is signed in to in place of user,
// which is being removed: "" when it isn't signed in to user, or has no
// other saved. The first other in the order that is on, else one whose
// sign-in still holds, else any other.
func nextOnForget(agent, user string) string {
ls := Logins(agent)
if !slices.ContainsFunc(ls, func(l Login) bool { return l.Active && strings.EqualFold(l.User, user) }) {
return ""
}
for _, fit := range []func(Login) bool{
func(l Login) bool { return l.On && !l.Paused && l.Lapsed == "" },
func(l Login) bool { return l.Lapsed == "" },
func(Login) bool { return true },
} {
for _, l := range ls {
if !strings.EqualFold(l.User, user) && fit(l) {
return l.User
}
}
}
return ""
}
func forgetLogin(agent, user string) (signedOut bool, err error) {
loginsMu.Lock()
defer loginsMu.Unlock()
+4 -6
View File
@@ -66,14 +66,12 @@ func TestCodexLogins(t *testing.T) {
}
}
if err := ForgetLogin("codex", "me@example.com"); err == nil {
t.Fatal("forgot the active login")
}
if err := ForgetLogin("codex", "work@example.com"); err != nil {
// the active one forgotten: Codex is signed in to the other first
if err := ForgetLogin("codex", "me@example.com"); err != nil {
t.Fatal(err)
}
if users, _ := loginUsers(Logins("codex")); strings.Join(users, ",") != "me@example.com" {
t.Fatalf("after forget: %v", users)
if users, active := loginUsers(Logins("codex")); strings.Join(users, ",") != "work@example.com" || active != "work@example.com" {
t.Fatalf("after forget: %v, active %q", users, active)
}
if err := SwitchLogin("codex", "nobody@example.com"); err == nil {
t.Fatal("switched to an unknown login")