diff --git a/apps/kimi-code/src/cli/v2/run-v2-print.ts b/apps/kimi-code/src/cli/v2/run-v2-print.ts index 659802127a6..632efeee982 100644 --- a/apps/kimi-code/src/cli/v2/run-v2-print.ts +++ b/apps/kimi-code/src/cli/v2/run-v2-print.ts @@ -266,13 +266,14 @@ async function resolveNativeSession( throw new Error(t('tui.statusMessages.sessionNotFound', { sessionId: opts.session! })); } if (target.cwd !== undefined && resolve(target.cwd) !== resolve(workDir)) { - stderr.write( + // The session exists but belongs to another directory — say so in the + // thrown error instead of the contradictory "session not found". + throw new Error( t('tui.statusMessages.sessionDifferentDir', { sessionId: opts.session!, cwd: target.cwd, - }), + }).trimEnd(), ); - throw new Error(t('tui.statusMessages.sessionNotFound', { sessionId: opts.session! })); } const session = await resumeById(opts.session); const agent = await ensureMainAgent(session); diff --git a/apps/kimi-code/src/i18n/index.ts b/apps/kimi-code/src/i18n/index.ts index 419a9676066..3a2afc13414 100644 --- a/apps/kimi-code/src/i18n/index.ts +++ b/apps/kimi-code/src/i18n/index.ts @@ -139,12 +139,13 @@ export interface CreateI18nOptions { * * @example * const { t, setLocale } = createI18n(); - * console.log(t('errors.sessionNotFound')); + * console.log(t('cli.errors.promptEmpty')); */ export function createI18n(options: CreateI18nOptions = {}): I18nInstance { - let currentLocale: Locale = options.noDetect - ? 'en' - : (options.initialLocale ?? detectLocaleNode()); + // An explicit initialLocale always wins; noDetect only suppresses + // env-based detection (falling back to 'en') when no locale was given. + let currentLocale: Locale = options.initialLocale + ?? (options.noDetect ? 'en' : detectLocaleNode()); let localeCurrentJson = JSON.stringify(messages[currentLocale]); @@ -237,4 +238,6 @@ const defaultI18n = createI18n(); export const t = defaultI18n.t.bind(defaultI18n); export const setLocale = defaultI18n.setLocale.bind(defaultI18n); export const getLocale = defaultI18n.getLocale.bind(defaultI18n); -export const getEngine = defaultI18n.getEngine.bind(defaultI18n); \ No newline at end of file +export const getEngine = defaultI18n.getEngine.bind(defaultI18n); +export const getMessages = defaultI18n.getMessages.bind(defaultI18n); +export const translateBatch = defaultI18n.translateBatch.bind(defaultI18n); \ No newline at end of file diff --git a/apps/kimi-code/src/i18n/locales/en.json b/apps/kimi-code/src/i18n/locales/en.json index a5e345bc432..465b816a0b8 100644 --- a/apps/kimi-code/src/i18n/locales/en.json +++ b/apps/kimi-code/src/i18n/locales/en.json @@ -161,6 +161,7 @@ "yolo": "yolo", "plan": "plan", "swarm": "swarm", + "swarmPlan": "swarm-plan", "turn": "turn", "turns": "turns" }, diff --git a/apps/kimi-code/src/i18n/locales/en.ts b/apps/kimi-code/src/i18n/locales/en.ts index 18c0f18fd1a..75feef877ca 100644 --- a/apps/kimi-code/src/i18n/locales/en.ts +++ b/apps/kimi-code/src/i18n/locales/en.ts @@ -466,6 +466,24 @@ export default { usageDesc: 'Show session tokens, context window, and plan quotas.', githubToken: 'GitHub Token', githubTokenDesc: 'Set the GitHub personal access token for built-in GitHub tools.', + astron: 'Astron', + astronDesc: 'Configure the Astron model parameters.', + }, + astronSettings: { + title: 'Astron Settings', + hintNavigate: '↑↓ navigate', + hintEnter: 'Enter edit/toggle', + hintSave: 'Ctrl+S save', + hintCancel: 'Esc cancel', + hintBackspace: 'Backspace delete', + saveButton: '[ Save ]', + saved: 'Saved', + statusEnabled: 'enabled', + statusDisabled: 'disabled', + fieldStream: 'Stream', + fieldTemperature: 'Temperature', + fieldMaxTokens: 'Max Tokens', + fieldSearchDisable: 'Search Disable', }, permissionSelector: { title: 'Select permission mode', @@ -1683,6 +1701,7 @@ export default { configGithubTokenInput: 'Enter your GitHub personal access token (classic).', configGithubTokenSaved: 'GitHub token saved.', configGithubTokenSaveFailed: 'Failed to save GitHub token: {{error}}', + configAstronSaveFailed: 'Failed to save Astron settings: {{error}}', // tui/commands/dispatch.ts configInvalidSlashCommand: 'Invalid slash command: /{{name}}', configVersionDisplay: 'Kimi Code v{{version}}', diff --git a/apps/kimi-code/src/i18n/locales/zh.json b/apps/kimi-code/src/i18n/locales/zh.json index dd7fb982a0b..76211d48010 100644 --- a/apps/kimi-code/src/i18n/locales/zh.json +++ b/apps/kimi-code/src/i18n/locales/zh.json @@ -161,6 +161,7 @@ "yolo": "yolo", "plan": "plan", "swarm": "swarm", + "swarmPlan": "swarm-plan", "turn": "turn", "turns": "turns" }, @@ -893,7 +894,7 @@ "noModelSelected": "未选择模型。请运行 /model 选择模型。", "unknownTheme": "未知主题:{{theme}}", "unknownModelAlias": "未知模型别名:{{alias}}", - "unsupportedEffort": "{{alias}} 不支持的思考强度 \"{{arg}\"。可用:{{segments}}", + "unsupportedEffort": "{{alias}} 不支持的思考强度 \"{{arg}}\"。可用:{{segments}}", "switchModelFailed": "切换模型失败:{{msg}}", "switchSavedButDefaultFailed": "已切换到 {{name}},但保存默认设置失败:{{msg}}", "loadExperimentsFailed": "加载实验性功能失败:{{error}}", diff --git a/apps/kimi-code/src/i18n/locales/zh.ts b/apps/kimi-code/src/i18n/locales/zh.ts index 4129ddca919..3273e584dd4 100644 --- a/apps/kimi-code/src/i18n/locales/zh.ts +++ b/apps/kimi-code/src/i18n/locales/zh.ts @@ -466,6 +466,24 @@ export default { usageDesc: '查看会话令牌数、上下文窗口和计划配额。', githubToken: 'GitHub Token', githubTokenDesc: '设置内置 GitHub 工具的 Personal Access Token。', + astron: 'Astron', + astronDesc: '配置 Astron 模型参数。', + }, + astronSettings: { + title: 'Astron 设置', + hintNavigate: '↑↓ 导航', + hintEnter: 'Enter 编辑/切换', + hintSave: 'Ctrl+S 保存', + hintCancel: 'Esc 取消', + hintBackspace: 'Backspace 删除', + saveButton: '[ 保存 ]', + saved: '已保存', + statusEnabled: '已启用', + statusDisabled: '已禁用', + fieldStream: '流式输出', + fieldTemperature: '温度', + fieldMaxTokens: '最大 Token 数', + fieldSearchDisable: '禁用搜索', }, permissionSelector: { title: '选择权限模式', @@ -882,7 +900,7 @@ export default { noModelSelected: '未选择模型。请运行 /model 选择模型。', unknownTheme: '未知主题:{{theme}}', unknownModelAlias: '未知模型别名:{{alias}}', - unsupportedEffort: '{{alias}} 不支持的思考强度 "{{arg}"。可用:{{segments}}', + unsupportedEffort: '{{alias}} 不支持的思考强度 "{{arg}}"。可用:{{segments}}', switchModelFailed: '切换模型失败:{{msg}}', switchSavedButDefaultFailed: '已切换到 {{name}},但保存默认设置失败:{{msg}}', loadExperimentsFailed: '加载实验性功能失败:{{error}}', @@ -1678,6 +1696,7 @@ export default { configGithubTokenInput: '输入你的 GitHub Personal Access Token(经典版)。', configGithubTokenSaved: 'GitHub Token 已保存。', configGithubTokenSaveFailed: '保存 GitHub Token 失败:{{error}}', + configAstronSaveFailed: '保存 Astron 设置失败:{{error}}', // tui/commands/dispatch.ts configInvalidSlashCommand: '无效的斜杠命令:/{{name}}', configVersionDisplay: 'Kimi Code v{{version}}', diff --git a/apps/kimi-code/src/migration/badge.ts b/apps/kimi-code/src/migration/badge.ts index 52e71d16198..fbe5bcc4c00 100644 --- a/apps/kimi-code/src/migration/badge.ts +++ b/apps/kimi-code/src/migration/badge.ts @@ -9,7 +9,7 @@ import { t } from '#/i18n'; export function formatImportedBadge(): string { - return t('migration.badgeImported'); + return t('tui.migration.badgeImported'); } export interface SessionLabelInput { diff --git a/apps/kimi-code/src/migration/migration-screen.ts b/apps/kimi-code/src/migration/migration-screen.ts index 38f843f5b42..e380d9ace90 100644 --- a/apps/kimi-code/src/migration/migration-screen.ts +++ b/apps/kimi-code/src/migration/migration-screen.ts @@ -39,10 +39,10 @@ const SPINNER_INTERVAL_MS = 80; function stepLabels(): ReadonlyArray { return [ - ['config', t('migration.stepLabelConfig')], - ['mcp', t('migration.stepLabelMcp')], - ['user-history', t('migration.stepLabelReplHistory')], - ['sessions', t('migration.stepLabelSessions')], + ['config', t('tui.migration.stepLabelConfig')], + ['mcp', t('tui.migration.stepLabelMcp')], + ['user-history', t('tui.migration.stepLabelReplHistory')], + ['sessions', t('tui.migration.stepLabelSessions')], ]; } @@ -285,26 +285,26 @@ export class MigrationScreenComponent extends Container implements Focusable { const colors = this.opts.colors ?? currentTheme.palette; const lines: string[] = [chalk.hex(colors.primary)('─'.repeat(width))]; if (this.migrationFailed) { - lines.push(chalk.hex(colors.error).bold(t('migration.failed'))); + lines.push(chalk.hex(colors.error).bold(t('tui.migration.failed'))); if (this.migrationFailureReason !== undefined) { lines.push(''); - lines.push(chalk.hex(colors.text)(t('migration.reason', { reason: this.migrationFailureReason }))); + lines.push(chalk.hex(colors.text)(t('tui.migration.reason', { reason: this.migrationFailureReason }))); } lines.push(''); - lines.push(chalk.hex(colors.text)(t('migration.retryHint'))); + lines.push(chalk.hex(colors.text)(t('tui.migration.retryHint'))); lines.push(''); - lines.push(chalk.hex(colors.textMuted)(t('migration.continueHint'))); + lines.push(chalk.hex(colors.textMuted)(t('tui.migration.continueHint'))); lines.push(chalk.hex(colors.primary)('─'.repeat(width))); return lines.map((l) => truncateToWidth(l, width)); } const r = this.report; - lines.push(chalk.hex(colors.primary).bold(t('migration.complete'))); + lines.push(chalk.hex(colors.primary).bold(t('tui.migration.complete'))); lines.push(''); if (r !== undefined) { const sum = r.summary; if (sum.sessions.sessionsMigrated > 0) { lines.push( - chalk.hex(colors.success)(t('migration.sessionsMigrated', { count: String(sum.sessions.sessionsMigrated) })), + chalk.hex(colors.success)(t('tui.migration.sessionsMigrated', { count: String(sum.sessions.sessionsMigrated) })), ); } // Only claim a data class was migrated when the summary says it was — @@ -316,36 +316,36 @@ export class MigrationScreenComponent extends Container implements Focusable { if (sum.userHistory.copied > 0) migratedKinds.push('REPL history'); if (sum.skills.copied > 0) migratedKinds.push('skills'); if (migratedKinds.length > 0) { - lines.push(chalk.hex(colors.success)(t('migration.kindsMigrated', { kinds: migratedKinds.join(' · ') }))); + lines.push(chalk.hex(colors.success)(t('tui.migration.kindsMigrated', { kinds: migratedKinds.join(' · ') }))); } if (sum.sessions.sessionsMigrated === 0 && migratedKinds.length === 0) { - lines.push(chalk.hex(colors.textMuted)(t('migration.skipped'))); + lines.push(chalk.hex(colors.textMuted)(t('tui.migration.skipped'))); } if (r.notices.detectedPlugins.length > 0) { lines.push( chalk.hex(colors.warning)( - t('migration.pluginsNotSupported', { count: String(r.notices.detectedPlugins.length) }), + t('tui.migration.pluginsNotSupported', { count: String(r.notices.detectedPlugins.length) }), ), ); } if (sum.config.droppedHooks > 0) { lines.push( chalk.hex(colors.warning)( - t('migration.hooksDropped', { count: String(sum.config.droppedHooks) }), + t('tui.migration.hooksDropped', { count: String(sum.config.droppedHooks) }), ), ); } if (sum.config.configConflicts.length > 0) { lines.push( chalk.hex(colors.warning)( - t('migration.configConflicts', { count: String(sum.config.configConflicts.length), keys: sum.config.configConflicts.join(' · ') }), + t('tui.migration.configConflicts', { count: String(sum.config.configConflicts.length), keys: sum.config.configConflicts.join(' · ') }), ), ); } if (sum.config.wroteSiblingDueToConflict) { lines.push( chalk.hex(colors.warning)( - t('migration.configParseError'), + t('tui.migration.configParseError'), ), ); const sc = sum.config.siblingContents; @@ -360,41 +360,41 @@ export class MigrationScreenComponent extends Container implements Focusable { items.push(`${sc.hooks} hook${sc.hooks === 1 ? '' : 's'}`); } if (items.length > 0) { - lines.push(chalk.hex(colors.warning)(t('migration.contains', { items: items.join(', ') }))); + lines.push(chalk.hex(colors.warning)(t('tui.migration.contains', { items: items.join(', ') }))); } } if (sum.config.wroteTuiSibling) { lines.push( chalk.hex(colors.warning)( - t('migration.tuiConflict'), + t('tui.migration.tuiConflict'), ), ); } if (sum.mcp.wroteSiblingDueToConflict) { lines.push( chalk.hex(colors.warning)( - t('migration.mcpUnreadable'), + t('tui.migration.mcpUnreadable'), ), ); } if (r.notices.mcpOauthServersRequiringReauth.length > 0) { lines.push( chalk.hex(colors.warning)( - t('migration.mcpNeedsAuth', { count: String(r.notices.mcpOauthServersRequiringReauth.length) }), + t('tui.migration.mcpNeedsAuth', { count: String(r.notices.mcpOauthServersRequiringReauth.length) }), ), ); } if (sum.sessions.sessionsFailed.length > 0) { lines.push( chalk.hex(colors.warning)( - t('migration.sessionsFailed', { count: String(sum.sessions.sessionsFailed.length) }), + t('tui.migration.sessionsFailed', { count: String(sum.sessions.sessionsFailed.length) }), ), ); } if (sum.sessions.sessionsConflicts.length > 0) { lines.push( chalk.hex(colors.warning)( - t('migration.sessionsSkipped', { count: String(sum.sessions.sessionsConflicts.length) }), + t('tui.migration.sessionsSkipped', { count: String(sum.sessions.sessionsConflicts.length) }), ), ); } @@ -403,17 +403,17 @@ export class MigrationScreenComponent extends Container implements Focusable { if (sum.sessions.sessionsSkippedEmpty > 0) { lines.push( chalk.hex(colors.textMuted)( - t('migration.emptySessionsSkipped', { count: String(sum.sessions.sessionsSkippedEmpty) }), + t('tui.migration.emptySessionsSkipped', { count: String(sum.sessions.sessionsSkippedEmpty) }), ), ); } lines.push(''); lines.push( - chalk.hex(colors.textMuted)(t('migration.oldDataKept')), + chalk.hex(colors.textMuted)(t('tui.migration.oldDataKept')), ); } lines.push(''); - lines.push(chalk.hex(colors.textMuted)(t('migration.continueHint'))); + lines.push(chalk.hex(colors.textMuted)(t('tui.migration.continueHint'))); lines.push(chalk.hex(colors.primary)('─'.repeat(width))); return lines.map((l) => truncateToWidth(l, width)); } @@ -423,14 +423,14 @@ export class MigrationScreenComponent extends Container implements Focusable { const spinner = SPINNER_FRAMES[this.spinnerFrame] ?? SPINNER_FRAMES[0]; const lines: string[] = [ chalk.hex(colors.primary)('─'.repeat(width)), - chalk.hex(colors.primary).bold(t('migration.progressTitle')), + chalk.hex(colors.primary).bold(t('tui.migration.progressTitle')), '', ]; if (this.progressTotal > 0) { lines.push( chalk.hex(colors.accent)(` ${spinner} `) + chalk.hex(colors.text)( - t('migration.progressTranslating', { done: String(this.progressDone), total: String(this.progressTotal) }), + t('tui.migration.progressTranslating', { done: String(this.progressDone), total: String(this.progressTotal) }), ), ); lines.push(''); @@ -453,11 +453,11 @@ export class MigrationScreenComponent extends Container implements Focusable { const step = this.currentStep(); const lines: string[] = [ chalk.hex(colors.primary)('─'.repeat(width)), - chalk.hex(colors.primary).bold(t('migration.title')), + chalk.hex(colors.primary).bold(t('tui.migration.title')), '', ]; if (this.phase === 'ask1') { - lines.push(chalk.hex(colors.text)(t('migration.foundExisting'))); + lines.push(chalk.hex(colors.text)(t('tui.migration.foundExisting'))); lines.push(chalk.hex(colors.textMuted)(` ${summarizePlan(this.opts.plan)}`)); lines.push(''); } @@ -476,7 +476,7 @@ export class MigrationScreenComponent extends Container implements Focusable { lines.push(''); lines.push( chalk.hex(colors.textMuted)( - t('migration.navHintAsk', { action: this.opts.skipDecisionStep === true ? 'cancel' : 'later' }), + t('tui.migration.navHintAsk', { action: this.opts.skipDecisionStep === true ? 'cancel' : 'later' }), ), ); lines.push(chalk.hex(colors.primary)('─'.repeat(width))); @@ -535,11 +535,11 @@ function summarizePlan(plan: MigrationPlan): string { function stepFor(phase: Phase, plan: MigrationPlan): StepDef { if (phase === 'ask1') { return { - title: t('migration.ask1Title'), + title: t('tui.migration.ask1Title'), options: [ - { label: t('migration.migrateNow'), value: 'now' satisfies Prompt1Choice }, - { label: t('migration.askLater'), value: 'later' satisfies Prompt1Choice }, - { label: t('migration.neverAgain'), value: 'never' satisfies Prompt1Choice }, + { label: t('tui.migration.migrateNow'), value: 'now' satisfies Prompt1Choice }, + { label: t('tui.migration.askLater'), value: 'later' satisfies Prompt1Choice }, + { label: t('tui.migration.neverAgain'), value: 'never' satisfies Prompt1Choice }, ], }; } @@ -548,12 +548,12 @@ function stepFor(phase: Phase, plan: MigrationPlan): StepDef { // word only (no count) when no sessions were detected. const sessionsLabel = plan.totalSessions > 0 - ? t('migration.configPlusSessions', { count: String(plan.totalSessions) }) - : t('migration.configPlusAllSessions'); + ? t('tui.migration.configPlusSessions', { count: String(plan.totalSessions) }) + : t('tui.migration.configPlusAllSessions'); return { - title: t('migration.ask2Title'), + title: t('tui.migration.ask2Title'), options: [ - { label: t('migration.configOnly'), value: 'config-only' satisfies Prompt2Choice }, + { label: t('tui.migration.configOnly'), value: 'config-only' satisfies Prompt2Choice }, { label: sessionsLabel, value: 'all-sessions' satisfies Prompt2Choice }, ], }; diff --git a/apps/kimi-code/src/tui/commands/config.ts b/apps/kimi-code/src/tui/commands/config.ts index 2e83db0a258..3a73d1d8415 100644 --- a/apps/kimi-code/src/tui/commands/config.ts +++ b/apps/kimi-code/src/tui/commands/config.ts @@ -21,7 +21,7 @@ import { SettingsSelectorComponent, type SettingsSelection } from '../components import { ThemeSelectorComponent } from '../components/dialogs/theme-selector'; import { AstronSettingsComponent } from '../components/dialogs/astron-settings'; import { UpdatePreferenceSelectorComponent } from '../components/dialogs/update-preference-selector'; -import { DEFAULT_TUI_CONFIG, saveTuiConfig, type TuiConfig } from '../config'; +import { DEFAULT_TUI_CONFIG, loadTuiConfig, saveTuiConfig, type TuiConfig } from '../config'; import type { ThemeName } from '#/tui/theme'; import { currentTheme, isBuiltInTheme, lightColors, loadCustomThemeMerged } from '#/tui/theme'; import type { Locale } from '#/i18n'; @@ -50,7 +50,10 @@ function hasConversationHistory(host: SlashCommandHost): boolean { ); } -function currentTuiConfig(host: SlashCommandHost): TuiConfig { +async function currentTuiConfig(host: SlashCommandHost): Promise { + // Settings not mirrored into appState (e.g. astron) must survive unrelated + // saves — seed them from the persisted config instead of defaults. + const persisted = await loadTuiConfig().catch(() => null); return { theme: host.state.appState.theme, locale: host.state.appState.locale as Locale, @@ -58,7 +61,7 @@ function currentTuiConfig(host: SlashCommandHost): TuiConfig { disablePasteBurst: host.state.appState.disablePasteBurst ?? DEFAULT_TUI_CONFIG.disablePasteBurst, notifications: host.state.appState.notifications, upgrade: host.state.appState.upgrade, - astron: DEFAULT_TUI_CONFIG.astron, + astron: persisted?.astron ?? DEFAULT_TUI_CONFIG.astron, }; } @@ -89,7 +92,7 @@ export async function handlePlanCommand(host: SlashCommandHost, args: string): P else if (subcmd === 'on') enabled = true; else if (subcmd === 'off') enabled = false; else { - host.showError(t('tui.dialogs.config.configUnknownPlanSubcommand', { subcmd })); + host.showError(t('tui.messages.configUnknownPlanSubcommand', { subcmd })); return; } @@ -104,7 +107,7 @@ async function applyPlanMode(host: SlashCommandHost, session: Session, enabled: const plan = await session.getPlan().catch(() => null); host.showNotice( t('tui.statusMessages.planModeOn'), - plan?.path !== undefined ? t('tui.dialogs.config.configPlanPath', { path: plan.path }) : undefined, + plan?.path !== undefined ? t('tui.messages.configPlanPath', { path: plan.path }) : undefined, ); return; } @@ -246,7 +249,7 @@ export async function handleModelCommand(host: SlashCommandHost, args: string): return; } if (host.state.appState.availableModels[alias] === undefined) { - host.showError(t('tui.dialogs.config.configUnknownModelAlias', { alias })); + host.showError(t('tui.messages.configUnknownModelAlias', { alias })); return; } showModelPicker(host, alias); @@ -271,13 +274,13 @@ export async function handleEffortCommand(host: SlashCommandHost, args: string): const protocol = effective.protocol ?? providerType; if (protocol !== 'anthropic') { host.showError( - t('tui.dialogs.config.configUnsupportedEffort', { arg, alias, segments: segments.join(', ') }), + t('tui.messages.configUnsupportedEffort', { arg, alias, segments: segments.join(', ') }), ); return; } - const knownEfforts = effective.supportEfforts?.join(', ') ?? t('tui.dialogs.config.configNoneDeclared'); + const knownEfforts = effective.supportEfforts?.join(', ') ?? t('tui.messages.configNoneDeclared'); host.showStatus( - t('tui.dialogs.config.configUnknownEffort', { arg, alias, knownEfforts }), + t('tui.messages.configUnknownEffort', { arg, alias, knownEfforts }), 'warning', ); } @@ -296,7 +299,7 @@ function showEffortPicker( new EffortSelectorComponent({ efforts: segments, currentValue, - warning: hasConversationHistory(host) ? t('tui.dialogs.config.configEffortCachedWarning') : undefined, + warning: hasConversationHistory(host) ? t('tui.messages.configEffortCachedWarning') : undefined, onSelect: (effort) => { host.restoreEditor(); void performModelSwitch(host, alias, effort, true); @@ -340,10 +343,10 @@ async function refreshModelsForPicker(host: SlashCommandHost): Promise { ); if (result === undefined) return; for (const f of result.failed) { - host.showStatus(t('tui.dialogs.config.configSkippedRefreshing', { provider: f.provider, reason: f.reason }), 'warning'); + host.showStatus(t('tui.messages.configSkippedRefreshing', { provider: f.provider, reason: f.reason }), 'warning'); } } catch (error) { - host.showStatus(t('tui.dialogs.config.configSkippedRefreshingModels', { error: formatErrorMessage(error) }), 'warning'); + host.showStatus(t('tui.messages.configSkippedRefreshingModels', { error: formatErrorMessage(error) }), 'warning'); } } @@ -366,19 +369,19 @@ async function withTimeout(promise: Promise, timeoutMs: number): Promise { const previous = host.state.appState.editorCommand ?? ''; if (value === previous && value.length > 0) { - host.showStatus(t('tui.dialogs.config.configEditorUnchanged', { value: value.length > 0 ? value : t('tui.dialogs.config.configEditorAutoDetect') })); + host.showStatus(t('tui.messages.configEditorUnchanged', { value: value.length > 0 ? value : t('tui.messages.configEditorAutoDetect') })); return; } const editorCommand = value.length > 0 ? value : null; try { await saveTuiConfig({ - ...currentTuiConfig(host), + ...(await currentTuiConfig(host)), editorCommand, }); } catch (error) { host.showStatus( - t('tui.dialogs.config.configEditorSaveFailed', { error: formatErrorMessage(error) }), + t('tui.messages.configEditorSaveFailed', { error: formatErrorMessage(error) }), 'error', ); return; @@ -387,8 +390,8 @@ async function applyEditorChoice(host: SlashCommandHost, value: string): Promise host.setAppState({ editorCommand }); host.showStatus( value.length > 0 - ? t('tui.dialogs.config.configEditorSet', { value }) - : t('tui.dialogs.config.configEditorAutoSet'), + ? t('tui.messages.configEditorSet', { value }) + : t('tui.messages.configEditorAutoSet'), ); } @@ -413,7 +416,7 @@ export function showModelPicker(host: SlashCommandHost, selectedValue: string = currentValue: host.state.appState.model, selectedValue, currentThinkingEffort: host.state.appState.thinkingEffort, - warning: hasConversationHistory(host) ? t('tui.dialogs.config.configModelCachedWarning') : undefined, + warning: hasConversationHistory(host) ? t('tui.messages.configModelCachedWarning') : undefined, onSelect: ({ alias, thinking }) => { host.restoreEditor(); void performModelSwitch(host, alias, thinking, true); @@ -436,7 +439,7 @@ async function performModelSwitch( persist: boolean, ): Promise { if (host.state.appState.streamingPhase !== 'idle') { - host.showError(t('tui.dialogs.config.configCannotSwitchWhileStreaming')); + host.showError(t('tui.messages.configCannotSwitchWhileStreaming')); return; } @@ -504,7 +507,7 @@ async function performModelSwitch( ); } catch (error) { const msg = formatErrorMessage(error); - host.showError(t('tui.dialogs.config.configModelSwitchedSaveFailed', { name: displayName, msg })); + host.showError(t('tui.messages.configModelSwitchedSaveFailed', { name: displayName, msg })); return; } } @@ -512,16 +515,16 @@ async function performModelSwitch( let status: string; if (effectiveModelChanged) { status = persist - ? t('tui.dialogs.config.configModelSwitched', { name: displayName, effort: effectiveEffort }) - : t('tui.dialogs.config.configModelSwitchedSession', { name: displayName, effort: effectiveEffort }); + ? t('tui.messages.configModelSwitched', { name: displayName, effort: effectiveEffort }) + : t('tui.messages.configModelSwitchedSession', { name: displayName, effort: effectiveEffort }); } else if (effectiveEffortChanged) { status = persist - ? t('tui.dialogs.config.configThinkingSet', { effort: effectiveEffort }) - : t('tui.dialogs.config.configThinkingSetSession', { effort: effectiveEffort }); + ? t('tui.messages.configThinkingSet', { effort: effectiveEffort }) + : t('tui.messages.configThinkingSetSession', { effort: effectiveEffort }); } else if (persist && persisted) { - status = t('tui.dialogs.config.configModelSavedDefault', { name: displayName, effort: effectiveEffort }); + status = t('tui.messages.configModelSavedDefault', { name: displayName, effort: effectiveEffort }); } else { - status = t('tui.dialogs.config.configModelAlreadyUsing', { name: displayName, effort: effectiveEffort }); + status = t('tui.messages.configModelAlreadyUsing', { name: displayName, effort: effectiveEffort }); } host.showStatus(status, 'success'); } @@ -573,7 +576,7 @@ function showThemePicker(host: SlashCommandHost): void { async function applyThemeChoice(host: SlashCommandHost, theme: ThemeName): Promise { if (theme === host.state.appState.theme) { if (theme === 'auto') host.refreshTerminalThemeTracking(); - host.showStatus(t('tui.dialogs.config.configThemeUnchanged', { theme })); + host.showStatus(t('tui.messages.configThemeUnchanged', { theme })); return; } @@ -583,19 +586,19 @@ async function applyThemeChoice(host: SlashCommandHost, theme: ThemeName): Promi if (!isBuiltInTheme(theme)) { const palette = await loadCustomThemeMerged(theme); if (palette === null) { - host.showStatus(t('tui.dialogs.config.configThemeLoadFailed', { theme }), 'error'); + host.showStatus(t('tui.messages.configThemeLoadFailed', { theme }), 'error'); return; } } try { await saveTuiConfig({ - ...currentTuiConfig(host), + ...(await currentTuiConfig(host)), theme, }); } catch (error) { host.showStatus( - t('tui.dialogs.config.configThemeSaveFailed', { error: formatErrorMessage(error) }), + t('tui.messages.configThemeSaveFailed', { error: formatErrorMessage(error) }), 'error', ); return; @@ -607,8 +610,8 @@ async function applyThemeChoice(host: SlashCommandHost, theme: ThemeName): Promi await host.applyTheme(theme, resolved); host.refreshTerminalThemeTracking(); host.track('theme_switch', { theme }); - const detail = theme === 'auto' ? t('tui.dialogs.config.configThemeTracking', { resolved }) : ''; - host.showStatus(t('tui.dialogs.config.configThemeSet', { theme, detail })); + const detail = theme === 'auto' ? t('tui.messages.configThemeTracking', { resolved }) : ''; + host.showStatus(t('tui.messages.configThemeSet', { theme, detail })); } export function showLocalePicker(host: SlashCommandHost): void { @@ -628,18 +631,18 @@ export function showLocalePicker(host: SlashCommandHost): void { async function applyLocaleChoice(host: SlashCommandHost, locale: Locale): Promise { if (locale === host.state.appState.locale) { - host.showStatus(t('tui.dialogs.config.configLanguageUnchanged', { locale })); + host.showStatus(t('tui.messages.configLanguageUnchanged', { locale })); return; } try { await saveTuiConfig({ - ...currentTuiConfig(host), + ...(await currentTuiConfig(host)), locale, }); } catch (error) { host.showStatus( - t('tui.dialogs.config.configLanguageSaveFailed', { error: formatErrorMessage(error) }), + t('tui.messages.configLanguageSaveFailed', { error: formatErrorMessage(error) }), 'error', ); return; @@ -647,7 +650,7 @@ async function applyLocaleChoice(host: SlashCommandHost, locale: Locale): Promis setLocale(locale); host.setAppState({ locale }); - host.showStatus(t('tui.dialogs.config.configLanguageSet', { locale })); + host.showStatus(t('tui.messages.configLanguageSet', { locale })); } export function showPermissionPicker(host: SlashCommandHost): void { @@ -707,7 +710,7 @@ export async function applyExperimentalFeatureChanges( ): Promise { if (changes.length === 0) { host.showStatus( - t('tui.dialogs.config.configNoExperimentalChanges'), + t('tui.messages.configNoExperimentalChanges'), 'textMuted', ); return; @@ -773,19 +776,19 @@ export async function applyUpdatePreferenceChoice( autoInstall: boolean, ): Promise { if (autoInstall === host.state.appState.upgrade.autoInstall) { - host.showStatus(t('tui.dialogs.config.configAutoUpdateAlready', { state: autoInstall ? t('tui.dialogs.config.configAutoUpdateEnabled') : t('tui.dialogs.config.configAutoUpdateDisabled') })); + host.showStatus(t('tui.messages.configAutoUpdateAlready', { state: autoInstall ? t('tui.messages.configAutoUpdateEnabled') : t('tui.messages.configAutoUpdateDisabled') })); return; } const upgrade = { autoInstall }; try { await saveTuiConfig({ - ...currentTuiConfig(host as unknown as SlashCommandHost), + ...(await currentTuiConfig(host as unknown as SlashCommandHost)), upgrade, }); } catch (error) { host.showStatus( - t('tui.dialogs.config.configAutoUpdateSaveFailed', { error: formatErrorMessage(error) }), + t('tui.messages.configAutoUpdateSaveFailed', { error: formatErrorMessage(error) }), 'error', ); return; @@ -793,12 +796,12 @@ export async function applyUpdatePreferenceChoice( host.setAppState({ upgrade }); host.track('upgrade_preference_changed', { auto_install: autoInstall }); - host.showStatus(t('tui.dialogs.config.configAutoUpdateSet', { state: autoInstall ? t('tui.dialogs.config.configAutoUpdateEnabled') : t('tui.dialogs.config.configAutoUpdateDisabled') })); + host.showStatus(t('tui.messages.configAutoUpdateSet', { state: autoInstall ? t('tui.messages.configAutoUpdateEnabled') : t('tui.messages.configAutoUpdateDisabled') })); } async function applyPermissionChoice(host: SlashCommandHost, mode: PermissionMode): Promise { if (mode === host.state.appState.permissionMode) { - host.showStatus(t('tui.dialogs.config.configPermissionUnchanged', { mode })); + host.showStatus(t('tui.messages.configPermissionUnchanged', { mode })); return; } @@ -811,7 +814,7 @@ async function applyPermissionChoice(host: SlashCommandHost, mode: PermissionMod } host.setAppState({ permissionMode: mode }); - host.showNotice(t('tui.dialogs.config.configPermissionMode', { mode })); + host.showNotice(t('tui.messages.configPermissionMode', { mode })); } export function showSettingsSelector(host: SlashCommandHost): void { @@ -847,13 +850,13 @@ async function handleGitHubTokenInput(host: SlashCommandHost): Promise { const token = await promptApiKey( host, 'GitHub', - [t('tui.dialogs.config.configGithubTokenInput')], + [t('tui.messages.configGithubTokenInput')], ); if (token === undefined) return; try { await host.harness.setConfig({ experimental: { github_token: token } }); - host.showStatus(t('tui.dialogs.config.configGithubTokenSaved'), 'success'); + host.showStatus(t('tui.messages.configGithubTokenSaved'), 'success'); } catch (error) { - host.showError(t('tui.dialogs.config.configGithubTokenSaveFailed', { error: formatErrorMessage(error) })); + host.showError(t('tui.messages.configGithubTokenSaveFailed', { error: formatErrorMessage(error) })); } } diff --git a/apps/kimi-code/src/tui/commands/discuss.ts b/apps/kimi-code/src/tui/commands/discuss.ts index e87e6a2cbf5..bffeff7afcb 100644 --- a/apps/kimi-code/src/tui/commands/discuss.ts +++ b/apps/kimi-code/src/tui/commands/discuss.ts @@ -98,12 +98,14 @@ export async function handleDiscussCommand( // Build participant configs const participants = roles.map((role) => { - const safeName = role.replace(/[^a-zA-Z0-9\u4e00-\u9fff]/g, '_'); + // Escape quotes/backslashes so user-supplied roles can't break the + // quoted string structure of the generated prompt. + const safeRole = role.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); const assignedStance = stances[role]; const stanceField = assignedStance - ? `, assignedStance: "${assignedStance.replace(/"/g, '\\"')}"` + ? `, assignedStance: "${assignedStance.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"` : ''; - return `{ profileName: "coder", roleDescription: "You are a ${role} participating in a roundtable ${mode}.",${stanceField} }`; + return `{ profileName: "coder", roleDescription: "You are a ${safeRole} participating in a roundtable ${mode}.",${stanceField} }`; }).join(',\n '); const prompt = [ diff --git a/apps/kimi-code/src/tui/commands/workflow.ts b/apps/kimi-code/src/tui/commands/workflow.ts index 0bbb1ef5aed..cd7cbc28b94 100644 --- a/apps/kimi-code/src/tui/commands/workflow.ts +++ b/apps/kimi-code/src/tui/commands/workflow.ts @@ -21,18 +21,17 @@ const trimmed = args.trim(); // No args → show usage. if (trimmed.length === 0) { - const wf = t('tui.slashCommands.workflowHelp') as Record; host.showNotice( t('tui.slashCommands.workflow'), -[ - wf.usage, - wf.list, - wf.run, - wf.status, - wf.cancel, + [ + t('tui.slashCommands.workflowHelp.usage'), + t('tui.slashCommands.workflowHelp.list'), + t('tui.slashCommands.workflowHelp.run'), + t('tui.slashCommands.workflowHelp.status'), + t('tui.slashCommands.workflowHelp.cancel'), '', - wf.example, - t('tui.messages.workflowSearchExample'), + t('tui.slashCommands.workflowHelp.example'), + t('tui.messages.workflowSearchExample'), ].join('\n'), ); return; diff --git a/apps/kimi-code/src/tui/components/chrome/footer.ts b/apps/kimi-code/src/tui/components/chrome/footer.ts index f7d3947f9c2..8f2b033b683 100644 --- a/apps/kimi-code/src/tui/components/chrome/footer.ts +++ b/apps/kimi-code/src/tui/components/chrome/footer.ts @@ -238,7 +238,7 @@ export class FooterComponent implements Component { } this.syncGoalClock(state.goal); this.syncGoalTimer(state.goal); - this.syncPulseTimer(state.thinkingEffort !== 'off'); + this.syncPulseTimer(state.thinkingEffort !== 'off' && state.streamingPhase !== 'idle'); this.state = state; } diff --git a/apps/kimi-code/src/tui/components/dialogs/coding-plan-config.ts b/apps/kimi-code/src/tui/components/dialogs/coding-plan-config.ts index eb89bd67bae..3b11c6b757b 100644 --- a/apps/kimi-code/src/tui/components/dialogs/coding-plan-config.ts +++ b/apps/kimi-code/src/tui/components/dialogs/coding-plan-config.ts @@ -1,5 +1,6 @@ import { Container, Key, matchesKey, type Focusable } from '@moonshot-ai/pi-tui'; import { currentTheme } from '#/tui/theme'; +import { printableChar } from '#/tui/utils/printable-key'; import { t } from '#/i18n'; export interface CodingPlanConfigOptions { @@ -45,7 +46,7 @@ export class CodingPlanConfigComponent extends Container implements Focusable { if (schema !== undefined) { const parsed = schema.parse(raw); if (schema.validate !== undefined && !schema.validate(parsed)) { - this.errorMsg = t('codingPlan.invalidValue', { key, raw }); + this.errorMsg = t('tui.codingPlan.invalidValue', { key, raw }); return; } config[key] = parsed; @@ -71,17 +72,21 @@ export class CodingPlanConfigComponent extends Container implements Focusable { if (matchesKey(data, Key.backspace)) { const current = this.fields[field]; this.fields[field] = current !== undefined ? current.slice(0, -1) : ''; - } else if (data.length > 0 && !matchesKey(data, Key.enter) && !matchesKey(data, Key.escape)) { - // Accept any printable character input (including paste) - const current = this.fields[field] ?? ''; - this.fields[field] = current + data; + } else { + // Accept printable input only (including paste): arrow keys, Tab and + // Ctrl combos must not leak escape bytes into the field value. + const ch = printableChar(data); + if (ch.length > 0) { + const current = this.fields[field] ?? ''; + this.fields[field] = current + ch; + } } } override render(width: number): string[] { const lines: string[] = [ currentTheme.fg('primary', '─'.repeat(width)), - currentTheme.boldFg('primary', t('codingPlan.title')), + currentTheme.boldFg('primary', t('tui.codingPlan.title')), '', ]; for (let i = 0; i < this.fieldOrder.length; i++) { @@ -99,7 +104,7 @@ export class CodingPlanConfigComponent extends Container implements Focusable { if (this.errorMsg.length > 0) { lines.push(currentTheme.fg('error', ` ${this.errorMsg}`)); } - lines.push(currentTheme.fg('textMuted', t('codingPlan.navHint'))); + lines.push(currentTheme.fg('textMuted', t('tui.codingPlan.navHint'))); lines.push(currentTheme.fg('primary', '─'.repeat(width))); return lines.map((line) => line.slice(0, width)); } @@ -107,15 +112,15 @@ export class CodingPlanConfigComponent extends Container implements Focusable { function fieldLabel(key: string): string { const labels: Record = { - protocol: t('codingPlan.fieldProtocol'), - stream: t('codingPlan.fieldStream'), - temperature: t('codingPlan.fieldTemperature'), - maxTokens: t('codingPlan.fieldMaxTokens'), - enableThinking: t('codingPlan.fieldEnableThinking'), - searchDisable: t('codingPlan.fieldSearchDisable'), - showRefLabel: t('codingPlan.fieldShowRefLabel'), - loraId: t('codingPlan.fieldLoraId'), - reasoningEffort: t('codingPlan.fieldReasoningEffort'), + protocol: t('tui.codingPlan.fieldProtocol'), + stream: t('tui.codingPlan.fieldStream'), + temperature: t('tui.codingPlan.fieldTemperature'), + maxTokens: t('tui.codingPlan.fieldMaxTokens'), + enableThinking: t('tui.codingPlan.fieldEnableThinking'), + searchDisable: t('tui.codingPlan.fieldSearchDisable'), + showRefLabel: t('tui.codingPlan.fieldShowRefLabel'), + loraId: t('tui.codingPlan.fieldLoraId'), + reasoningEffort: t('tui.codingPlan.fieldReasoningEffort'), }; return labels[key] ?? key; }