Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions packages/web-shell/client/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4116,7 +4116,6 @@ export function App({
};

const commands = useMemo(() => {
const skillNames = new Set(connection.skills ?? []);
return localizeBuiltinDescriptions(
mergeCommands(connection.commands ?? [], getLocalCommands(t)),
t,
Expand All @@ -4125,17 +4124,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(
() => ({
Expand Down
5 changes: 4 additions & 1 deletion packages/web-shell/client/components/ChatPane.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
25 changes: 20 additions & 5 deletions packages/web-shell/client/components/ChatPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) => ({
Expand Down
11 changes: 10 additions & 1 deletion packages/web-shell/client/constants/localCommands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [
{
Expand Down
12 changes: 8 additions & 4 deletions packages/web-shell/client/constants/localCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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) };
});
}
3 changes: 3 additions & 0 deletions packages/web-shell/client/i18n.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down
Loading