Skip to content

fix(web-shell): localize built-in command and skill descriptions in the slash menu - #6326

Merged
wenshao merged 1 commit into
QwenLM:mainfrom
wenshao:fix/web-shell-zh-command-skill-descriptions
Jul 5, 2026
Merged

fix(web-shell): localize built-in command and skill descriptions in the slash menu#6326
wenshao merged 1 commit into
QwenLM:mainfrom
wenshao:fix/web-shell-zh-command-skill-descriptions

Conversation

@wenshao

@wenshao wenshao commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Re-localizes the descriptions shown in the web-shell slash-command menu so a Chinese (zh-CN) session no longer mixes languages. Built-in command descriptions the daemon advertises (/bug, /directory, /effort, …) and skill descriptions (/dataviz, /bugfix, …) are now shown in the active web-shell UI language instead of the daemon's English. This is display-only — the model still receives the daemon's canonical English text, so skill invocation is unaffected.

Why it's needed

The web-shell already translated its local fallback command list, but every command and skill the daemon advertises carries a description filled from the daemon process language, which is independent of the web-shell UI language (they are separate concepts — a daemon serves any client). So in a Chinese session the menu came out half-Chinese, half-English. Because the two languages can't be assumed equal, the menu can only be made to match the UI language by re-localizing on the client.

Reviewer Test Plan

How to verify

  1. npm run dev:daemon and open the web-shell with UI language zh-CN.
  2. Type / to open the slash menu.
  3. Built-in commands (/bug, /compress, /config, /diff, /directory, /docs, /doctor, /export, /init, /lsp, /effort, …) show Chinese descriptions.
  4. Skills — both bundled (/dataviz, /batch, /review, /stuck, …) and this repo's project skills (/bugfix, /codegraph, /triage, …) — show Chinese descriptions, including on the welcome screen before any message is sent (skills only carry a reliable source once a session exists, so localization is keyed off connection.skills / skill name).
  5. Switch UI language to English and confirm the same entries read in English; custom commands and unknown/user skills keep their own authored descriptions.

Unit tests: npm test -w packages/web-shell -- client/constants/localCommands.test.ts (covers commands, bundled + project skills, the no-session pipeline, and the untouched-custom/unknown cases).

Evidence (Before & After)

Built-in commands — before (mixed) vs after (all Chinese):

commands before/after

Skills on the welcome screen (no session) — all Chinese, bundled + project:

skills all Chinese

Verified live end-to-end: real dev:daemon + a Chromium (Playwright) session in zh-CN, reading the actual rendered menu DOM.

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

npm run dev:daemon (daemon from source via tsx + web-shell vite dev), Chromium via Playwright.

Risk & Scope

  • Main risk or tradeoff: display-only string substitution in the slash menu; no change to command/skill execution or to what the model receives. The translated set is a curated by-name map, so a user's own or newly-added skills fall back to their authored (English) description.
  • Not validated / out of scope: automatic localization of arbitrary/user skills — that would need a SKILL.md frontmatter i18n mechanism (core + daemon + web-shell) and is left as a follow-up. Windows/Linux rely on CI.
  • Breaking changes / migration notes: none.

Linked Issues

N/A

中文说明

这个 PR 做了什么

修复 web-shell 斜杠命令菜单在中文(zh-CN)会话下描述中英混杂的问题。daemon 广播的内置命令(/bug/directory/effort …)和技能(/dataviz/bugfix …)的描述,现在按 web-shell 当前 UI 语言显示,而不是 daemon 的英文。仅显示层替换 —— 模型仍收到 daemon 的英文规范描述,不影响技能调用。

为什么需要

web-shell 本地 fallback 命令列表已翻译,但 daemon 广播的每个命令/技能,其描述来自 daemon 进程 语言,和 浏览器 UI 语言无关(两者是独立概念:一个 daemon 可服务任意客户端)。所以中文会话下菜单一半中文一半英文。既然两种语言不能假定相等,只能在客户端按 UI 语言重新本地化。

关键点

  • localizeBuiltinDescriptions() 按名重译内置命令,source === 'builtin-command' 守卫,自定义命令保留原描述。
  • 技能在打标步骤按名重译(key 于 connection.skills),欢迎屏无会话时也生效 —— 技能只有建了会话才带可靠 source
  • 覆盖 20 个 daemon-only 内置命令 + 27 个技能(9 bundled + 18 本仓库 project)。未收录的技能(用户自建/新增)回退英文原文。
  • 真机验证:真实 dev:daemon + Playwright(zh-CN + 真实会话/欢迎屏)读菜单 DOM。

…he slash menu

The slash-command menu mixed languages in a zh-CN session: the local fallback
commands were translated, but daemon-advertised built-in commands (/bug,
/directory, /effort, …) and bundled/project skills (/dataviz, /bugfix, …) showed
the daemon's English descriptions.

The daemon fills descriptions from its own process language, which is independent
of the web-shell UI language, so the menu can only match the UI language by
re-localizing on the client.

- localizeBuiltinDescriptions() re-localizes built-in commands by name, guarded
  by source === 'builtin-command' so custom commands keep their own description.
- Skills are localized by name in the skill-tagging step (keyed off
  connection.skills), so it also works on the welcome screen before a session
  exists — skills only carry a reliable source once a session is created.
- Covers 20 daemon-only built-in commands and 27 skills (9 bundled + 18 project).
  Display-only: the model still receives the daemon's canonical English text.
  Unknown/user skills keep their authored descriptions.
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR, @wenshao!

Template looks good ✓

Problem: Real observed issue — the slash-menu mixes languages when the web-shell UI is set to zh-CN because daemon-advertised command/skill descriptions come from the daemon process locale, independent of the browser UI locale. Before/after screenshots in the PR body demonstrate the fix.

Direction: Aligned with web-shell i18n goals. The web-shell already had a translated fallback command list; this closes the gap for daemon-advertised entries. No CHANGELOG reference in Claude Code (this is Qwen-specific infra), but the area is clearly within scope.

Approach: Clean and minimal. Curated name→i18n-key maps for 20 built-in commands + 27 skills (9 bundled + 18 project), applied client-side. Guarded by source === 'builtin-command' so custom commands aren't touched. Unknown/newly-added skills fall back to their authored description — reasonable tradeoff versus building a full SKILL.md i18n mechanism. Unit tests cover the pipeline end-to-end, including the no-session welcome-screen case.

One observation: the skill localization keys off skill name rather than source, which is explicitly called out as intentional (skills only get a reliable source once a session exists). The name-keyed approach is pragmatic given the constraint.

Moving on to code review. 🔍

中文说明

感谢贡献,@wenshao

模板完整 ✓

问题: 真实存在的 bug —— web-shell UI 设置为 zh-CN 时,斜杠菜单出现中英混杂。原因是 daemon 广播的命令/技能描述来自 daemon 进程的语言,和浏览器 UI 语言无关。PR 正文中的 before/after 截图证明了问题及修复效果。

方向: 与 web-shell i18n 目标一致。web-shell 已有本地化的 fallback 命令列表,这次补齐了 daemon 广播条目的缺口。Claude Code 没有对应 CHANGELOG 条目(这是 Qwen 特有基础设施),但该方向明确在范围内。

方案: 简洁且最小化。为 20 个内置命令 + 27 个技能(9 bundled + 18 项目)建立 name→i18n key 映射,在客户端应用。用 source === 'builtin-command' 守卫,避免影响自定义命令。未知/新增技能回退到原始描述 —— 比搞一套完整的 SKILL.md i18n 机制务实得多。单测端到端覆盖整个 pipeline,包括无会话的欢迎屏场景。

一个观察:技能本地化是按 skill 名字而非 source 来 key 的,PR 中也明确说明了意图(技能只有在会话建立后才有可靠的 source)。按名字 key 在当前约束下是合理的务实选择。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal before reading the diff: add name→i18n-key maps for daemon-advertised commands and known skills, apply as a post-merge step guarded by source === 'builtin-command', handle the no-session case where skills lack a reliable source, add EN+ZH translations, and write unit tests covering custom-command-shadowing and unknown-skill fallback.

