Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/refresh-slash-menu-on-lang-switch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Refresh localized slash-command descriptions immediately after language switches or TUI config reloads, and update the Chinese thinking-mode footer label.
5 changes: 5 additions & 0 deletions .changeset/tui-i18n-language-footer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": minor
---

Add a `language` setting to `tui.toml` (`auto` | `en` | `zh-CN`) and render the footer status bar in Simplified Chinese when Chinese is selected or detected from the system locale.
5 changes: 5 additions & 0 deletions .changeset/tui-i18n-language-selector.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": minor
---

Add a Language entry to `/settings` for switching the UI language at runtime. Picking Auto / English / 简体中文 flips the interface immediately (no restart), persists to `tui.toml`, and is re-applied on startup and by `/reload`.
5 changes: 5 additions & 0 deletions .changeset/tui-i18n-reverse-rpc-panels.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": minor
---

Translate the reverse-RPC approval and question panels through the i18n layer: headers, choice buttons, danger labels, prompts, and key hints now render in the active UI language, with `en` and `zh-CN` translations under the new `reverseRpc` namespace.
1 change: 1 addition & 0 deletions apps/kimi-code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"type": "module",
"imports": {
"#/tui/theme": "./src/tui/theme/index.ts",
"#/tui/i18n": "./src/tui/i18n/index.ts",
"#/cli/sub/server": "./src/cli/sub/server/index.ts",
"#/cli/sub/server/*": "./src/cli/sub/server/*.ts",
"#/*": [
Expand Down
15 changes: 13 additions & 2 deletions apps/kimi-code/src/cli/run-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { detectPendingMigration } from '#/migration/index';
import type { TuiConfig } from '#/tui/config';
import { loadTuiConfig, TuiConfigParseError } from '#/tui/config';
import { CHROME_GUTTER } from '#/tui/constant/rendering';
import { i18n, resolveLocale } from '#/tui/i18n';
import { KimiTUI } from '#/tui/index';
import { currentTheme, getColorPalette } from '#/tui/theme';
import { combineStartupNotice } from '#/tui/utils/startup';
Expand All @@ -38,19 +39,29 @@ export async function runShell(
const startedAt = Date.now();
const configStartedAt = startedAt;
let tuiConfig: TuiConfig;
let configWarning: string | undefined;
// The config-parse notice is rendered through i18n *after* the locale is
// resolved below — config load runs before we know the language, so we only
// record that it failed here and translate once the locale is available.
let configParseFailed = false;
try {
tuiConfig = await loadTuiConfig();
} catch (error) {
if (!(error instanceof TuiConfigParseError)) throw error;
tuiConfig = error.fallback;
configWarning = error.message;
configParseFailed = true;
}

// Initialise the global Theme singleton before pi-tui grabs stdin.
const palette = await getColorPalette(tuiConfig.theme);
currentTheme.setPalette(palette);

// Initialise the global I18n singleton alongside the theme: resolve the
// configured language (including `'auto'`) to a concrete locale so the TUI
// renders in the right language from the first frame.
i18n.setLocale(resolveLocale(tuiConfig.language));

let configWarning = configParseFailed ? i18n.t('cli.config.invalidTuiConfig') : undefined;

const workDir = process.cwd();
const telemetryBootstrap = createCliTelemetryBootstrap();
const telemetryClient: TelemetryClient = {
Expand Down
73 changes: 72 additions & 1 deletion apps/kimi-code/src/tui/commands/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
} from '@moonshot-ai/kimi-code-sdk';

import { EditorSelectorComponent } from '../components/dialogs/editor-selector';
import { LanguageSelectorComponent } from '../components/dialogs/language-selector';
import {
ExperimentsSelectorComponent,
type ExperimentalFeatureDraftChange,
Expand All @@ -16,6 +17,8 @@ import { SettingsSelectorComponent, type SettingsSelection } from '../components
import { ThemeSelectorComponent } from '../components/dialogs/theme-selector';
import { UpdatePreferenceSelectorComponent } from '../components/dialogs/update-preference-selector';
import { saveTuiConfig } from '../config';
import type { TuiLanguage } from '../config';
import { i18n, resolveLocale } from '#/tui/i18n';
import type { ThemeName } from '#/tui/theme';
import { currentTheme, isBuiltInTheme, lightColors, loadCustomThemeMerged } from '#/tui/theme';
import { NO_ACTIVE_SESSION_MESSAGE } from '../constant/kimi-tui';
Expand Down Expand Up @@ -274,6 +277,7 @@ async function applyEditorChoice(host: SlashCommandHost, value: string): Promise
try {
await saveTuiConfig({
theme: host.state.appState.theme,
language: host.state.appState.language,
editorCommand,
notifications: host.state.appState.notifications,
upgrade: host.state.appState.upgrade,
Expand Down Expand Up @@ -424,6 +428,7 @@ async function applyThemeChoice(host: SlashCommandHost, theme: ThemeName): Promi
try {
await saveTuiConfig({
theme,
language: host.state.appState.language,
editorCommand: host.state.appState.editorCommand,
notifications: host.state.appState.notifications,
upgrade: host.state.appState.upgrade,
Expand All @@ -446,6 +451,56 @@ async function applyThemeChoice(host: SlashCommandHost, theme: ThemeName): Promi
host.showStatus(`Theme set to "${theme}"${detail}.`);
}

function showLanguagePicker(host: SlashCommandHost): void {
host.mountEditorReplacement(
new LanguageSelectorComponent({
currentValue: host.state.appState.language,
onSelect: (value) => {
host.restoreEditor();
void applyLanguageChoice(host, value);
},
onCancel: () => {
host.restoreEditor();
},
}),
);
}

export async function applyLanguageChoice(host: LanguageHost, language: TuiLanguage): Promise<void> {
if (language === host.state.appState.language) {
host.showStatus(`Language unchanged: "${language}".`);
return;
}

try {
await saveTuiConfig({
theme: host.state.appState.theme,
language,
editorCommand: host.state.appState.editorCommand,
notifications: host.state.appState.notifications,
upgrade: host.state.appState.upgrade,
});
} catch (error) {
host.showStatus(
`Failed to save language: ${formatErrorMessage(error)}`,
'error',
);
return;
}

host.setAppState({ language });
// Flip the live UI locale and repaint so every component — footer, chrome,
// and any open dialog — re-renders in the new language without a restart.
i18n.setLocale(resolveLocale(language));
// The `/` autocomplete holds a one-time snapshot of localized command
// descriptions (see setupAutocomplete); rebuild it so the command menu
// follows the new locale immediately instead of only after a restart.
host.refreshSlashCommandAutocomplete();
host.state.ui.requestRender();
host.track('language_switch', { language });
host.showStatus(`Language set to "${language}".`);
}

export function showPermissionPicker(host: SlashCommandHost): void {
host.mountEditorReplacement(
new PermissionSelectorComponent({
Expand Down Expand Up @@ -542,11 +597,25 @@ function mountExperimentsPanel(
);
}

type LanguageHost = {
readonly state: {
readonly appState: Pick<
SlashCommandHost['state']['appState'],
'theme' | 'language' | 'editorCommand' | 'notifications' | 'upgrade'
>;
readonly ui: Pick<SlashCommandHost['state']['ui'], 'requestRender'>;
};
setAppState(patch: Pick<SlashCommandHost['state']['appState'], 'language'>): void;
showStatus(msg: string, color?: string): void;
track: SlashCommandHost['track'];
refreshSlashCommandAutocomplete: SlashCommandHost['refreshSlashCommandAutocomplete'];
};

type UpdatePreferenceHost = {
readonly state: {
readonly appState: Pick<
SlashCommandHost['state']['appState'],
'theme' | 'editorCommand' | 'notifications' | 'upgrade'
'theme' | 'language' | 'editorCommand' | 'notifications' | 'upgrade'
>;
};
setAppState(patch: Pick<SlashCommandHost['state']['appState'], 'upgrade'>): void;
Expand All @@ -567,6 +636,7 @@ export async function applyUpdatePreferenceChoice(
try {
await saveTuiConfig({
theme: host.state.appState.theme,
language: host.state.appState.language,
editorCommand: host.state.appState.editorCommand,
notifications: host.state.appState.notifications,
upgrade,
Expand Down Expand Up @@ -621,6 +691,7 @@ function handleSettingsSelection(host: SlashCommandHost, value: SettingsSelectio
case 'model': showModelPicker(host); return;
case 'permission': showPermissionPicker(host); return;
case 'theme': showThemePicker(host); return;
case 'language': showLanguagePicker(host); return;
case 'editor': showEditorPicker(host); return;
case 'experiments': void showExperimentsPanel(host); return;
case 'upgrade': showUpdatePreferencePicker(host); return;
Expand Down
7 changes: 6 additions & 1 deletion apps/kimi-code/src/tui/commands/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from '../constant/feedback';
import { isManagedUsageProvider } from '../constant/kimi-tui';
import { formatErrorMessage } from '../utils/event-payload';
import { i18n } from '#/tui/i18n';
import { openUrl } from '#/utils/open-url';
import { promptFeedbackInput } from './prompts';
import type { SlashCommandHost } from './dispatch';
Expand Down Expand Up @@ -125,7 +126,11 @@ export async function showStatusReport(host: SlashCommandHost): Promise<void> {
managedUsage: managedUsage?.usage,
managedUsageError: managedUsage?.error,
};
const panel = new UsagePanelComponent(() => buildStatusReportLines(reportArgs), 'primary', ' Status ');
const panel = new UsagePanelComponent(
() => buildStatusReportLines(reportArgs),
'primary',
i18n.t('components.status.panelTitle'),
);
host.state.transcriptContainer.addChild(panel);
host.state.ui.requestRender();
}
Expand Down
61 changes: 42 additions & 19 deletions apps/kimi-code/src/tui/commands/registry.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,50 @@
import type { AutocompleteItem } from '@earendil-works/pi-tui';

import { i18n } from '#/tui/i18n';

import { completeLeadingArg, type ArgCompletionSpec } from './complete-args';
import type { KimiSlashCommand, SlashCommandAvailability } from './types';

/** Subcommands offered when autocompleting `/goal <…>`. */
const GOAL_ARG_COMPLETIONS: readonly ArgCompletionSpec[] = [
{ value: 'status', description: 'Show the current goal' },
{ value: 'pause', description: 'Pause the active goal' },
{ value: 'resume', description: 'Resume a paused goal' },
{ value: 'cancel', description: 'Cancel and remove the current goal' },
{ value: 'replace', description: 'Replace the current goal with a new objective' },
{ value: 'next', description: 'Queue an upcoming goal' },
];
/**
* Subcommand argument-completion specs, built at call time so descriptions
* follow the active locale. `value` is an identifier and stays untranslated;
* only `description` is localized via `commands.args.*`.
*/
function goalArgCompletions(): ArgCompletionSpec[] {
return ['status', 'pause', 'resume', 'cancel', 'replace', 'next'].map((value) => ({
value,
description: i18n.t(`commands.args.goal.${value}`),
}));
}

const GOAL_NEXT_ARG_COMPLETIONS: readonly ArgCompletionSpec[] = [
{ value: 'manage', description: 'Manage upcoming goals' },
];
function goalNextArgCompletions(): ArgCompletionSpec[] {
return [{ value: 'manage', description: i18n.t('commands.args.goal.manage') }];
}

const SWARM_ARG_COMPLETIONS: readonly ArgCompletionSpec[] = [
{ value: 'on', description: 'Turn swarm mode on' },
{ value: 'off', description: 'Turn swarm mode off' },
];
function swarmArgCompletions(): ArgCompletionSpec[] {
return ['on', 'off'].map((value) => ({
value,
description: i18n.t(`commands.args.swarm.${value}`),
}));
}

/** Argument autocompletion for the `/goal` command (subcommands). */
export function goalArgumentCompletions(argumentPrefix: string): AutocompleteItem[] | null {
const nextMatch = argumentPrefix.match(/^next\s+(\S*)$/i);
if (nextMatch !== null) {
return (
completeLeadingArg(GOAL_NEXT_ARG_COMPLETIONS, nextMatch[1] ?? '')?.map((item) => ({
completeLeadingArg(goalNextArgCompletions(), nextMatch[1] ?? '')?.map((item) => ({
...item,
value: `next ${item.value}`,
})) ?? null
);
}
return completeLeadingArg(GOAL_ARG_COMPLETIONS, argumentPrefix);
return completeLeadingArg(goalArgCompletions(), argumentPrefix);
}

/** Argument autocompletion for the `/swarm` command (subcommands). */
export function swarmArgumentCompletions(argumentPrefix: string): AutocompleteItem[] | null {
return completeLeadingArg(SWARM_ARG_COMPLETIONS, argumentPrefix);
return completeLeadingArg(swarmArgCompletions(), argumentPrefix);
}

export const BUILTIN_SLASH_COMMANDS = [
Expand Down Expand Up @@ -309,6 +315,23 @@ export function findBuiltInSlashCommand(commandName: string): BuiltinSlashComman
) as BuiltinSlashCommand | undefined;
}

/**
* Builtin commands with their `description` resolved for the active locale.
*
* The static `description` strings on `BUILTIN_SLASH_COMMANDS` are the English
* source of truth; display surfaces (autocomplete + the `/help` panel) call
* this so the description follows `i18n.setLocale(...)` at render time, exactly
* as components call `i18n.t(...)`. Command names / aliases are identifiers and
* are never translated. Skill-provided commands keep their own descriptions —
* only the framework's own builtins are localized here.
*/
export function localizedBuiltinSlashCommands(): readonly KimiSlashCommand[] {
return BUILTIN_SLASH_COMMANDS.map((command) => ({
...command,
description: i18n.t(`commands.descriptions.${command.name}`),
}));
}

export function resolveSlashCommandAvailability(
command: KimiSlashCommand,
args: string,
Expand Down
13 changes: 12 additions & 1 deletion apps/kimi-code/src/tui/commands/reload.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { KimiConfig } from '@moonshot-ai/kimi-code-sdk';

import { i18n, resolveLocale } from '#/tui/i18n';
import { currentTheme, lightColors } from '#/tui/theme';
import { loadTuiConfig, type TuiConfig } from '../config';
import type { SlashCommandHost } from './dispatch';
Expand All @@ -22,8 +23,9 @@ export async function handleReloadCommand(host: SlashCommandHost): Promise<void>

const config = await host.harness.getConfig({ reload: true });
setExperimentalFeatures(await host.harness.getExperimentalFeatures());
host.refreshSlashCommandAutocomplete();
applyRuntimeConfig(host, config);
// applyReloadedTuiConfig rebuilds the slash-command autocomplete, picking up
// both the refreshed experimental-flag gating above and the reloaded locale.
await applyReloadedTuiConfig(host, tuiConfig);

if (session === undefined) {
Expand All @@ -43,10 +45,19 @@ export async function applyReloadedTuiConfig(
: undefined;
await host.applyTheme(config.theme, resolved);
host.refreshTerminalThemeTracking();
// Re-apply the language alongside the theme: update the persisted preference
// in app state and flip the live i18n locale so a reloaded `language` takes
// effect without a process restart.
i18n.setLocale(resolveLocale(config.language));
// Rebuild the `/` autocomplete snapshot so its localized command descriptions
// follow the reloaded locale. `/reload` already refreshes it separately, but
// `/reload-tui` reaches this helper directly, so refresh here too.
host.refreshSlashCommandAutocomplete();
host.setAppState({
editorCommand: config.editorCommand,
notifications: config.notifications,
upgrade: config.upgrade,
language: config.language,
});
}

Expand Down
Loading