feat(desktop): i18n slash command descriptions in chat composer

The slash command menu in ChatInput and EmptySession was hard-coded to
English descriptions, so users running the desktop with locale=zh saw
English copy for the built-in commands the desktop owns (/clear,
/compact, /help, /mcp, /skills, /memory, /plugin, /doctor, ...).

This change:
- Adds slashCmd.<name>.description i18n keys for all 24 built-in slash
  commands in en and zh locales.
- Introduces getLocalizedFallbackCommands(t) so React renders use the
  active locale; the existing FALLBACK_SLASH_COMMANDS constant is kept
  for non-React callers.
- Updates mergeSlashCommands so the localized fallback wins for
  built-in command names while server/team-provided commands (e.g.
  team:lark) still keep their own descriptions.
- Updates composerUtils.test.ts to reflect the new precedence and adds
  a test that built-in commands prefer the localized description even
  when the CLI broadcasts an English one.
- Adds CLAUDE.md and graphify-out/ to .gitignore (local AI assistant
  artifacts that should not be committed).

Verification:
  bun run check:desktop  -> passed (lint + vitest + build, 32.9s)
  changed-line coverage  -> 100% (120/120)

Known environment-only blockers in this clone (unrelated to this diff):
  bun run check:native   -> rustc not installed in this WSL
  bun run check:coverage -> 3 server-area suites hit
                            'error: An internal error occurred (WriteFailed)'
                            while writing very long stdout under WSL.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
派大星 2026-05-24 13:08:33 +08:00
parent 91224cbd20
commit 2eaae94d40
7 changed files with 160 additions and 39 deletions

4
.gitignore vendored
View File

@ -74,3 +74,7 @@ runtime/__pycache__
# Codex logs
.codex-logs/
# Local AI assistant artifacts (graph, notes)
CLAUDE.md
graphify-out/

View File