The PR's approach matches this exactly. Two clean Record<string, string> maps (BUILTIN_COMMAND_DESCRIPTION_KEYS, SKILL_DESCRIPTION_KEYS), a dedicated localizeBuiltinDescriptions() pass, and skill localization keyed by name in the existing tagging step — which is the right call given skills only get a reliable source after a session exists. No over-abstraction, no speculative code.

Reuse check: nothing in the repo already provides this — the gap between getLocalCommands() (fallback list) and daemon-advertised commands is genuinely new territory this PR fills. Messages is Record<string, MessageValue> so the new i18n keys are type-safe without any type plumbing.

No critical blockers. No AGENTS.md violations.

Test Results

New unit tests (localCommands.test.ts) — 9/9 passing:

  • getLocalCommands translates fallback descriptions
  • localizeBuiltinDescriptions re-localizes built-ins, leaves custom commands alone, skips unmapped built-ins, skips skills, preserves other fields
  • skillDescriptionKey maps known skills, returns undefined for unknown
  • Full App pipeline test: commands + bundled skills + project skills + unknown skill + custom command, no session needed

Full web-shell suite — 986/986 passing across 65 test files (3.6s). No regressions.

Typechecktsc -p packages/web-shell/tsconfig.json --noEmit reports errors in MessageList.test.ts and Markdown.test.ts, but these predate the PR (identical errors on main without the PR's code). Not introduced here.

Real-Scenario Testing

⚠️ Not runnable in triage. This is a browser-UI change (slash-menu descriptions in the web-shell), not a CLI behavior change. The project has no Playwright/browser E2E infrastructure for web-shell, and tmux-based real-scenario testing targets the CLI only. The author's own Playwright-based verification (real dev:daemon + Chromium reading menu DOM in zh-CN) is the appropriate verification path here; the before/after screenshots in the PR body are the evidence.

Summary

Clean, minimal i18n fix with strong test coverage. No correctness, security, or regression concerns. The only verification gap is that triage cannot independently drive a browser to confirm the rendered menu, but the unit tests comprehensively cover the transformation pipeline and the author's live Playwright evidence is credible.

中文说明

代码审查

读 diff 前我的独立方案:为 daemon 广播的命令和已知技能建立 name→i18n-key 映射,作为 merge 后的一个步骤应用,用 source === 'builtin-command' 守卫,处理无会话时技能缺乏可靠 source 的情况,加 EN+ZH 翻译,并写覆盖自定义命令重名和未知技能回退的单测。

PR 的方案与我的完全一致。两个干净的 Record<string, string> 映射(BUILTIN_COMMAND_DESCRIPTION_KEYSSKILL_DESCRIPTION_KEYS),独立的 localizeBuiltinDescriptions() 步骤,技能在现有的打标步骤中按名字本地化 —— 鉴于技能只有在会话建立后才有可靠的 source,这是正确的选择。没有过度抽象,没有投机代码。

复用检查:仓库内没有现成的方案 —— getLocalCommands()(fallback 列表)和 daemon 广播命令之间的缺口正是这个 PR 填补的。MessagesRecord<string, MessageValue>,新增 i18n key 类型安全,无需类型改动。

无关键阻塞问题。无 AGENTS.md 违规。

测试结果

新单测 (localCommands.test.ts) — 9/9 通过:

  • getLocalCommands 翻译 fallback 描述
  • localizeBuiltinDescriptions 重译内置、保留自定义命令、跳过未映射内置、跳过技能、保留其他字段
  • skillDescriptionKey 映射已知技能、未知返回 undefined
  • 完整 App pipeline 测试:命令 + bundled 技能 + 项目技能 + 未知技能 + 自定义命令,无需会话

完整 web-shell 测试套件 — 65 个文件 986/986 通过(3.6s)。无回归。

类型检查tsc -p packages/web-shell/tsconfig.json --noEmitMessageList.test.tsMarkdown.test.ts 报错,但这些错误先于 PR 存在(在 main 上、无 PR 代码时完全相同)。非本 PR 引入。

真实场景测试

⚠️ 三态环境无法运行。 这是浏览器 UI 改动(web-shell 斜杠菜单描述),不是 CLI 行为变更。项目没有针对 web-shell 的 Playwright/浏览器 E2E 基础设施,基于 tmux 的真实场景测试仅针对 CLI。作者自己的 Playwright 验证(真实 dev:daemon + Chromium 读取 zh-CN 菜单 DOM)是正确的验证路径;PR 正文中的 before/after 截图就是证据。

总结

简洁、最小的 i18n 修复,测试覆盖强。无正确性、安全或回归问题。唯一的验证缺口是三态无法独立驱动浏览器确认渲染后的菜单,但单测全面覆盖了转换 pipeline,作者的 Playwright 实机证据可信。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Reflection

Stepping back: this is a straightforward, well-scoped i18n fix that solves a real user-visible problem — the slash-menu language mix in zh-CN sessions. The motivation is genuine, the approach matches what I would have proposed independently, and the test coverage is strong.

A few reflection points:

  • Does it solve a real user problem? Yes. The before/after screenshots make the issue concrete. Anyone using the web-shell in a non-English UI language hits this.
  • Is the code straightforward? Yes. Two Record<string, string> maps, a pure function, and a three-line wiring change in App.tsx. No cleverness.
  • Is every change necessary? Yes. The command map, skill map, localization function, translations (EN+ZH), and tests are all load-bearing for the stated goal. Nothing feels bolted on.
  • Maintainability? The curated name→key maps are the accepted tradeoff — adding a new built-in command or skill means adding one line to the map and one EN/ZH translation. That's documented in the code comments. A future SKILL.md i18n mechanism could supersede the skill side, but the author has explicitly scoped that out as a follow-up.
  • Independent proposal match? The PR's approach matches my independent proposal exactly. I don't see a materially simpler path.
  • Pattern / volume check? Single PR, single concern. Not part of a spray.
  • Pushover check? I'm not approving because I ran out of reasons to say no — I'm approving because the change is genuinely clean, the tests are comprehensive (9 new + 986 existing), and the scope is tight.

The one verification gap — triage cannot drive a browser to confirm the rendered slash menu — is honestly flagged in Stage 2, and the author's Playwright evidence is the appropriate substitute. Unit tests comprehensively cover the pipeline.

Approving. ✅

中文说明

反思

退一步看:这是一个直接、范围明确的 i18n 修复,解决了一个真实可见的用户问题 —— zh-CN 会话下斜杠菜单语言混杂。动机真实,方案与我独立提出的方案一致,测试覆盖强。

几个反思维度:

  • 是否解决真实用户问题? 是的。before/after 截图把问题具体化了。任何用非英文 UI 语言使用 web-shell 的人都会遇到。
  • 代码是否直观? 是的。两个 Record<string, string> 映射,一个纯函数,加上 App.tsx 中三行的接线改动。没有花哨的东西。
  • 每个改动都是必要的吗? 是的。命令映射、技能映射、本地化函数、翻译(EN+ZH)、测试都是实现目标的必要部分。没有多余的附加物。
  • 可维护性? 策划好的 name→key 映射是接受的权衡 —— 新增一个内置命令或技能只需在映射中加一行、加一条 EN/ZH 翻译。代码注释里已经说明。未来 SKILL.md i18n 机制可以替代技能侧,但作者已经明确将其作为后续工作排除。
  • 独立方案匹配? PR 的方案与我独立提出的方案完全一致。我没看到更简单的路径。
  • 模式 / 数量检查? 单个 PR,单个关注点。不是一连串 PR 的一部分。
  • 烂好人检查? 我不是因为找不到拒绝理由才批准的 —— 我批准是因为这个改动确实干净,测试全面(9 个新测试 + 986 个现有测试),范围紧凑。

唯一的验证缺口 —— 三态无法驱动浏览器确认渲染后的斜杠菜单 —— 已在 Stage 2 中诚实说明,作者的 Playwright 证据是恰当的替代。单测全面覆盖了 pipeline。

批准。✅

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

expect(byName.get('bugfix')).toBe('按先复现流程修复 GitHub issue 中的 bug'); // project skill
expect(byName.get('my-skill')).toBe('my custom skill'); // unknown skill untouched
expect(byName.get('deploy')).toBe('ship it'); // custom command untouched
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Consider adding a test that validates every i18n key in BUILTIN_COMMAND_DESCRIPTION_KEYS and SKILL_DESCRIPTION_KEYS resolves to a real translation in both EN and ZH dictionaries. Currently, if a future contributor adds a map entry but forgets the matching local.* or skilldesc.* key in i18n.tsx, the getTranslator fallback chain (messages[key] ?? EN[key] ?? key) silently shows the raw key string (e.g. "skilldesc.newSkill") in the slash menu — a user-visible regression with no error signal.

A simple test would catch this:

import { BUILTIN_COMMAND_DESCRIPTION_KEYS, SKILL_DESCRIPTION_KEYS } from './localCommands';

const allKeys = [
  ...Object.values(BUILTIN_COMMAND_DESCRIPTION_KEYS),
  ...Object.values(SKILL_DESCRIPTION_KEYS),
];

it('every i18n key resolves in both EN and ZH', () => {
  for (const key of allKeys) {
    expect(en(key), `EN missing: ${key}`).not.toBe(key);
    expect(zh(key), `ZH missing: ${key}`).not.toBe(key);
  }
});

This makes the three-way sync (map → EN → ZH) self-enforcing via CI.

— qwen3.7-max via Qwen Code /review

@ytahdn ytahdn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed — no blockers. Suggestion-level recommendations are in the Suggestion summary comment below.

@ytahdn

ytahdn commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

Suggestions — commit 88e31c82d

File Issue Suggested fix
packages/web-shell/client/constants/localCommands.ts:197 Prototype chain pollution in skillDescriptionKey(). SKILL_DESCRIPTION_KEYS and BUILTIN_COMMAND_DESCRIPTION_KEYS are plain {} objects inheriting from Object.prototype. A skill or command named constructor, valueOf, hasOwnProperty, or toString returns the inherited method instead of undefined. For constructor/valueOf, t() calls the inherited method which returns {} — an object that React cannot render as a child, crashing the component. For hasOwnProperty, t() returns "false" (the result of calling hasOwnProperty(undefined)). Exploitability is low (requires a skill with a prototype-method name), but the fix is two lines. Use Object.hasOwn(): return Object.hasOwn(SKILL_DESCRIPTION_KEYS, name) ? SKILL_DESCRIPTION_KEYS[name] : undefined; — or Object.create(null) for both maps.
packages/web-shell/client/App.tsx:3399 Missing fallback when t(skillKey) returns a raw key string. If an i18n key is missing from both EN and ZH dictionaries, t() returns the raw key (e.g., "skilldesc.newSkill"). The code unconditionally uses t(skillKey) when the skill is known, discarding the daemon's English description. Pre-PR behavior was better: `command.description
packages/web-shell/client/constants/localCommands.ts:135-200 No automated check for the 3-way sync between key maps (BUILTIN_COMMAND_DESCRIPTION_KEYS, SKILL_DESCRIPTION_KEYS), EN translations, and ZH translations in i18n.tsx. Messages is Record<string, MessageValue>, so TypeScript won't flag a typo or missing entry. A mismatch silently shows the raw key string to users. Add a unit test that iterates both maps and asserts every key resolves in EN and ZH to something other than the raw key string. Export the maps or test via skillDescriptionKey + localizeBuiltinDescriptions.
packages/web-shell/client/App.tsx:3396 Hardcoded skill names override user-authored descriptions by name. SKILL_DESCRIPTION_KEYS contains common words (review, batch, loop, simplify, bugfix). Any user skill sharing these names silently gets the bundled skill's localized description instead of the user's own. The PR acknowledges the tradeoff (name-only matching is needed for the pre-session welcome screen where source is unreliable). When command.source IS available, gate on it: only localize when source is 'bundled-skill' or matches the project-skill source pattern. Fall through to the authored description for unknown sources.

— qwen3.7-max via Qwen Code /review

@ytahdn ytahdn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! ✅

— qwen3.7-max via Qwen Code /review

@wenshao
wenshao added this pull request to the merge queue Jul 5, 2026
Merged via the queue into QwenLM:main with commit adda526 Jul 5, 2026
53 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants