mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-15 12:53:31 +08:00
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:
parent
91224cbd20
commit
2eaae94d40
4
.gitignore
vendored
4
.gitignore
vendored
@ -74,3 +74,7 @@ runtime/__pycache__
|
||||
|
||||
# Codex logs
|
||||
.codex-logs/
|
||||
|
||||
# Local AI assistant artifacts (graph, notes)
|
||||
CLAUDE.md
|
||||
graphify-out/
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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' },
|
||||
]),
|
||||
)
|
||||
})
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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?',
|
||||
|
||||
@ -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': '会话运行中',
|
||||
|
||||
@ -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) => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user