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:
2026-10-07 11:18:10 +07:00
parent 7543b8d62a
commit 8fdda66893
24 changed files with 504 additions and 59 deletions
@@ -59,6 +59,25 @@ vi.mock("../../../lib/api", async (importOriginal) => {
skills: r({
items: [{ scope: "user", name: "review-pr", frontmatter: {}, preview: "Review a PR" }],
}),
plugins: r({
plugins: [
{
name: "vercel",
enabled: true,
contributes: {
items: {
skills: [{ name: "deploy", description: "Deploy to Vercel" }],
commands: [],
},
},
},
{
name: "off",
enabled: false,
contributes: { items: { skills: [{ name: "deployer" }], commands: [] } },
},
],
}),
},
},
};
@@ -96,6 +115,9 @@ describe("PromptEditor — / and @ suggestions", () => {
expect(options.map((o) => o.textContent)).toEqual([
expect.stringContaining("/review"),
expect.stringContaining("/review-pr"),
// Claude Code's bundled skills, matched on a word of their name.
expect.stringContaining("/code-review"),
expect.stringContaining("/security-review"),
]);
fireEvent.keyDown(box(), { key: "ArrowDown" });
// Read for the directory the session will start in, not the server's own.
@@ -108,6 +130,13 @@ describe("PromptEditor — / and @ suggestions", () => {
expect(spies.onSubmit).not.toHaveBeenCalled();
});
it("offers enabled plugins' skills under their plugin's name", async () => {
renderEditor("");
fireEvent.change(box(), { target: { value: "/deplo" } });
const options = await screen.findAllByRole("option");
expect(options.map((o) => o.textContent)).toEqual([expect.stringContaining("/vercel:deploy")]);
});
it("suggests files under the folder on @", async () => {
vi.mocked(api.run.files).mockResolvedValue({ items: ["src/main.ts"] });
// The prompt is controlled by the caller: put the cursor at the end of it.