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:
+49
-2
@@ -46,6 +46,7 @@ const profiles = require("./cc-profiles");
|
||||
const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
||||
|
||||
const USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
||||
const PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
|
||||
const TOKEN_URL = "https://console.anthropic.com/v1/oauth/token";
|
||||
|
||||
/** Treat a token expiring within this window as already expired: a refresh is
|
||||
@@ -154,6 +155,44 @@ async function fetchUsage(accessToken) {
|
||||
return { fiveHour: windowOf(data.five_hour), sevenDay: windowOf(data.seven_day) };
|
||||
}
|
||||
|
||||
/** Token → owner. An access token never changes hands, so a hit never goes stale. */
|
||||
const owners = new Map();
|
||||
|
||||
/**
|
||||
* The account a token belongs to — `{uuid, email}` — or null when the profile
|
||||
* endpoint does not say. A saved profile's label and its token come from two
|
||||
* different files (`oauthAccount` and the credentials store), and a running
|
||||
* Claude Code session can leave them naming different accounts, so the token's
|
||||
* own answer is the only one that says who a request will be billed to.
|
||||
*/
|
||||
async function fetchOwner(accessToken) {
|
||||
if (owners.has(accessToken)) return owners.get(accessToken);
|
||||
try {
|
||||
const res = await fetch(PROFILE_URL, {
|
||||
headers: { Authorization: `Bearer ${accessToken}`, "anthropic-beta": "oauth-2025-04-20" },
|
||||
signal: AbortSignal.timeout(TIMEOUT_MS),
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const account = (await res.json())?.account;
|
||||
if (!account?.uuid) return null;
|
||||
const owner = { uuid: account.uuid, email: account.email || null };
|
||||
if (owners.size > 50) owners.clear(); // ponytail: crude cap; a few accounts per machine
|
||||
owners.set(accessToken, owner);
|
||||
return owner;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Who the CLI's live token belongs to; null when offline, expired or not OAuth. */
|
||||
async function liveOwner() {
|
||||
const { oauth } = profiles.readActiveAuth();
|
||||
if (!oauth?.accessToken || !(Number(oauth.expiresAt) > Date.now() + EXPIRY_MARGIN_MS)) {
|
||||
return null;
|
||||
}
|
||||
return fetchOwner(oauth.accessToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Usage for one saved profile, refreshing its token first when that token has
|
||||
* expired (which, for any profile that is not the active one, it usually has).
|
||||
@@ -171,7 +210,7 @@ async function profileUsage(name) {
|
||||
// since, retiring the profile's. Use what the CLI holds, and never refresh
|
||||
// it: that would retire the token the user's own terminal is using, and this
|
||||
// module does not write `.credentials.json` back.
|
||||
const live = profiles.liveCredentialsFor(stored);
|
||||
const live = profiles.liveCredentialsFor(stored, { owner: await liveOwner() });
|
||||
const creds = live || stored.credentials || {};
|
||||
const fresh = Number(creds.expiresAt) > Date.now() + EXPIRY_MARGIN_MS;
|
||||
if (live && !fresh) {
|
||||
@@ -200,4 +239,12 @@ async function profileUsage(name) {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { profileUsage, fetchUsage, refreshProfile, CLIENT_ID, USAGE_URL, TOKEN_URL };
|
||||
module.exports = {
|
||||
profileUsage,
|
||||
fetchUsage,
|
||||
fetchOwner,
|
||||
refreshProfile,
|
||||
CLIENT_ID,
|
||||
USAGE_URL,
|
||||
TOKEN_URL,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user