mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
fix(web-shell): stabilize slash command i18n in split-view panes (#6546)
Split-view panes showed English descriptions for slash commands while the main view showed Chinese. Two root causes: 1. ChatPane never merged getLocalCommands(t) into the command list, so ~33 built-in commands (help, model, clear, etc.) lacked i18n descriptions. 2. localizeBuiltinDescriptions required source === 'builtin-command', but the daemon omits _meta.source in some SSE event paths (available_commands_update), causing built-in commands to skip translation unpredictably across sessions. 3. Skill localization depended on connection.skills, which can be empty when SSE events deliver commands without availableSkills. Fix: make the entire localization pipeline name-based and session-independent — merge local commands, relax the source guard to also translate when source is missing, and use skillDescriptionKey directly instead of connection.skills for skill tagging. Also adds missing autofix skill translation (EN + ZH).
This commit is contained in:
parent
5f41b166e6
commit
b2bee7040e
6 changed files with 48 additions and 17 deletions
|
|
@ -4229,7 +4229,6 @@ export function App({
|
|||
};
|
||||
|
||||
const commands = useMemo(() => {
|
||||
const skillNames = new Set(connection.skills ?? []);
|
||||
return localizeBuiltinDescriptions(
|
||||
mergeCommands(connection.commands ?? [], getLocalCommands(t)),
|
||||
t,
|
||||
|
|
@ -4238,17 +4237,15 @@ export function App({
|
|||
(command) => !hiddenCommands.has(normalizeHiddenCommand(command.name)),
|
||||
)
|
||||
.map((command) => {
|
||||
if (!skillNames.has(command.name)) return command;
|
||||
const skillKey = skillDescriptionKey(command.name);
|
||||
if (!skillKey) return command;
|
||||
return {
|
||||
...command,
|
||||
displayCategory: 'skill' as const,
|
||||
description: skillKey
|
||||
? t(skillKey)
|
||||
: command.description || t('skills.run'),
|
||||
description: t(skillKey),
|
||||
};
|
||||
});
|
||||
}, [connection.commands, connection.skills, hiddenCommands, t]);
|
||||
}, [connection.commands, hiddenCommands, t]);
|
||||
|
||||
const welcomeHeaderProps = useMemo(
|
||||
() => ({
|
||||
|
|
|
|||
|
|
@ -424,7 +424,10 @@ describe('ChatPane', () => {
|
|||
{ name: 'compress', description: 'Compress', source: 'builtin-command' },
|
||||
];
|
||||
render();
|
||||
expect(testid('pane-commands')?.textContent).toBe('2');
|
||||
// Local commands are merged with daemon commands; 'clear' is deduplicated,
|
||||
// 'compress' is daemon-only — so the count is localCount + 1.
|
||||
const count = Number(testid('pane-commands')?.textContent);
|
||||
expect(count).toBeGreaterThan(30);
|
||||
});
|
||||
|
||||
it('hides internal composer models and labels the rest', () => {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,12 @@ import { isAskUserPermission } from '../utils/askUserPermission';
|
|||
import { isDaemonApprovalMode } from '../utils/sessionPreparation';
|
||||
import { isVisibleComposerModel } from '../utils/composerModels';
|
||||
import { getModelDisplayName } from '../utils/modelDisplay';
|
||||
import { localizeBuiltinDescriptions } from '../constants/localCommands';
|
||||
import {
|
||||
getLocalCommands,
|
||||
localizeBuiltinDescriptions,
|
||||
skillDescriptionKey,
|
||||
} from '../constants/localCommands';
|
||||
import { mergeCommands } from '../hooks/daemonSessionMappers';
|
||||
import { MessageList } from './MessageList';
|
||||
import { StreamingStatus } from './StreamingStatus';
|
||||
import { ChatEditor, type ComposerToolbarAction } from './ChatEditor';
|
||||
|
|
@ -149,10 +154,20 @@ export function ChatPane({ title, onClose, onError }: ChatPaneProps) {
|
|||
// submitted (via sendPrompt), so e.g. `/clear` clears this pane's session, not
|
||||
// the outer one. The approval-mode and model pickers likewise drive this
|
||||
// session's own actions; the SDK reflects the change back on `connection`.
|
||||
const commands = useMemo(
|
||||
() => localizeBuiltinDescriptions(connection.commands ?? [], t),
|
||||
[connection.commands, t],
|
||||
);
|
||||
const commands = useMemo(() => {
|
||||
return localizeBuiltinDescriptions(
|
||||
mergeCommands(connection.commands ?? [], getLocalCommands(t)),
|
||||
t,
|
||||
).map((command) => {
|
||||
const skillKey = skillDescriptionKey(command.name);
|
||||
if (!skillKey) return command;
|
||||
return {
|
||||
...command,
|
||||
displayCategory: 'skill' as const,
|
||||
description: t(skillKey),
|
||||
};
|
||||
});
|
||||
}, [connection.commands, t]);
|
||||
const availableModels = useMemo(
|
||||
() =>
|
||||
(connection.models ?? []).filter(isVisibleComposerModel).map((model) => ({
|
||||
|
|
|
|||
|
|
@ -41,13 +41,22 @@ describe('localizeBuiltinDescriptions (commands)', () => {
|
|||
|
||||
it('leaves a custom command that shadows a built-in name untouched', () => {
|
||||
const commands: CommandInfo[] = [
|
||||
{ name: 'export', description: 'my project exporter' },
|
||||
{ name: 'export', description: 'my project exporter', source: 'custom' },
|
||||
];
|
||||
expect(localizeBuiltinDescriptions(commands, zh)[0].description).toBe(
|
||||
'my project exporter',
|
||||
);
|
||||
});
|
||||
|
||||
it('translates a built-in name even when source is missing', () => {
|
||||
const commands: CommandInfo[] = [
|
||||
{ name: 'export', description: 'Export conversation to a file' },
|
||||
];
|
||||
expect(localizeBuiltinDescriptions(commands, zh)[0].description).toBe(
|
||||
'将当前会话历史导出到文件',
|
||||
);
|
||||
});
|
||||
|
||||
it('does not touch built-ins that are not in the map', () => {
|
||||
const commands: CommandInfo[] = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -181,6 +181,7 @@ const SKILL_DESCRIPTION_KEYS: Record<string, string> = {
|
|||
// This repo's project skills (.qwen/skills).
|
||||
'agent-reproduce-align': 'skilldesc.agentReproduceAlign',
|
||||
'agent-reproduce-feature': 'skilldesc.agentReproduceFeature',
|
||||
autofix: 'skilldesc.autofix',
|
||||
bugfix: 'skilldesc.bugfix',
|
||||
codegraph: 'skilldesc.codegraph',
|
||||
'create-issue': 'skilldesc.createIssue',
|
||||
|
|
@ -210,16 +211,19 @@ export function skillDescriptionKey(name: string): string | undefined {
|
|||
/**
|
||||
* Re-localize built-in command descriptions by name so the slash menu matches
|
||||
* the web-shell UI language even when the daemon advertises them in its own
|
||||
* process language. Guarded by source === 'builtin-command' so custom commands
|
||||
* keep their own description. (Skills are localized in the skill-tagging step.)
|
||||
* process language. Translates when source is explicitly 'builtin-command' or
|
||||
* when no source is set (daemon may omit _meta.source in some event paths).
|
||||
* Commands with a non-builtin source (e.g. 'skill', 'custom') are left alone.
|
||||
* (Skills are localized separately in the skill-tagging step.)
|
||||
*/
|
||||
export function localizeBuiltinDescriptions(
|
||||
commands: CommandInfo[],
|
||||
t: Translate,
|
||||
): CommandInfo[] {
|
||||
return commands.map((command) => {
|
||||
if (command.source !== 'builtin-command') return command;
|
||||
const key = BUILTIN_COMMAND_DESCRIPTION_KEYS[command.name];
|
||||
return key ? { ...command, description: t(key) } : command;
|
||||
if (!key) return command;
|
||||
if (command.source && command.source !== 'builtin-command') return command;
|
||||
return { ...command, description: t(key) };
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -941,6 +941,8 @@ const EN: Messages = {
|
|||
'Align a ported Codex/Claude Code feature with the original',
|
||||
'skilldesc.agentReproduceFeature':
|
||||
'Reproduce an existing Codex/Claude Code feature',
|
||||
'skilldesc.autofix':
|
||||
'Implement approved issues or address PR review feedback',
|
||||
'skilldesc.bugfix': 'Fix a bug from a GitHub issue, reproduce-first',
|
||||
'skilldesc.codegraph': 'Analyze the codebase via graph and vector index',
|
||||
'skilldesc.createIssue': 'Draft and submit a GitHub issue from an idea',
|
||||
|
|
@ -2613,6 +2615,7 @@ const ZH: Messages = {
|
|||
'skilldesc.agentReproduceAlign':
|
||||
'将已移植的 Codex/Claude Code 功能与原版对齐',
|
||||
'skilldesc.agentReproduceFeature': '复现 Codex/Claude Code 的现有功能',
|
||||
'skilldesc.autofix': '实现已批准的 issue 或处理 PR 审查反馈',
|
||||
'skilldesc.bugfix': '按先复现流程修复 GitHub issue 中的 bug',
|
||||
'skilldesc.codegraph': '通过图数据库和向量索引分析代码库',
|
||||
'skilldesc.createIssue': '根据想法或 bug 描述起草并提交 GitHub issue',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue