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:
+42
-12
@@ -11,7 +11,11 @@
|
||||
* while that token is valid and never refreshed (a refresh rotates the
|
||||
* saved credentials); past expiry it shows its last figure, `asOf`. Cached
|
||||
* `CLAUDE_TTL_MS` per account, so a poll from every open tab is one
|
||||
* upstream call each. The live entry is `active: true`.
|
||||
* upstream call each. The live entry is `active: true`. Each token's owner
|
||||
* is asked too (`fetchOwner`): the live row is named after the account its
|
||||
* token belongs to (`.claude.json` can name another), and a saved profile
|
||||
* whose token belongs to a different account answers `EWRONGACCOUNT`,
|
||||
* never that account's usage.
|
||||
* - **Codex**: no network. Every `token_count` event Codex writes into its
|
||||
* rollout journal carries the account's `rate_limits`; the newest one wins.
|
||||
*
|
||||
@@ -28,7 +32,7 @@ const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { getCodexHome } = require("./codex-home");
|
||||
const { readActiveAuth, readProfile, listProfiles } = require("./cc-profiles");
|
||||
const { fetchUsage } = require("./cc-usage");
|
||||
const { fetchUsage, fetchOwner } = require("./cc-usage");
|
||||
const { allRolloutFiles } = require("./codex-ingest");
|
||||
|
||||
const CLAUDE_TTL_MS = 5 * 60_000;
|
||||
@@ -71,6 +75,17 @@ function claudeWindows(u) {
|
||||
return windows;
|
||||
}
|
||||
|
||||
/** A token's windows — unless it belongs to another account than `uuid`. */
|
||||
async function usageFor(token, uuid) {
|
||||
const [usage, owner] = await Promise.all([fetchUsage(token), fetchOwner(token)]);
|
||||
if (uuid && owner && owner.uuid !== uuid) {
|
||||
throw Object.assign(new Error("this sign-in belongs to another account"), {
|
||||
code: "EWRONGACCOUNT",
|
||||
});
|
||||
}
|
||||
return claudeWindows(usage);
|
||||
}
|
||||
|
||||
/** The account the CLI is logged into, with the token it holds right now. */
|
||||
async function liveClaude() {
|
||||
let who = {};
|
||||
@@ -82,7 +97,17 @@ async function liveClaude() {
|
||||
expiresAt: iso(oauth.expiresAt),
|
||||
};
|
||||
if (!fresh(oauth.expiresAt)) return { ...who, windows: [], error: "ELIVEEXPIRED" };
|
||||
return { ...who, windows: claudeWindows(await fetchUsage(oauth.accessToken)) };
|
||||
// The token's owner, not `.claude.json`, is who this usage is billed to.
|
||||
const [usage, owner] = await Promise.all([
|
||||
fetchUsage(oauth.accessToken),
|
||||
fetchOwner(oauth.accessToken),
|
||||
]);
|
||||
return {
|
||||
...who,
|
||||
account: owner?.email || who.account,
|
||||
ownerUuid: owner?.uuid || account?.accountUuid || null,
|
||||
windows: claudeWindows(usage),
|
||||
};
|
||||
} catch (e) {
|
||||
return { ...who, windows: [], error: e.code || "EUPSTREAM" };
|
||||
}
|
||||
@@ -105,7 +130,7 @@ async function profileClaude(item) {
|
||||
}
|
||||
if (creds?.accessToken && fresh(creds.expiresAt)) {
|
||||
try {
|
||||
const windows = claudeWindows(await fetchUsage(creds.accessToken));
|
||||
const windows = await usageFor(creds.accessToken, item.accountUuid);
|
||||
lastGood.set(item.name, { windows, at: Date.now() });
|
||||
return { ...who, windows };
|
||||
} catch (e) {
|
||||
@@ -130,19 +155,24 @@ async function claudeQuotas(force) {
|
||||
} catch {
|
||||
/* no profiles dir */
|
||||
}
|
||||
const [live, ...others] = await Promise.all([
|
||||
cached("live", force, liveClaude),
|
||||
...items
|
||||
.filter((it) => !it.active)
|
||||
const live = await cached("live", force, liveClaude);
|
||||
// The live row is the profile of whoever the token belongs to.
|
||||
const mine = live?.ownerUuid
|
||||
? items.find((it) => it.accountUuid === live.ownerUuid)
|
||||
: items.find((it) => it.active);
|
||||
const others = await Promise.all(
|
||||
items
|
||||
.filter((it) => it !== mine)
|
||||
.map(async (it) => ({
|
||||
id: "claude",
|
||||
active: false,
|
||||
label: it.label,
|
||||
...(await cached(`profile:${it.name}`, force, () => profileClaude(it))),
|
||||
})),
|
||||
]);
|
||||
const mine = items.find((it) => it.active);
|
||||
return [live && { id: "claude", active: true, label: mine?.label || null, ...live }, ...others];
|
||||
}))
|
||||
);
|
||||
if (!live) return others;
|
||||
const { ownerUuid: _uuid, ...shown } = live;
|
||||
return [{ id: "claude", active: true, label: mine?.label || null, ...shown }, ...others];
|
||||
}
|
||||
|
||||
/** The newest `rate_limits` in the last `TAIL_BYTES` of a rollout, or null. */
|
||||
|
||||
Reference in New Issue
Block a user