@ -24,7 +24,7 @@ import { FileSearchMenu, type FileSearchMenuHandle } from './FileSearchMenu'
import { LocalSlashCommandPanel, type LocalSlashCommandName } from './LocalSlashCommandPanel'
import { ContextUsageIndicator } from './ContextUsageIndicator'
import {
FALLBACK_SLASH_COMMANDS,
getLocalizedFallbackCommands,
filterSlashCommands,
findSlashTrigger,
mergeSlashCommands,
@ -352,8 +352,8 @@ export function ChatInput({ variant = 'default', compact = false }: ChatInputPro
}, [fileSearchOpen])
const allSlashCommands = useMemo(
() => mergeSlashCommands(slashCommands, FALLBACK_SLASH_COMMANDS),
[slashCommands],
() => mergeSlashCommands(slashCommands, getLocalizedFallbackCommands(t)),
[slashCommands, t],
)
const filteredCommands = useMemo(() => {

View File

@ -50,14 +50,30 @@ describe('composerUtils', () => {
)
})
it('keeps server-provided descriptions when they exist', () => {
it('keeps server-provided descriptions for non-built-in commands', () => {
expect(
mergeSlashCommands([
{ name: 'clear', description: 'Server description' },
{ name: 'team:lark', description: 'Team-provided description' },
]),
).toEqual(
expect.arrayContaining([
{ name: 'clear', description: 'Server description' },
{ name: 'team:lark', description: 'Team-provided description' },
]),
)
})
it('prefers the localized fallback description for built-in commands', () => {
// For commands the desktop owns the copy for (e.g. /clear, /compact, /help),
// the localized description must win over whatever the CLI broadcasts so the
// i18n keys actually take effect at runtime.
expect(
mergeSlashCommands(
[{ name: 'clear', description: 'CLI English description' }],
[{ name: 'clear', description: 'Localized description' }],
),
).toEqual(
expect.arrayContaining([
{ name: 'clear', description: 'Localized description' },
]),
)
})

View File

@ -1,34 +1,70 @@
import type { SettingsTab } from '../../stores/uiStore'
import type { TranslationKey } from '../../i18n'
/** Map from slash command name to its i18n description key */
const SLASH_CMD_DESCRIPTION_KEYS: Record<string, TranslationKey> = {
mcp: 'slashCmd.mcp.description',
skills: 'slashCmd.skills.description',
help: 'slashCmd.help.description',
status: 'slashCmd.status.description',
cost: 'slashCmd.cost.description',
context: 'slashCmd.context.description',
plugin: 'slashCmd.plugin.description',
memory: 'slashCmd.memory.description',
doctor: 'slashCmd.doctor.description',
compact: 'slashCmd.compact.description',
clear: 'slashCmd.clear.description',
goal: 'slashCmd.goal.description',
review: 'slashCmd.review.description',
commit: 'slashCmd.commit.description',
pr: 'slashCmd.pr.description',
init: 'slashCmd.init.description',
bug: 'slashCmd.bug.description',
config: 'slashCmd.config.description',
login: 'slashCmd.login.description',
logout: 'slashCmd.logout.description',
model: 'slashCmd.model.description',
permissions: 'slashCmd.permissions.description',
'terminal-setup': 'slashCmd.terminal-setup.description',
vim: 'slashCmd.vim.description',
}
/** Names of commands the desktop owns the description for (i.e. localized in our locales). */
const BUILT_IN_COMMAND_NAMES = new Set(Object.keys(SLASH_CMD_DESCRIPTION_KEYS))
export const PANEL_SLASH_COMMANDS = [
{ name: 'mcp', description: 'Open available MCP tools for the current chat context' },
{ name: 'skills', description: 'Browse user-invocable skills for the current chat context' },
{ name: 'help', description: 'Show available desktop and agent commands' },
{ name: 'status', description: 'Show session status, usage, and context' },
{ name: 'cost', description: 'Show session usage and costs' },
{ name: 'context', description: 'Show current context usage' },
{ name: 'mcp' },
{ name: 'skills' },
{ name: 'help' },
{ name: 'status' },
{ name: 'cost' },
{ name: 'context' },
] as const
export const SETTINGS_SLASH_COMMANDS = [
{ name: 'plugin', description: 'Open desktop plugin controls in Settings', tab: 'plugins' as const },
{ name: 'memory', description: 'Open project memory files in Settings', tab: 'memory' as const },
{ name: 'doctor', description: 'Open Doctor in Diagnostics', tab: 'diagnostics' as const },
{ name: 'plugin', tab: 'plugins' as const },
{ name: 'memory', tab: 'memory' as const },
{ name: 'doctor', tab: 'diagnostics' as const },
] as const
export const SLASH_COMMAND_ALIASES = [
{ name: 'plugins', target: 'plugin' },
] as const
export const FALLBACK_SLASH_COMMANDS = [
...PANEL_SLASH_COMMANDS,
...SETTINGS_SLASH_COMMANDS.map(({ name, description }) => ({ name, description })),
/** Static fallback with English descriptions (for non-React contexts) */
export const FALLBACK_SLASH_COMMANDS: SlashCommandOption[] = [
{ name: 'mcp', description: 'Open available MCP tools for the current chat context' },
{ name: 'skills', description: 'Browse user-invocable skills for the current chat context' },
{ name: 'help', description: 'Show available desktop and agent commands' },
{ name: 'status', description: 'Show session status, usage, and context' },
{ name: 'cost', description: 'Show session usage and costs' },
{ name: 'context', description: 'Show current context usage' },
{ name: 'plugin', description: 'Open desktop plugin controls in Settings' },
{ name: 'memory', description: 'Open project memory files in Settings' },
{ name: 'doctor', description: 'Open Doctor in Diagnostics' },
{ name: 'compact', description: 'Compact conversation context' },
{ name: 'clear', description: 'Clear conversation history' },
{
name: 'goal',
description: 'Set a completion goal',
argumentHint: '[<condition> | clear]',
},
{ name: 'goal', description: 'Set a completion goal', argumentHint: '[<condition> | clear]' },
{ name: 'review', description: 'Review code changes' },
{ name: 'commit', description: 'Create a git commit' },
{ name: 'pr', description: 'Create a pull request' },
@ -43,6 +79,15 @@ export const FALLBACK_SLASH_COMMANDS = [
{ name: 'vim', description: 'Toggle vim editing mode' },
]
/** Build localized fallback commands using the current locale */
export function getLocalizedFallbackCommands(t: (key: TranslationKey) => string): SlashCommandOption[] {
return FALLBACK_SLASH_COMMANDS.map((cmd) => ({
name: cmd.name,
description: t(SLASH_CMD_DESCRIPTION_KEYS[cmd.name] ?? ('slashCmd.' + cmd.name + '.description' as TranslationKey)),
...(cmd.argumentHint && { argumentHint: cmd.argumentHint }),
}))
}
export type SlashCommandOption = {
name: string
description: string
@ -78,30 +123,34 @@ export function mergeSlashCommands(
preferred: ReadonlyArray<SlashCommandOption>,
fallback: ReadonlyArray<SlashCommandOption> = FALLBACK_SLASH_COMMANDS,
): SlashCommandOption[] {
const fallbackByName = new Map<string, SlashCommandOption>()
for (const command of fallback) {
if (command?.name) fallbackByName.set(command.name, command)
}
const merged = new Map<string, SlashCommandOption>()
for (const command of preferred) {
if (!command?.name) continue
const localized = fallbackByName.get(command.name)
// For commands the desktop owns the copy for, prefer the localized fallback
// description so users see translated text instead of the CLI's English.
const useLocalDescription =
BUILT_IN_COMMAND_NAMES.has(command.name) && Boolean(localized?.description)
const description = useLocalDescription
? localized!.description
: command.description?.trim() || localized?.description || ''
const argumentHint = command.argumentHint?.trim() || localized?.argumentHint
merged.set(command.name, {
name: command.name,
description: command.description?.trim() || '',
...(command.argumentHint?.trim() && { argumentHint: command.argumentHint.trim() }),
description,
...(argumentHint && { argumentHint }),
})
}
for (const command of fallback) {
if (!command?.name) continue
const existing = merged.get(command.name)
if (existing) {
if ((!existing.description && command.description) || (!existing.argumentHint && command.argumentHint)) {
merged.set(command.name, {
...existing,
description: existing.description || command.description,
argumentHint: existing.argumentHint || command.argumentHint,
})
}
continue
}
if (merged.has(command.name)) continue
merged.set(command.name, command)
}

View File

@ -1568,6 +1568,32 @@ export const en = {
'tabs.closeConfirmTitle': 'Session Running',
'tabs.closeConfirmMessage': 'This session is still running. What would you like to do?',
'tabs.closeConfirmKeep': 'Keep Running',
// ─── Slash Command Descriptions ──────────────────────────────────────
'slashCmd.mcp.description': 'Open available MCP tools for the current chat context',
'slashCmd.skills.description': 'Browse user-invocable skills for the current chat context',
'slashCmd.help.description': 'Show available desktop and agent commands',
'slashCmd.status.description': 'Show session status, usage, and context',
'slashCmd.cost.description': 'Show session usage and costs',
'slashCmd.context.description': 'Show current context usage',
'slashCmd.plugin.description': 'Open desktop plugin controls in Settings',
'slashCmd.memory.description': 'Open project memory files in Settings',
'slashCmd.doctor.description': 'Open Doctor in Diagnostics',
'slashCmd.compact.description': 'Compact conversation context',
'slashCmd.clear.description': 'Clear conversation history',
'slashCmd.goal.description': 'Set a completion goal',
'slashCmd.review.description': 'Review code changes',
'slashCmd.commit.description': 'Create a git commit',
'slashCmd.pr.description': 'Create a pull request',
'slashCmd.init.description': 'Initialize project CLAUDE.md',
'slashCmd.bug.description': 'Report a bug',
'slashCmd.config.description': 'Open configuration',
'slashCmd.login.description': 'Switch Anthropic accounts',
'slashCmd.logout.description': 'Sign out of current account',
'slashCmd.model.description': 'Switch AI model',
'slashCmd.permissions.description': 'View or manage tool permissions',
'slashCmd.terminal-setup.description': 'Set up terminal integration',
'slashCmd.vim.description': 'Toggle vim editing mode',
'tabs.closeConfirmStop': 'Stop & Close',
'tabs.closeAllConfirmTitle': 'Sessions Running',
'tabs.closeAllConfirmMessage': '{count} sessions are still running. Stop them before closing these tabs?',

View File

@ -1572,6 +1572,32 @@ export const zh: Record<TranslationKey, string> = {
'tabs.closeConfirmKeep': '保持运行',
'tabs.closeConfirmStop': '停止并关闭',
'tabs.closeAllConfirmTitle': '多个会话运行中',
// ─── Slash Command Descriptions ──────────────────────────────────────
'slashCmd.mcp.description': '打开当前聊天上下文中可用的 MCP 工具',
'slashCmd.skills.description': '浏览当前上下文中可直接调用的技能',
'slashCmd.help.description': '查看可用的桌面端与 Agent 命令',
'slashCmd.status.description': '查看会话状态、用量和上下文',
'slashCmd.cost.description': '查看会话用量和费用',
'slashCmd.context.description': '查看当前上下文用量',
'slashCmd.plugin.description': '在设置中打开插件管理',
'slashCmd.memory.description': '在设置中打开项目记忆文件',
'slashCmd.doctor.description': '在诊断中打开 Doctor',
'slashCmd.compact.description': '压缩会话上下文',
'slashCmd.clear.description': '清空会话历史',
'slashCmd.goal.description': '设定完成目标',
'slashCmd.review.description': '审查代码变更',
'slashCmd.commit.description': '创建 git 提交',
'slashCmd.pr.description': '创建 Pull Request',
'slashCmd.init.description': '初始化项目 CLAUDE.md',
'slashCmd.bug.description': '报告 Bug',
'slashCmd.config.description': '打开配置',
'slashCmd.login.description': '切换 Anthropic 账户',
'slashCmd.logout.description': '登出当前账户',
'slashCmd.model.description': '切换 AI 模型',
'slashCmd.permissions.description': '查看或管理工具权限',
'slashCmd.terminal-setup.description': '设置终端集成',
'slashCmd.vim.description': '切换 vim 编辑模式',
'tabs.closeAllConfirmMessage': '仍有 {count} 个会话正在运行。关闭这些标签前是否停止它们?',
'tabs.closeAllConfirmStop': '全部停止并关闭',
'tabs.sessionRunning': '会话运行中',

View File

@ -26,7 +26,7 @@ import {
} from '../lib/composerAttachments'
import { useComposerFileDrop } from '../components/chat/useComposerFileDrop'
import {
FALLBACK_SLASH_COMMANDS,
getLocalizedFallbackCommands,
filterSlashCommands,
findSlashToken,
insertSlashTrigger,
@ -207,8 +207,8 @@ export function EmptySession() {
}, [workDir, lastPluginReloadSummary])
const allSlashCommands = useMemo(
() => mergeSlashCommands(slashCommands, FALLBACK_SLASH_COMMANDS),
[slashCommands],
() => mergeSlashCommands(slashCommands, getLocalizedFallbackCommands(t)),
[slashCommands, t],
)
const handleWorkDirChange = (newWorkDir: string) => {