diff --git a/.gitignore b/.gitignore index 828c8cee..e4c8f5c1 100644 --- a/.gitignore +++ b/.gitignore @@ -74,3 +74,7 @@ runtime/__pycache__ # Codex logs .codex-logs/ + +# Local AI assistant artifacts (graph, notes) +CLAUDE.md +graphify-out/ diff --git a/desktop/src/components/chat/ChatInput.tsx b/desktop/src/components/chat/ChatInput.tsx index 48bc7aae..cddca7f3 100644 --- a/desktop/src/components/chat/ChatInput.tsx +++ b/desktop/src/components/chat/ChatInput.tsx @@ -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(() => { diff --git a/desktop/src/components/chat/composerUtils.test.ts b/desktop/src/components/chat/composerUtils.test.ts index 9a1ad7e4..a9a91011 100644 --- a/desktop/src/components/chat/composerUtils.test.ts +++ b/desktop/src/components/chat/composerUtils.test.ts @@ -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' }, ]), ) }) diff --git a/desktop/src/components/chat/composerUtils.ts b/desktop/src/components/chat/composerUtils.ts index 884903d9..ccf967ae 100644 --- a/desktop/src/components/chat/composerUtils.ts +++ b/desktop/src/components/chat/composerUtils.ts @@ -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 = { + 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: '[ | clear]', - }, + { name: 'goal', description: 'Set a completion goal', argumentHint: '[ | 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, fallback: ReadonlyArray = FALLBACK_SLASH_COMMANDS, ): SlashCommandOption[] { + const fallbackByName = new Map() + for (const command of fallback) { + if (command?.name) fallbackByName.set(command.name, command) + } + const merged = new Map() 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) } diff --git a/desktop/src/i18n/locales/en.ts b/desktop/src/i18n/locales/en.ts index 34732afa..ed926621 100644 --- a/desktop/src/i18n/locales/en.ts +++ b/desktop/src/i18n/locales/en.ts @@ -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?', diff --git a/desktop/src/i18n/locales/zh.ts b/desktop/src/i18n/locales/zh.ts index c337bef1..bba22d0c 100644 --- a/desktop/src/i18n/locales/zh.ts +++ b/desktop/src/i18n/locales/zh.ts @@ -1572,6 +1572,32 @@ export const zh: Record = { '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': '会话运行中', diff --git a/desktop/src/pages/EmptySession.tsx b/desktop/src/pages/EmptySession.tsx index 82469abb..ee57aa14 100644 --- a/desktop/src/pages/EmptySession.tsx +++ b/desktop/src/pages/EmptySession.tsx @@ -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) => {