fix: v2.5.9 — Vietnamese in the terminal on macOS, every slash command, the right Claude account
Issue #62: a tmux client whose LC_ALL/LC_CTYPE/LANG names no UTF-8 prints `_` for every non-ASCII character, and a macOS LaunchAgent starts the dashboard with no locale at all — Vietnamese text and Claude Code's logo came out as underscores in every browser terminal. - shell-path.js: ensureUtf8Locale() defaults LC_CTYPE (en_US.UTF-8 on macOS, C.UTF-8 elsewhere) at start, before any tmux call; an explicit LC_ALL and Windows are left alone - RunSetup.tsx: the `/` list now offers Claude Code's bundled skills (/code-review, /security-review, /simplify, /loop…) and newer built-ins, and every enabled plugin's commands and skills as /<plugin>:<name> - Claude login decided by the TOKEN's owner (GET /api/oauth/profile, cached per token), not by .claude.json: a running session can leave the credentials store holding one account's token under another account's name. The Profiles modal, its Check quota and the status-bar quota now show the account requests are really billed to and warn on the mismatch; a switch saves the outgoing token into its OWNER's profile (credentials only) instead of overwriting the profile .claude.json names; saving a profile while they disagree is refused (EMISMATCH); a saved profile holding another account's token reports EWRONGACCOUNT instead of that account's usage - Running Claude Code sessions follow a switch on their own (they re-read the credentials store when it changes), so the "close your sessions first" warning is gone
This commit is contained in:
@@ -384,3 +384,63 @@ describe("cc-profiles", () => {
|
||||
assert.throws(() => profiles.listProfiles(), { code: "EBADJSON" });
|
||||
});
|
||||
});
|
||||
|
||||
// The state a running session can leave behind: `.claude.json` still names one
|
||||
// account while the credentials store holds ANOTHER account's token. The token
|
||||
// is what Claude Code actually uses, so its owner (asked of Anthropic by the
|
||||
// route, passed in here) decides who is logged in.
|
||||
describe("cc-profiles — identity and token disagree", () => {
|
||||
/** Thắng and Victor saved; then Victor's token lands under Thắng's identity. */
|
||||
function mixedLogin() {
|
||||
login("uuid-t", "t@example.com");
|
||||
profiles.saveProfile("thang");
|
||||
login("uuid-v", "v@example.com");
|
||||
profiles.saveProfile("victor");
|
||||
login("uuid-t", "t@example.com");
|
||||
fs.writeFileSync(
|
||||
CREDS,
|
||||
JSON.stringify({
|
||||
claudeAiOauth: { accessToken: "at-v-new", refreshToken: "rt-v-new", expiresAt: 222 },
|
||||
})
|
||||
);
|
||||
return { uuid: "uuid-v", email: "v@example.com" };
|
||||
}
|
||||
const stored = (name) => profiles.readProfile(name);
|
||||
|
||||
it("marks the token's owner active and reports the disagreement", () => {
|
||||
const owner = mixedLogin();
|
||||
const { items, active, mismatch } = profiles.listProfiles({ owner });
|
||||
assert.deepEqual(
|
||||
items.filter((p) => p.active).map((p) => p.name),
|
||||
["victor"]
|
||||
);
|
||||
assert.equal(active.email, "v@example.com");
|
||||
assert.equal(mismatch.configEmail, "t@example.com");
|
||||
// Without an owner (offline) the config is all there is, as before.
|
||||
assert.equal(profiles.listProfiles().mismatch, null);
|
||||
});
|
||||
|
||||
it("a switch saves the outgoing token into its OWNER's profile, never the one config names", () => {
|
||||
const owner = mixedLogin();
|
||||
profiles.activateProfile("thang", { owner });
|
||||
assert.equal(stored("victor").credentials.refreshToken, "rt-v-new");
|
||||
assert.equal(stored("victor").account.emailAddress, "v@example.com");
|
||||
assert.equal(stored("thang").credentials.refreshToken, "rt-uuid-t");
|
||||
// And the switch itself landed: Thắng's token and identity, together.
|
||||
assert.equal(JSON.parse(fs.readFileSync(CREDS, "utf8")).claudeAiOauth.accessToken, "at-uuid-t");
|
||||
assert.equal(JSON.parse(fs.readFileSync(CONFIG, "utf8")).oauthAccount.accountUuid, "uuid-t");
|
||||
});
|
||||
|
||||
it("switching to the owner's own profile keeps the live (newest) token and fixes the identity", () => {
|
||||
const owner = mixedLogin();
|
||||
profiles.activateProfile("victor", { owner });
|
||||
assert.equal(JSON.parse(fs.readFileSync(CREDS, "utf8")).claudeAiOauth.refreshToken, "rt-v-new");
|
||||
assert.equal(JSON.parse(fs.readFileSync(CONFIG, "utf8")).oauthAccount.accountUuid, "uuid-v");
|
||||
assert.equal(stored("thang").credentials.refreshToken, "rt-uuid-t");
|
||||
});
|
||||
|
||||
it("refuses to save a profile while they disagree", () => {
|
||||
const owner = mixedLogin();
|
||||
assert.throws(() => profiles.saveProfile(undefined, { owner }), { code: "EMISMATCH" });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user