You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by as...@apache.org on 2026/09/01 08:06:35 UTC
(maka) branch main updated: fix(cli): localize remaining TUI copy (#4422)
This is an automated email from the ASF dual-hosted git repository.
Astro-Han pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git
The following commit(s) were added to refs/heads/main by this push:
new 920d7142d0 fix(cli): localize remaining TUI copy (#4422)
920d7142d0 is described below
commit 920d7142d001e81868bcd50a28a5bb3898415e3b
Author: NekoPunch <95...@users.noreply.github.com>
AuthorDate: Tue Sep 1 01:06:30 2026 -0700
fix(cli): localize remaining TUI copy (#4422)
The TUI still rendered the wrong language in both directions. English sessions
saw Chinese copy in the flows #3990 left as follow-up work — rewind notices and
the rewind picker, skill-invocation notices, `/skill` usage and the skill
picker, the foreign-transcript scan failure, and the `/new` failure fallback —
while Chinese sessions saw English `Steering:`/`Queued:` labels in the
pending-queue bar. These strings lived outside the message contract #3990
established: as Chinese-only literals, as `locale === 'zh'` ternaries, and, in
the MCP guided editor, as an inline `copy()` helper carrying its own parallel
en/zh maps alongside `heading(locale, en, zh)` pairs.
Move the remaining copy into typed `TUI_COPY_RESOURCES` domains — `rewind`,
`skills`, `connection-identity`, `session-actions`, `pending-queue`, and an
`editor` sub-tree under `mcp-status` — consumed through the existing
`defineUiMessageCatalog` / `resolveUiMessageCatalog` / `formatUiMessage`
contract. The catalog becomes the single authority: `copy()`, `heading()`, and
`confirmCopy()` are deleted, and `pi-tui-mcp-status.ts` is net smaller. Result
codes are typed against the union `tui-mcp-control.ts` produces and skill
failure reasons against `SkillInvocationFailureReason`, so a missing entry is
now a compile error rather than a raw code leaking to the user;
`resolution_failed` was previously unmapped and is now covered.
Three behavior changes beyond copy sourcing. The Chinese MCP editor busy label
distinguished nothing — test, reconnect, and apply all rendered one generic
line — and now matches English. `/new` failure no longer renders the driver's
raw `error.message`; it shows a cause-neutral localized notice. The
foreign-transcript scan failure keeps its `{detail}` interpolation as a
documented exception, because the detail is a local file-scan diagnostic and
the notice exists so a store bug is not mistaken for "no sessions".
No migration impact: no persisted representation, protocol, or public API
changes, and every moved string keeps its text.
---
packages/cli/src/__tests__/pi-transcript.test.ts | 8 +-
.../cli/src/__tests__/pi-tui-mcp-status.test.ts | 273 ++++++++++++++++++++-
packages/cli/src/__tests__/pi-tui-runner.test.ts | 148 ++++++++---
.../cli/src/__tests__/tui-copy-catalog.test.ts | 30 +++
packages/cli/src/pi-transcript.ts | 29 ++-
packages/cli/src/pi-tui-layout.ts | 8 +-
packages/cli/src/pi-tui-mcp-status.ts | 213 ++++++----------
packages/cli/src/pi-tui-runner.ts | 176 +++++++++----
packages/cli/src/tui-copy-catalog.ts | 237 ++++++++++++++++++
9 files changed, 892 insertions(+), 230 deletions(-)
diff --git a/packages/cli/src/__tests__/pi-transcript.test.ts b/packages/cli/src/__tests__/pi-transcript.test.ts
index 1330205b82..83e67374a2 100644
--- a/packages/cli/src/__tests__/pi-transcript.test.ts
+++ b/packages/cli/src/__tests__/pi-transcript.test.ts
@@ -152,8 +152,12 @@ describe('Maka Pi TUI transcript', () => {
const renderFor = (platform: NodeJS.Platform) =>
renderMakaPiPendingQueue(state, 80, platform).map(stripAnsi);
- assert.equal(renderFor('darwin').at(-1), '⌥+↑ 取回队列以重新编辑');
- assert.equal(renderFor('linux').at(-1), 'Alt+↑ 取回队列以重新编辑');
+ assert.equal(renderFor('darwin').at(-1), '⌥+↑ take queued messages back to re-edit');
+ assert.equal(renderFor('linux').at(-1), 'Alt+↑ take queued messages back to re-edit');
+ assert.equal(
+ renderMakaPiPendingQueue(state, 80, 'linux', 'zh').map(stripAnsi).at(-1),
+ 'Alt+↑ 取回队列以重新编辑',
+ );
});
test('renders goal-origin prompts as autonomous provenance, not as user prompts', () => {
diff --git a/packages/cli/src/__tests__/pi-tui-mcp-status.test.ts b/packages/cli/src/__tests__/pi-tui-mcp-status.test.ts
index d255feb83f..d058440674 100644
--- a/packages/cli/src/__tests__/pi-tui-mcp-status.test.ts
+++ b/packages/cli/src/__tests__/pi-tui-mcp-status.test.ts
@@ -20,8 +20,15 @@
import assert from 'node:assert/strict';
import { describe, test } from 'node:test';
import type { TUI } from '@earendil-works/pi-tui';
+import type { McpTestResult } from '@maka/core/mcp';
import { McpManagementOverlay } from '../pi-tui-mcp-status.js';
-import type { TuiMcpAction, TuiMcpManagement, TuiMcpSnapshot } from '../tui-mcp-control.js';
+import type {
+ TuiMcpAction,
+ TuiMcpActionResult,
+ TuiMcpManagement,
+ TuiMcpSnapshot,
+} from '../tui-mcp-control.js';
+import { TUI_COPY_RESOURCES } from '../tui-copy-catalog.js';
import { stripAnsi } from '../tui-ansi.js';
describe('MCP management overlay', () => {
@@ -243,8 +250,272 @@ describe('MCP management overlay', () => {
text = overlay.render(100).map(stripAnsi).join('\n');
assert.match(text, /› ○ s4/u);
});
+
+ const editorCopy = {
+ en: {
+ add: 'Add MCP server',
+ transport: 'Transport',
+ protocol: 'Protocol preference',
+ confirmAdd: 'Add this MCP server?',
+ confirmRemove: 'Remove filesystem?',
+ confirmHint: 'y Confirm · Esc cancel',
+ serverId: 'Server ID',
+ command: 'Command',
+ args: 'Arguments',
+ argsHint: 'JSON string array, optional',
+ cwd: 'Working directory',
+ optionalHint: 'optional',
+ env: 'Environment',
+ mapHint: 'JSON string map, optional',
+ submitHint: 'Enter submit · Esc back',
+ testing: 'Testing MCP server…',
+ reconnecting: 'Reconnecting MCP server…',
+ applying: 'Applying MCP configuration…',
+ published: 'Configuration saved and tools refreshed.',
+ managerFailed: 'The MCP connection action failed.',
+ },
+ zh: {
+ add: '添加 MCP 服务器',
+ transport: '传输方式',
+ protocol: '协议偏好',
+ confirmAdd: '添加该 MCP 服务器?',
+ confirmRemove: '删除 filesystem?',
+ confirmHint: 'y 确认 · Esc 取消',
+ serverId: '服务器 ID',
+ command: '命令',
+ args: '参数',
+ argsHint: 'JSON string array, 可留空',
+ cwd: '工作目录',
+ optionalHint: '可留空',
+ env: '环境变量',
+ mapHint: 'JSON string map, 可留空',
+ submitHint: 'Enter 提交 · Esc 返回',
+ testing: '正在测试 MCP 服务器…',
+ reconnecting: '正在重连 MCP 服务器…',
+ applying: '正在应用 MCP 配置…',
+ published: '配置已保存,工具已刷新。',
+ managerFailed: 'MCP 连接操作失败。',
+ },
+ } as const;
+
+ for (const locale of ['en', 'zh'] as const) {
+ const expected = editorCopy[locale];
+
+ test(`walks the guided add flow with ${locale} headings, labels, and hints`, () => {
+ const overlay = new McpManagementOverlay({
+ locale,
+ tui: fakeTui(),
+ surface: surface(listSnapshot()),
+ viewportRows: () => 14,
+ onClose: () => undefined,
+ onChange: () => undefined,
+ });
+ const rendered = () => overlay.render(100).map(stripAnsi).join('\n');
+
+ overlay.handleInput('a');
+ assert.ok(rendered().includes(expected.add));
+ overlay.handleInput('g');
+ let text = rendered();
+ assert.ok(text.includes(expected.serverId));
+ assert.ok(text.includes(expected.submitHint));
+ for (const char of 'demo') overlay.handleInput(char);
+ overlay.handleInput('\r');
+ assert.ok(rendered().includes(expected.transport));
+ overlay.handleInput('1');
+ text = rendered();
+ assert.ok(text.includes(expected.command));
+ for (const char of 'echo') overlay.handleInput(char);
+ overlay.handleInput('\r');
+ text = rendered();
+ assert.ok(text.includes(expected.args));
+ assert.ok(text.includes(expected.argsHint));
+ overlay.handleInput('\r');
+ assert.ok(rendered().includes(expected.protocol));
+ overlay.handleInput('2');
+ text = rendered();
+ assert.ok(text.includes(expected.cwd));
+ assert.ok(text.includes(expected.optionalHint));
+ overlay.handleInput('\r');
+ text = rendered();
+ assert.ok(text.includes(expected.env));
+ assert.ok(text.includes(expected.mapHint));
+ overlay.handleInput('\r');
+ text = rendered();
+ assert.ok(text.includes(expected.confirmAdd));
+ assert.ok(text.includes('demo · stdio · auto'));
+ assert.ok(text.includes('echo'));
+ assert.ok(text.includes(expected.confirmHint));
+ });
+
+ test(`renders the ${locale} remove confirmation with the server id`, () => {
+ const overlay = new McpManagementOverlay({
+ locale,
+ surface: surface(listSnapshot()),
+ viewportRows: () => 8,
+ onClose: () => undefined,
+ onChange: () => undefined,
+ });
+ overlay.render(100);
+
+ overlay.handleInput('d');
+ const text = overlay.render(100).map(stripAnsi).join('\n');
+ assert.ok(text.includes(expected.confirmRemove));
+ assert.ok(text.includes(expected.confirmHint));
+ });
+
+ test(`labels the busy phase per action kind in ${locale}`, () => {
+ const mcp = surface(listSnapshot());
+ mcp.execute = () => new Promise(() => undefined);
+ const overlay = new McpManagementOverlay({
+ locale,
+ surface: mcp,
+ viewportRows: () => 8,
+ onClose: () => undefined,
+ onChange: () => undefined,
+ });
+ overlay.render(100);
+
+ const labels: [string, string][] = [
+ ['t', expected.testing],
+ ['r', expected.reconnecting],
+ [' ', expected.applying],
+ ];
+ for (const [key, label] of labels) {
+ overlay.handleInput(key);
+ assert.ok(overlay.render(100).map(stripAnsi).join('\n').includes(label));
+ overlay.handleInput('\u001b');
+ }
+ });
+
+ test(`localizes ${locale} action result notices`, async () => {
+ const mcp = surface(listSnapshot());
+ mcp.execute = async () => ({ status: 'applied', effect: 'published' });
+ const overlay = new McpManagementOverlay({
+ locale,
+ surface: mcp,
+ viewportRows: () => 8,
+ onClose: () => undefined,
+ onChange: () => undefined,
+ });
+ overlay.render(100);
+
+ overlay.handleInput(' ');
+ await new Promise<void>((resolve) => setImmediate(resolve));
+ assert.ok(overlay.render(100).map(stripAnsi).join('\n').includes(expected.published));
+
+ mcp.execute = async () => ({ status: 'failed', reason: 'manager-failed' });
+ overlay.handleInput('t');
+ await new Promise<void>((resolve) => setImmediate(resolve));
+ assert.ok(overlay.render(100).map(stripAnsi).join('\n').includes(expected.managerFailed));
+ });
+ }
+
+ const RESULT_CASES: readonly [TuiMcpActionResult, string][] = [
+ [{ status: 'conflict', reason: 'exists' }, 'exists'],
+ [{ status: 'conflict', reason: 'stale_config' }, 'stale_config'],
+ [{ status: 'conflict', reason: 'stale_edit' }, 'stale_edit'],
+ [{ status: 'conflict', reason: 'stale_import' }, 'stale_import'],
+ [{ status: 'conflict', reason: 'missing' }, 'missing'],
+ [{ status: 'failed', reason: 'closed' }, 'closed'],
+ [{ status: 'failed', reason: 'invalid-config' }, 'invalid-config'],
+ [{ status: 'failed', reason: 'credential-cleanup-failed' }, 'credential-cleanup-failed'],
+ [{ status: 'failed', reason: 'persist-failed' }, 'persist-failed'],
+ [{ status: 'failed', reason: 'manager-failed' }, 'manager-failed'],
+ [{ status: 'applied', effect: 'published' }, 'published'],
+ [{ status: 'applied', effect: 'pending_host' }, 'pending_host'],
+ [{ status: 'applied', effect: 'sync_failed' }, 'sync_failed'],
+ [{ status: 'applied', effect: 'publication_failed' }, 'publication_failed'],
+ [{ status: 'tested', test: testResult(true), effect: 'published' }, 'test_ok'],
+ [{ status: 'tested', test: testResult(false), effect: 'published' }, 'test_failed'],
+ [
+ { status: 'tested', test: testResult(true), effect: 'publication_failed' },
+ 'test_publication_failed',
+ ],
+ [{ status: 'tested', test: testResult(true), effect: 'pending_host' }, 'test_pending_host'],
+ ];
+
+ for (const locale of ['en', 'zh'] as const) {
+ test(`routes every action result to its ${locale} catalog notice`, async () => {
+ const results = TUI_COPY_RESOURCES['mcp-status'][locale].editor.results;
+ for (const [result, code] of RESULT_CASES) {
+ const mcp = surface(listSnapshot());
+ mcp.execute = async () => result;
+ const overlay = new McpManagementOverlay({
+ locale,
+ surface: mcp,
+ viewportRows: () => 8,
+ onClose: () => undefined,
+ onChange: () => undefined,
+ });
+ overlay.render(100);
+
+ overlay.handleInput(' ');
+ await new Promise<void>((resolve) => setImmediate(resolve));
+ const text = overlay.render(100).map(stripAnsi).join('\n');
+ assert.ok(text.includes(results[code as keyof typeof results]), `${code} (${locale})`);
+ }
+ });
+ }
+
+ test('renders an unknown runtime result code as the raw code', async () => {
+ const mcp = surface(listSnapshot());
+ mcp.execute = async () =>
+ ({ status: 'failed', reason: 'future-code' }) as unknown as TuiMcpActionResult;
+ const overlay = new McpManagementOverlay({
+ locale: 'en',
+ surface: mcp,
+ viewportRows: () => 8,
+ onClose: () => undefined,
+ onChange: () => undefined,
+ });
+ overlay.render(100);
+
+ overlay.handleInput(' ');
+ await new Promise<void>((resolve) => setImmediate(resolve));
+ assert.ok(overlay.render(100).map(stripAnsi).join('\n').includes('future-code'));
+ });
+
+ test('keeps punctuation-sensitive catalog strings byte-for-byte', () => {
+ const editor = (locale: 'en' | 'zh') => TUI_COPY_RESOURCES['mcp-status'][locale].editor;
+ assert.equal(editor('zh').results.stale_import, '导入项已变化,请重新预览。');
+ assert.equal(editor('en').confirmImportTitle, 'Import MCP servers?');
+ assert.equal(editor('zh').confirmImportTitle, '导入 MCP 服务器?');
+ });
});
+function testResult(ok: boolean): McpTestResult {
+ return {
+ ok,
+ latencyMs: 1,
+ status: {
+ serverId: 'filesystem',
+ state: ok ? 'connected' : 'error',
+ toolCount: 0,
+ tools: [],
+ updatedAt: 0,
+ },
+ };
+}
+
+function listSnapshot(): TuiMcpSnapshot {
+ return {
+ initialization: 'ready',
+ configuration: 'ready',
+ publication: 'published',
+ toolCount: 0,
+ servers: [
+ { serverId: 'filesystem', configured: true, synchronized: true, enabled: true, toolCount: 0 },
+ ],
+ };
+}
+
+function fakeTui(): TUI {
+ return {
+ requestRender: () => undefined,
+ terminal: { rows: 24, columns: 80 },
+ } as unknown as TUI;
+}
+
function surface(
snapshot: TuiMcpSnapshot,
): TuiMcpManagement & { subscribe(listener: () => void): () => void } {
diff --git a/packages/cli/src/__tests__/pi-tui-runner.test.ts b/packages/cli/src/__tests__/pi-tui-runner.test.ts
index 906fa40a51..e19d253044 100644
--- a/packages/cli/src/__tests__/pi-tui-runner.test.ts
+++ b/packages/cli/src/__tests__/pi-tui-runner.test.ts
@@ -393,12 +393,17 @@ describe('Maka Pi TUI runner', () => {
terminal.input('/new');
terminal.input('\r');
await waitFor(() => attempted === 1);
- await waitFor(() => plainTerminalOutput(terminal.output()).includes('host_draining'));
+ // The driver's raw error text is not product copy; the TUI reports the
+ // failure with its own localized notice (#2672).
+ await waitFor(() =>
+ plainTerminalOutput(terminal.output()).includes('Could not start a new session'),
+ );
// Identity untouched…
assert.equal(driver.getSessionId(), 'session-1');
// …and the transcript was not wiped by the aborted /new.
- assert.match(plainTerminalOutput(terminal.screenOutput()), /host_draining/);
+ assert.match(plainTerminalOutput(terminal.screenOutput()), /Could not start a new session/);
+ assert.doesNotMatch(plainTerminalOutput(terminal.screenOutput()), /host_draining/);
assert.ok(transcriptBefore.length > 0);
exitMaka(terminal);
@@ -4424,7 +4429,9 @@ describe('Maka Pi TUI runner', () => {
terminal.input('\r');
// The scan failure is surfaced, not swallowed into an empty list.
await waitFor(() =>
- plainTerminalOutput(terminal.output()).includes('读取外部对话失败:corrupt index'),
+ plainTerminalOutput(terminal.output()).includes(
+ 'Could not read external conversations: corrupt index',
+ ),
);
exitMaka(terminal);
@@ -4868,7 +4875,9 @@ describe('Maka Pi TUI runner', () => {
terminal.input('/rewind');
terminal.input('\r');
- await waitFor(() => plainTerminalOutput(terminal.output()).includes('回到选定轮次'));
+ await waitFor(() =>
+ plainTerminalOutput(terminal.output()).includes('Return to before the selected turn'),
+ );
terminal.input('\r');
await waitFor(() => hydrationAttempts === 1);
await waitFor(() => plainTerminalOutput(terminal.screenOutput()).includes('refilled: turn-2'));
@@ -5091,7 +5100,9 @@ describe('Maka Pi TUI runner', () => {
terminal.input('/rewind');
terminal.input('\r');
- await waitFor(() => plainTerminalOutput(terminal.output()).includes('回到选定轮次'));
+ await waitFor(() =>
+ plainTerminalOutput(terminal.output()).includes('Return to before the selected turn'),
+ );
await waitFor(() => plainTerminalOutput(terminal.output()).includes('second question'));
await waitFor(() => plainTerminalOutput(terminal.output()).includes('first question'));
@@ -5099,7 +5110,9 @@ describe('Maka Pi TUI runner', () => {
terminal.input('\r');
await waitFor(() => driver.rewound.length === 1);
assert.deepEqual(driver.rewound, ['turn-2']);
- await waitFor(() => plainTerminalOutput(terminal.output()).includes('已回退到该轮之前'));
+ await waitFor(() =>
+ plainTerminalOutput(terminal.output()).includes('Rewound to before this turn'),
+ );
await waitFor(() => plainTerminalOutput(terminal.output()).includes('first answer'));
// The rewound turn's prompt is refilled into the editor for an edit-and-resend.
await waitFor(() => plainTerminalOutput(terminal.output()).includes('refilled: turn-2'));
@@ -5139,15 +5152,21 @@ describe('Maka Pi TUI runner', () => {
terminal.input('\r');
// The branch switch is still in flight, but the selection must already be
// visibly accepted — control-busy otherwise renders nothing (#3383).
- await waitFor(() => plainTerminalOutput(terminal.output()).includes('正在回退到该轮之前'));
+ await waitFor(() =>
+ plainTerminalOutput(terminal.output()).includes('Rewinding to before this turn'),
+ );
assert.deepEqual(driver.rewound, []);
driver.gate.resolve();
- await waitFor(() => plainTerminalOutput(terminal.output()).includes('已回退到该轮之前'));
+ await waitFor(() =>
+ plainTerminalOutput(terminal.output()).includes('Rewound to before this turn'),
+ );
await waitFor(() => plainTerminalOutput(terminal.output()).includes('refilled: turn-1'));
// The in-progress notice is wiped together with the transcript it announced.
await waitFor(
- () => plainTerminalOutput(terminal.screenOutput()).includes('正在回退到该轮之前') === false,
+ () =>
+ plainTerminalOutput(terminal.screenOutput()).includes('Rewinding to before this turn') ===
+ false,
);
exitMaka(terminal);
@@ -5176,7 +5195,9 @@ describe('Maka Pi TUI runner', () => {
terminal.input('\r');
await waitFor(() => plainTerminalOutput(terminal.output()).includes('first question'));
terminal.input('\r');
- await waitFor(() => plainTerminalOutput(terminal.output()).includes('正在回退到该轮之前'));
+ await waitFor(() =>
+ plainTerminalOutput(terminal.output()).includes('Rewinding to before this turn'),
+ );
// Typed while the branch switch is in flight: this is newer user work and
// must win over the prompt refill (#3383). Enter stays swallowed by
@@ -5190,7 +5211,7 @@ describe('Maka Pi TUI runner', () => {
// The notice wraps across screen lines at 80 columns, so match against a
// whitespace-collapsed copy instead of the raw output.
const collapsedOutput = () => plainTerminalOutput(terminal.output()).replace(/\s+/g, '');
- await waitFor(() => collapsedOutput().includes('未覆盖'));
+ await waitFor(() => collapsedOutput().includes('untouched'));
await waitFor(() => editorInputText(terminal) === 'my draft');
assert.equal(plainTerminalOutput(terminal.output()).includes('refilled: turn-1'), false);
// The ↑ recovery promise must hold even though nothing was submitted in
@@ -5228,7 +5249,9 @@ describe('Maka Pi TUI runner', () => {
terminal.input('\r');
await waitFor(() => plainTerminalOutput(terminal.output()).includes('first question'));
terminal.input('\r');
- await waitFor(() => plainTerminalOutput(terminal.output()).includes('正在回退到该轮之前'));
+ await waitFor(() =>
+ plainTerminalOutput(terminal.output()).includes('Rewinding to before this turn'),
+ );
// A bracketed paste starts while the branch switch is in flight and its end
// marker has not arrived: getText() still reports empty, but the buffered
@@ -5239,7 +5262,7 @@ describe('Maka Pi TUI runner', () => {
// The completion notice wraps across screen lines at 80 columns, so match a
// whitespace-collapsed copy.
await waitFor(() =>
- plainTerminalOutput(terminal.output()).replace(/\s+/g, '').includes('未覆盖'),
+ plainTerminalOutput(terminal.output()).replace(/\s+/g, '').includes('untouched'),
);
// Only now does the paste complete — after the refill decision was made.
@@ -5280,7 +5303,7 @@ describe('Maka Pi TUI runner', () => {
await waitFor(() => terminal.progressStates.at(-1) === true);
terminal.input('\r');
- await waitFor(() => plainTerminalOutput(terminal.output()).includes('无法回退'));
+ await waitFor(() => plainTerminalOutput(terminal.output()).includes('Cannot rewind'));
assert.deepEqual(driver.rewound, []);
driver.turnGate.resolve();
@@ -5362,7 +5385,9 @@ describe('Maka Pi TUI runner', () => {
terminal.input('/rewind');
terminal.input('\r');
- await waitFor(() => plainTerminalOutput(terminal.output()).includes('回到选定轮次'));
+ await waitFor(() =>
+ plainTerminalOutput(terminal.output()).includes('Return to before the selected turn'),
+ );
terminal.input('\r');
await waitFor(() => hydrationAttempts === 1);
@@ -5527,7 +5552,9 @@ describe('Maka Pi TUI runner', () => {
terminal.input('/rewind');
terminal.input('\r');
- await waitFor(() => plainTerminalOutput(terminal.output()).includes('回到选定轮次'));
+ await waitFor(() =>
+ plainTerminalOutput(terminal.output()).includes('Return to before the selected turn'),
+ );
terminal.input('\r');
await waitFor(() => hydrationAttempts === 1);
assert.ok(listener);
@@ -5665,18 +5692,28 @@ describe('Maka Pi TUI runner', () => {
terminal.input('\x1b');
terminal.input('z');
await waitFor(() => editorInputText(terminal) === 'z');
- assert.equal(plainTerminalOutput(terminal.output()).includes('回到选定轮次'), false);
+ assert.equal(
+ plainTerminalOutput(terminal.output()).includes('Return to before the selected turn'),
+ false,
+ );
terminal.input('\x7f');
await waitFor(() => editorInputText(terminal) === '');
// A consecutive Escape pair completes the gesture and opens the picker.
terminal.input('\x1b');
terminal.input('\x1b');
- await waitFor(() => plainTerminalOutput(terminal.output()).includes('回到选定轮次'));
+ await waitFor(() =>
+ plainTerminalOutput(terminal.output()).includes('Return to before the selected turn'),
+ );
// Cancel the picker so Ctrl-C reaches the runner rather than the overlay.
terminal.input('\x1b');
- await waitFor(() => !plainTerminalOutput(terminal.screenOutput()).includes('回到选定轮次'));
+ await waitFor(
+ () =>
+ !plainTerminalOutput(terminal.screenOutput()).includes(
+ 'Return to before the selected turn',
+ ),
+ );
exitMaka(terminal);
await Promise.race([
@@ -5717,7 +5754,10 @@ describe('Maka Pi TUI runner', () => {
// the typed char paints.
terminal.input('z');
await waitFor(() => editorInputText(terminal)?.endsWith('z') === true);
- assert.equal(plainTerminalOutput(terminal.screenOutput()).includes('回到选定轮次'), false);
+ assert.equal(
+ plainTerminalOutput(terminal.screenOutput()).includes('Return to before the selected turn'),
+ false,
+ );
exitMaka(terminal);
await Promise.race([
@@ -5759,7 +5799,10 @@ describe('Maka Pi TUI runner', () => {
// the typed char paints.
terminal.input('z');
await waitFor(() => editorInputText(terminal) === 'z');
- assert.equal(plainTerminalOutput(terminal.screenOutput()).includes('回到选定轮次'), false);
+ assert.equal(
+ plainTerminalOutput(terminal.screenOutput()).includes('Return to before the selected turn'),
+ false,
+ );
exitMaka(terminal);
await Promise.race([
@@ -6026,7 +6069,7 @@ describe('Maka Pi TUI runner', () => {
// The transcript render trails the send by a tick — wait for it.
await waitFor(() => plainTerminalOutput(terminal.output()).includes('/skill:alpha 帮我整理'));
- await waitFor(() => plainTerminalOutput(terminal.output()).includes('已加载技能:Alpha'));
+ await waitFor(() => plainTerminalOutput(terminal.output()).includes('Skills loaded: Alpha'));
exitMaka(terminal);
await Promise.race([
@@ -6064,7 +6107,7 @@ describe('Maka Pi TUI runner', () => {
await waitFor(() => driver.prompts.length === 1);
await waitFor(() => {
const output = plainTerminalOutput(terminal.output());
- return output.includes('已加载技能:Alpha') && output.includes('/skill:typo(未找到)');
+ return output.includes('Skills loaded: Alpha') && output.includes('/skill:typo (not found)');
});
exitMaka(terminal);
@@ -6076,6 +6119,47 @@ describe('Maka Pi TUI runner', () => {
]);
});
+ test('localizes runner notices in Chinese mode', async () => {
+ const terminal = new FakeTerminal();
+ const driver = new HostSkillDriver({
+ loaded: [],
+ failed: [{ request: 'nope', reason: 'not_found' }],
+ receipts: [],
+ });
+ const run = runMakaPiTui({
+ title: 'Maka',
+ driver,
+ cwd: '/repo',
+ model: 'claude-sonnet-4-5',
+ connectionSlug: 'claude-subscription',
+ permissionMode: 'ask',
+ terminal,
+ locale: 'zh',
+ listSkills: async () => [],
+ });
+
+ terminal.input('/skill');
+ terminal.input('\r');
+ await waitFor(() => plainTerminalOutput(terminal.output()).includes('当前没有可调用的技能。'));
+
+ terminal.input('/skill:nope hi');
+ terminal.input('\r');
+ await waitFor(() =>
+ plainTerminalOutput(terminal.output())
+ .replace(/\s+/g, '')
+ .includes('未能加载技能/skill:nope(未找到);未发起模型请求。'),
+ );
+ assert.equal(driver.prompts.length, 0);
+
+ exitMaka(terminal);
+ await Promise.race([
+ run,
+ delay(CLOSE_BUDGET_MS).then(() => {
+ throw new Error('TUI did not close during test cleanup');
+ }),
+ ]);
+ });
+
test('does not create a turn when every skill token fails to resolve', async () => {
{
const terminal = new FakeTerminal();
@@ -6097,10 +6181,12 @@ describe('Maka Pi TUI runner', () => {
terminal.input('/skill:nope hi');
terminal.input('\r');
+ // The notice wraps across screen lines at 80 columns, so match against a
+ // whitespace-collapsed copy instead of the raw output.
await waitFor(() =>
- plainTerminalOutput(terminal.output()).includes(
- '未能加载技能 /skill:nope(未找到);未发起模型请求。',
- ),
+ plainTerminalOutput(terminal.output())
+ .replace(/\s+/g, ' ')
+ .includes('Could not load skills /skill:nope (not found); no model request was made.'),
);
assert.equal(driver.prompts.length, 0);
@@ -6140,10 +6226,14 @@ describe('Maka Pi TUI runner', () => {
terminal.input(prompt);
terminal.input('\r');
+ // The notice wraps across screen lines at 80 columns, so match against a
+ // whitespace-collapsed copy instead of the raw output.
await waitFor(() =>
- plainTerminalOutput(terminal.output()).includes(
- '请求超过 50 个上限(调用请求过多);未发起模型请求。',
- ),
+ plainTerminalOutput(terminal.output())
+ .replace(/\s+/g, ' ')
+ .includes(
+ 'more than the 50-request limit (too many requests); no model request was made.',
+ ),
);
assert.equal(driver.prompts.length, 0);
diff --git a/packages/cli/src/__tests__/tui-copy-catalog.test.ts b/packages/cli/src/__tests__/tui-copy-catalog.test.ts
index b59509f2a4..0a1ef6b4ea 100644
--- a/packages/cli/src/__tests__/tui-copy-catalog.test.ts
+++ b/packages/cli/src/__tests__/tui-copy-catalog.test.ts
@@ -27,6 +27,15 @@ const MESSAGE_VALUES = {
count: 2,
detail: 'HTTP 401',
hasDetail: true,
+ serverId: 'filesystem',
+ names: 'Alpha',
+ failures: '/skill:nope (not found)',
+ outcome: 'no model request was made.',
+ request: 'nope',
+ reason: 'not found',
+ limit: 3,
+ notice: 'The original account was deleted.',
+ recovery: 'Add or enable a connection first.',
} as const;
describe('TUI copy resources', () => {
@@ -80,6 +89,27 @@ describe('TUI copy resources', () => {
assert.equal(formatUiMessage(template, { count: 2 }, 'en'), '2 tools');
});
+ test('localizes rewind and skill notices in both locales', () => {
+ assert.equal(TUI_COPY_RESOURCES.rewind.en.noTargets, 'No turns to rewind to.');
+ assert.equal(TUI_COPY_RESOURCES.rewind.zh.noTargets, '没有可回退的轮次。');
+ assert.equal(
+ formatUiMessage(
+ TUI_COPY_RESOURCES.skills.en.failedItem,
+ { request: 'nope', reason: TUI_COPY_RESOURCES.skills.en.failureReasons.not_found },
+ 'en',
+ ),
+ '/skill:nope (not found)',
+ );
+ assert.equal(
+ formatUiMessage(
+ TUI_COPY_RESOURCES.skills.zh.failedItem,
+ { request: 'nope', reason: TUI_COPY_RESOURCES.skills.zh.failureReasons.not_found },
+ 'zh',
+ ),
+ '/skill:nope(未找到)',
+ );
+ });
+
test('localizes stable onboarding failure codes at the TUI boundary', () => {
assert.equal(
onboardingFailureMessage({ kind: 'rejected', reason: 'connection_not_found' }, 'en'),
diff --git a/packages/cli/src/pi-transcript.ts b/packages/cli/src/pi-transcript.ts
index bbbec37545..1ef16e170d 100644
--- a/packages/cli/src/pi-transcript.ts
+++ b/packages/cli/src/pi-transcript.ts
@@ -37,7 +37,12 @@ import {
import type { ContextBudgetDiagnostic } from '@maka/core/usage-stats/types';
import { providerRetryDisplaySeconds } from '@maka/core/provider-retry-countdown';
import type { ThinkingLevel } from '@maka/core/model-thinking';
-import type { UiLocale } from '@maka/core/ui-locale';
+import {
+ defineUiMessageCatalog,
+ resolveUiMessageCatalog,
+ type UiLocale,
+} from '@maka/core/ui-locale';
+import { TUI_COPY_RESOURCES } from './tui-copy-catalog.js';
import { isActiveShellRunStatus } from '@maka/core/shell-run';
import { mergeShellRunStateWithDiagnostics } from '@maka/core/shell-run-result';
import { projectToolActivityArgs } from '@maka/core/tool-activity-args';
@@ -1871,29 +1876,41 @@ function formatElapsedDuration(elapsedMs: number): string {
* turn). A trailing hint reminds the user that alt+↑ takes them back to edit.
* Renders nothing when both queues are empty.
*/
+interface TuiPendingQueueCopy {
+ readonly steeringLabel: string;
+ readonly queuedLabel: string;
+ readonly requeueHint: string;
+}
+
+const TUI_PENDING_QUEUE_COPY = resolveUiMessageCatalog(
+ defineUiMessageCatalog<TuiPendingQueueCopy>()(TUI_COPY_RESOURCES['pending-queue']),
+);
+
export function renderMakaPiPendingQueue(
state: MakaPiTranscriptState,
width: number,
platform: NodeJS.Platform = process.platform,
+ locale: UiLocale = 'en',
): string[] {
if (state.steering.length === 0 && state.followup.length === 0) {
return [];
}
+ const copy = TUI_PENDING_QUEUE_COPY[locale];
const safeWidth = Math.max(1, width);
const steering = state.steering;
const followup = state.followup;
const lines: string[] = [];
for (const text of steering) {
lines.push(
- fitLine(`${ansi.accent('Steering:')} ${ansi.dim(firstLinePreview(text))}`, safeWidth),
+ fitLine(`${ansi.accent(copy.steeringLabel)} ${ansi.dim(firstLinePreview(text))}`, safeWidth),
);
}
for (const text of followup) {
- lines.push(fitLine(`${ansi.dim('Queued:')} ${ansi.dim(firstLinePreview(text))}`, safeWidth));
+ lines.push(
+ fitLine(`${ansi.dim(copy.queuedLabel)} ${ansi.dim(firstLinePreview(text))}`, safeWidth),
+ );
}
- lines.push(
- fitLine(ansi.dim(renderTuiShortcutCopy('Alt+↑ 取回队列以重新编辑', platform)), safeWidth),
- );
+ lines.push(fitLine(ansi.dim(renderTuiShortcutCopy(copy.requeueHint, platform)), safeWidth));
return lines;
}
diff --git a/packages/cli/src/pi-tui-layout.ts b/packages/cli/src/pi-tui-layout.ts
index 3d31fbe689..bca2fe430c 100644
--- a/packages/cli/src/pi-tui-layout.ts
+++ b/packages/cli/src/pi-tui-layout.ts
@@ -18,6 +18,7 @@
*/
import { Container, type Component, type Terminal } from '@earendil-works/pi-tui';
+import type { UiLocale } from '@maka/core/ui-locale';
// Deep import (pi-tui does not re-export it): the viewport shadow diff must
// compare the same canonical lines pi-tui diffs, and pi-tui normalizes Thai/Lao
// AM sequences before its diff. Pinned to pi-tui 0.80.3.
@@ -111,12 +112,15 @@ export class MakaActivityStripComponent implements Component {
/** The pending-queue bar (Steering:/Queued:) rendered just above the editor. */
export class MakaPendingQueueComponent implements Component {
- constructor(private readonly state: MakaPiTranscriptState) {}
+ constructor(
+ private readonly state: MakaPiTranscriptState,
+ private readonly locale: UiLocale = 'en',
+ ) {}
invalidate(): void {}
render(width: number): string[] {
- return renderMakaPiPendingQueue(this.state, width);
+ return renderMakaPiPendingQueue(this.state, width, process.platform, this.locale);
}
}
diff --git a/packages/cli/src/pi-tui-mcp-status.ts b/packages/cli/src/pi-tui-mcp-status.ts
index 5777570ad2..300d757e99 100644
--- a/packages/cli/src/pi-tui-mcp-status.ts
+++ b/packages/cli/src/pi-tui-mcp-status.ts
@@ -67,8 +67,35 @@ interface TuiMcpStatusCopy {
readonly configuredOnly: string;
readonly configPending: string;
readonly toolCount: string;
+ readonly editor: {
+ readonly addTitle: string;
+ readonly transportTitle: string;
+ readonly protocolTitle: string;
+ readonly confirmAddTitle: string;
+ readonly confirmImportTitle: string;
+ readonly confirmRemoveTitle: string;
+ readonly confirmRemoveCredentialTitle: string;
+ readonly confirmRemoveCredentialDetail: string;
+ readonly confirmHint: string;
+ readonly synchronizing: string;
+ readonly outOfSync: string;
+ readonly action: Readonly<Record<'test' | 'reconnect' | 'apply', string>>;
+ readonly inputLabels: Readonly<Record<InputKind, string>>;
+ readonly hints: Readonly<Record<'args' | 'map' | 'optional' | 'submit' | 'credential', string>>;
+ readonly results: Readonly<Record<TuiMcpResultCode, string>>;
+ };
}
+type TuiMcpResultCode =
+ | Extract<TuiMcpActionResult, { status: 'conflict' | 'failed' }>['reason']
+ | Extract<TuiMcpActionResult, { status: 'applied' }>['effect']
+ | 'turn_active'
+ | 'invalid'
+ | 'test_ok'
+ | 'test_failed'
+ | 'test_publication_failed'
+ | 'test_pending_host';
+
const MCP_STATUS_COPY = resolveUiMessageCatalog(
defineUiMessageCatalog<TuiMcpStatusCopy>()(TUI_COPY_RESOURCES['mcp-status']),
);
@@ -192,7 +219,7 @@ export class McpManagementOverlay implements Component {
void this.runAction({ kind: 'add', serverId: this.phase.draft.serverId, config });
}
} catch {
- this.notice = { level: 'error', text: copy(this.input.locale, 'invalid') };
+ this.notice = { level: 'error', text: resultCopy(this.input.locale, 'invalid') };
this.backToList(false);
}
} else if (this.phase.kind === 'confirm_import' && matchesKey(data, 'y')) {
@@ -318,7 +345,7 @@ export class McpManagementOverlay implements Component {
private startEdit(serverId: string): void {
const edit = this.management()?.configForEdit(serverId);
if (!edit) {
- this.notice = { level: 'error', text: copy(this.input.locale, 'missing') };
+ this.notice = { level: 'error', text: resultCopy(this.input.locale, 'missing') };
return;
}
this.startInput(
@@ -412,7 +439,7 @@ export class McpManagementOverlay implements Component {
}
this.notice = undefined;
} catch {
- this.notice = { level: 'error', text: copy(this.input.locale, 'invalid') };
+ this.notice = { level: 'error', text: resultCopy(this.input.locale, 'invalid') };
}
this.input.onChange();
}
@@ -422,7 +449,7 @@ export class McpManagementOverlay implements Component {
if (!management || this.phase.kind === 'busy') return;
if (this.input.canManage && !this.input.canManage()) {
this.backToList(false);
- this.notice = { level: 'error', text: copy(this.input.locale, 'turn_active') };
+ this.notice = { level: 'error', text: resultCopy(this.input.locale, 'turn_active') };
this.input.onChange();
return;
}
@@ -447,30 +474,15 @@ export class McpManagementOverlay implements Component {
const snapshot = this.input.surface?.snapshot();
if (!snapshot) return unavailableDocument(this.input.locale);
if (this.phase.kind === 'input') return this.inputDocument(width);
+ const editor = MCP_STATUS_COPY[this.input.locale].editor;
if (this.phase.kind === 'add_choice') {
- return [
- heading(this.input.locale, 'Add MCP server', '添加 MCP 服务器'),
- '',
- 'g Guided setup',
- 'j Paste JSON',
- ];
+ return [ansi.bold(editor.addTitle), '', 'g Guided setup', 'j Paste JSON'];
}
if (this.phase.kind === 'transport') {
- return [
- heading(this.input.locale, 'Transport', '传输方式'),
- '',
- '1 stdio',
- '2 Streamable HTTP',
- ];
+ return [ansi.bold(editor.transportTitle), '', '1 stdio', '2 Streamable HTTP'];
}
if (this.phase.kind === 'protocol') {
- return [
- heading(this.input.locale, 'Protocol preference', '协议偏好'),
- '',
- '1 legacy',
- '2 auto',
- '3 2026-07-28',
- ];
+ return [ansi.bold(editor.protocolTitle), '', '1 legacy', '2 auto', '3 2026-07-28'];
}
if (this.phase.kind === 'confirm_add') {
return confirmAddDocument(this.phase.draft, this.input.locale);
@@ -481,29 +493,25 @@ export class McpManagementOverlay implements Component {
if (this.phase.kind === 'confirm_remove') {
return [
ansi.red(
- heading(
- this.input.locale,
- `Remove ${this.phase.serverId}?`,
- `删除 ${this.phase.serverId}?`,
+ ansi.bold(
+ formatUiMessage(
+ editor.confirmRemoveTitle,
+ { serverId: this.phase.serverId },
+ this.input.locale,
+ ),
),
),
'',
- confirmCopy(this.input.locale),
+ editor.confirmHint,
];
}
if (this.phase.kind === 'confirm_remove_publication_credential') {
return [
- heading(
- this.input.locale,
- 'Remove the remote provider credential?',
- '删除远程 Provider 凭据?',
- ),
+ ansi.bold(editor.confirmRemoveCredentialTitle),
'',
- this.input.locale === 'zh'
- ? '这会停止向所选 Runtime Host 发布 MCP 工具。'
- : 'This stops publishing MCP tools to the selected Runtime Host.',
+ editor.confirmRemoveCredentialDetail,
'',
- confirmCopy(this.input.locale),
+ editor.confirmHint,
];
}
if (this.phase.kind === 'busy') return [ansi.yellow(this.phase.label)];
@@ -732,147 +740,82 @@ function actionNotice(
result: TuiMcpActionResult,
locale: UiLocale,
): { level: 'info' | 'error'; text: string } {
- if (result.status === 'conflict') return { level: 'error', text: copy(locale, result.reason) };
- if (result.status === 'failed') return { level: 'error', text: copy(locale, result.reason) };
+ if (result.status === 'conflict' || result.status === 'failed') {
+ return { level: 'error', text: resultCopy(locale, result.reason) };
+ }
if (result.status === 'tested') {
if (result.test.ok && result.effect === 'publication_failed') {
- return { level: 'error', text: copy(locale, 'test_publication_failed') };
+ return { level: 'error', text: resultCopy(locale, 'test_publication_failed') };
}
if (result.test.ok && result.effect === 'pending_host') {
- return { level: 'info', text: copy(locale, 'test_pending_host') };
+ return { level: 'info', text: resultCopy(locale, 'test_pending_host') };
}
return {
level: result.test.ok ? 'info' : 'error',
- text: result.test.ok ? copy(locale, 'test_ok') : copy(locale, 'test_failed'),
+ text: result.test.ok ? resultCopy(locale, 'test_ok') : resultCopy(locale, 'test_failed'),
};
}
return {
level:
result.effect === 'sync_failed' || result.effect === 'publication_failed' ? 'error' : 'info',
- text: copy(locale, result.effect),
+ text: resultCopy(locale, result.effect),
};
}
-function copy(locale: UiLocale, key: string): string {
- const en: Record<string, string> = {
- exists: 'That server ID already exists.',
- stale_config: 'MCP configuration changed; retry the action.',
- stale_edit: 'This server changed; reopen it before editing.',
- stale_import: 'An imported entry changed; preview the import again.',
- missing: 'That server no longer exists.',
- closed: 'The MCP controller is closed.',
- 'invalid-config': 'The server configuration is invalid.',
- 'credential-cleanup-failed':
- 'Stored credentials could not be removed; the configuration was not changed.',
- 'publication-credential-failed': 'The provider credential could not be stored or applied.',
- 'persist-failed': 'The configuration could not be saved.',
- 'manager-failed': 'The MCP connection action failed.',
- turn_active: 'MCP cannot be changed while a turn or another control action is running.',
- invalid: 'Check the value and try again.',
- published: 'Configuration saved and tools refreshed.',
- pending_host: 'Configuration saved; publication is waiting for Runtime Host.',
- sync_failed: 'Configuration saved, but the MCP manager is out of sync.',
- publication_failed: 'Configuration saved, but capability publication failed.',
- test_ok: 'Connection test passed.',
- test_failed: 'Connection test failed.',
- test_publication_failed: 'Connection test passed, but capability publication failed.',
- test_pending_host: 'Connection test passed; publication is waiting for Runtime Host.',
- };
- const zh: Record<string, string> = {
- exists: '该服务器 ID 已存在。',
- stale_config: 'MCP 配置已变化,请重试。',
- stale_edit: '该服务器已变化,请重新打开后编辑。',
- stale_import: '导入项已变化,请重新预览。',
- missing: '该服务器已不存在。',
- closed: 'MCP 控制器已关闭。',
- 'invalid-config': '服务器配置无效。',
- 'credential-cleanup-failed': '无法删除旧凭据,配置未修改。',
- 'publication-credential-failed': '无法保存或应用 Provider 凭据。',
- 'persist-failed': '无法保存配置。',
- 'manager-failed': 'MCP 连接操作失败。',
- turn_active: 'Turn 或其他控制操作运行期间不能修改 MCP。',
- invalid: '请检查输入后重试。',
- published: '配置已保存,工具已刷新。',
- pending_host: '配置已保存,正等待 Runtime Host 发布。',
- sync_failed: '配置已保存,但 MCP Manager 尚未同步。',
- publication_failed: '配置已保存,但 capability 发布失败。',
- test_ok: '连接测试通过。',
- test_failed: '连接测试失败。',
- test_publication_failed: '连接测试通过,但 capability 发布失败。',
- test_pending_host: '连接测试通过,正等待 Runtime Host 发布。',
- };
- return (locale === 'zh' ? zh : en)[key] ?? key;
+function resultCopy(locale: UiLocale, code: TuiMcpResultCode): string {
+ return MCP_STATUS_COPY[locale].editor.results[code] ?? code;
}
function confirmAddDocument(draft: GuidedDraft, locale: UiLocale): string[] {
+ const editor = MCP_STATUS_COPY[locale].editor;
return [
- heading(locale, 'Add this MCP server?', '添加该 MCP 服务器?'),
+ ansi.bold(editor.confirmAddTitle),
'',
`${draft.serverId} · ${draft.transport} · ${draft.protocol}`,
draft.transport === 'stdio' ? (draft.command ?? '') : (draft.url ?? ''),
'',
- confirmCopy(locale),
+ editor.confirmHint,
];
}
function confirmImportDocument(preview: TuiMcpImportPreview, locale: UiLocale): string[] {
+ const editor = MCP_STATUS_COPY[locale].editor;
return [
- heading(locale, 'Import MCP servers?', '导入 MCP 服务器?'),
+ ansi.bold(editor.confirmImportTitle),
'',
...preview.entries.map(
(entry) =>
`${entry.change === 'add' ? '+' : '~'} ${entry.serverId} · ${entry.transport} · ${entry.protocol}`,
),
'',
- confirmCopy(locale),
+ editor.confirmHint,
];
}
function actionLabel(action: TuiMcpAction, locale: UiLocale): string {
- if (locale === 'zh') return '正在应用 MCP 更改…';
- if (action.kind === 'test') return 'Testing MCP server…';
- if (action.kind === 'reconnect') return 'Reconnecting MCP server…';
- return 'Applying MCP configuration…';
+ const labels = MCP_STATUS_COPY[locale].editor.action;
+ if (action.kind === 'test') return labels.test;
+ if (action.kind === 'reconnect') return labels.reconnect;
+ return labels.apply;
}
function inputLabel(kind: InputKind, locale: UiLocale): string {
- const labels: Record<InputKind, [string, string]> = {
- server_id: ['Server ID', '服务器 ID'],
- command: ['Command', '命令'],
- args: ['Arguments', '参数'],
- url: ['Streamable HTTP URL', 'Streamable HTTP URL'],
- cwd: ['Working directory', '工作目录'],
- env: ['Environment', '环境变量'],
- headers: ['Request headers', '请求头'],
- edit: ['Edit server JSON', '编辑服务器 JSON'],
- import: ['Paste MCP JSON', '粘贴 MCP JSON'],
- publication_credential: ['Provider credential', 'Provider 凭据'],
- };
- return labels[kind][locale === 'zh' ? 1 : 0];
+ return MCP_STATUS_COPY[locale].editor.inputLabels[kind];
}
function inputHint(kind: InputKind, locale: UiLocale): string {
- const optional = locale === 'zh' ? '可留空' : 'optional';
- if (kind === 'args') return `JSON string array, ${optional}`;
- if (kind === 'env' || kind === 'headers') return `JSON string map, ${optional}`;
- if (kind === 'cwd') return optional;
- if (kind === 'publication_credential') {
- return locale === 'zh'
- ? '仅保存到本机凭据存储;不会写入 profile、参数或对话 · Enter 提交 · Esc 返回'
- : 'Stored only in the local credential store; never written to profiles, arguments, or chat · Enter submit · Esc back';
- }
- return locale === 'zh' ? 'Enter 提交 · Esc 返回' : 'Enter submit · Esc back';
+ const hints = MCP_STATUS_COPY[locale].editor.hints;
+ if (kind === 'args') return hints.args;
+ if (kind === 'env' || kind === 'headers') return hints.map;
+ if (kind === 'cwd') return hints.optional;
+ if (kind === 'publication_credential') return hints.credential;
+ return hints.submit;
}
function configurationLine(state: 'synchronizing' | 'out_of_sync', locale: UiLocale): string {
- if (state === 'synchronizing') {
- return locale === 'zh' ? '配置同步中…' : 'Configuration is synchronizing…';
- }
- return ansi.red(
- locale === 'zh'
- ? '持久化配置与 MCP Manager 尚未同步。'
- : 'Durable configuration and MCP Manager are out of sync.',
- );
+ const editor = MCP_STATUS_COPY[locale].editor;
+ if (state === 'synchronizing') return editor.synchronizing;
+ return ansi.red(editor.outOfSync);
}
function unavailableDocument(locale: UiLocale): string[] {
@@ -880,14 +823,6 @@ function unavailableDocument(locale: UiLocale): string[] {
return [ansi.yellow(copy.unavailableTitle), copy.unavailableDetail];
}
-function heading(locale: UiLocale, en: string, zh: string): string {
- return ansi.bold(locale === 'zh' ? zh : en);
-}
-
-function confirmCopy(locale: UiLocale): string {
- return locale === 'zh' ? 'y 确认 · Esc 取消' : 'y Confirm · Esc cancel';
-}
-
function loadingCopy(locale: UiLocale): string {
return MCP_STATUS_COPY[locale].loading;
}
diff --git a/packages/cli/src/pi-tui-runner.ts b/packages/cli/src/pi-tui-runner.ts
index 167f1016da..299a3fca5a 100644
--- a/packages/cli/src/pi-tui-runner.ts
+++ b/packages/cli/src/pi-tui-runner.ts
@@ -40,7 +40,10 @@ import {
} from '@maka/core/model-thinking';
import { type ModelInfo, type ProviderType } from '@maka/core/llm-connections';
import type { OrchestrationMode } from '@maka/core/orchestration';
-import type { SkillInvocationResult } from '@maka/core/skill-invocation';
+import type {
+ SkillInvocationFailureReason,
+ SkillInvocationResult,
+} from '@maka/core/skill-invocation';
import { projectRevisionLinkedSessionTree } from '@maka/core/session-revisions';
import {
slashCommandsForSurface,
@@ -52,7 +55,12 @@ import {
type SessionSummary,
type StoredMessage,
} from '@maka/core/session';
-import { formatUiMessage, type UiLocale } from '@maka/core/ui-locale';
+import {
+ defineUiMessageCatalog,
+ formatUiMessage,
+ resolveUiMessageCatalog,
+ type UiLocale,
+} from '@maka/core/ui-locale';
import {
buildForeignSessionHandoffMessage,
foreignSessionHandoffDisplayText,
@@ -167,6 +175,7 @@ import {
isLiveGoalStatus,
} from './pi-goal.js';
import { getTuiPrimaryGuidance } from './tui-primary-guidance.js';
+import { TUI_COPY_RESOURCES } from './tui-copy-catalog.js';
import type { GoalControlAction, GoalProjection } from '@maka/runtime-host/protocol';
export interface MakaPiTuiInput {
@@ -323,36 +332,81 @@ export function safeBoundaryResumeParkedCopy(reason: TurnResumeParkReason): {
}
}
+interface TuiRewindCopy {
+ readonly pickerTitle: string;
+ readonly pending: string;
+ readonly doneRefilled: string;
+ readonly doneKeptDraft: string;
+ readonly noTargets: string;
+ readonly busy: string;
+ readonly pickerHint: string;
+}
+
+interface TuiSkillsCopy {
+ readonly pickerTitle: string;
+ readonly usage: string;
+ readonly noneAvailable: string;
+ readonly loaded: string;
+ readonly loadFailed: string;
+ readonly outcomeNoRequest: string;
+ readonly outcomeMarkersNotSent: string;
+ readonly failedItem: string;
+ readonly tooManyRequestsItem: string;
+ readonly listSeparator: string;
+ readonly failureReasons: Readonly<Record<SkillInvocationFailureReason, string>>;
+}
+
+interface TuiConnectionIdentityCopy {
+ readonly withRecovery: string;
+ readonly emptyChoiceRecovery: string;
+ readonly confirmAccount: string;
+ readonly accountDeleted: string;
+ readonly identityMismatch: string;
+ readonly accountDisabled: string;
+}
+
+interface TuiSessionActionsCopy {
+ readonly foreignScanFailed: string;
+ readonly newSessionFailed: string;
+}
+
+const TUI_REWIND_COPY = resolveUiMessageCatalog(
+ defineUiMessageCatalog<TuiRewindCopy>()(TUI_COPY_RESOURCES.rewind),
+);
+
+const TUI_SKILLS_COPY = resolveUiMessageCatalog(
+ defineUiMessageCatalog<TuiSkillsCopy>()(TUI_COPY_RESOURCES.skills),
+);
+
+const TUI_CONNECTION_IDENTITY_COPY = resolveUiMessageCatalog(
+ defineUiMessageCatalog<TuiConnectionIdentityCopy>()(TUI_COPY_RESOURCES['connection-identity']),
+);
+
+const TUI_SESSION_ACTIONS_COPY = resolveUiMessageCatalog(
+ defineUiMessageCatalog<TuiSessionActionsCopy>()(TUI_COPY_RESOURCES['session-actions']),
+);
+
function sessionConnectionIdentityNotice(
session: Pick<SessionSummary, 'llmConnectionId' | 'llmConnectionSlug'>,
identities: MakaPiTuiInput['connectionIdentities'],
locale: UiLocale,
): string | undefined {
if (!identities) return undefined;
- const emptyChoiceRecovery =
- locale === 'zh'
- ? '如果 /model 没有可选项,请先添加或启用连接(API Key 连接可运行 /setup)。'
- : 'If /model has no choices, add or enable a connection first (run /setup for API-key connections).';
+ const copy = TUI_CONNECTION_IDENTITY_COPY[locale];
+ const withRecovery = (notice: string) =>
+ formatUiMessage(copy.withRecovery, { notice, recovery: copy.emptyChoiceRecovery }, locale);
if (!session.llmConnectionId) {
- return locale === 'zh'
- ? `此任务来自旧版本,需要确认一次账号。运行 /model 选择现有账号和模型。${emptyChoiceRecovery}`
- : `This task comes from an older version and needs a one-time account confirmation. Run /model and choose an existing account and model. ${emptyChoiceRecovery}`;
+ return withRecovery(copy.confirmAccount);
}
const identified = identities.find((entry) => entry.connectionId === session.llmConnectionId);
if (!identified) {
- return locale === 'zh'
- ? `原账号已删除;运行 /model 选择新账号和模型后继续。${emptyChoiceRecovery}`
- : `The original account was deleted. Run /model and choose a new account and model to continue. ${emptyChoiceRecovery}`;
+ return withRecovery(copy.accountDeleted);
}
if (identified.connectionSlug !== session.llmConnectionSlug) {
- return locale === 'zh'
- ? `任务保存的账号身份与当前连接不一致;运行 /model 重新选择账号和模型。${emptyChoiceRecovery}`
- : `The saved account identity no longer matches its connection. Run /model and choose an account and model again. ${emptyChoiceRecovery}`;
+ return withRecovery(copy.identityMismatch);
}
if (!identified.enabled) {
- return locale === 'zh'
- ? `原账号已停用;请启用该账号,或运行 /model 选择新账号和模型。${emptyChoiceRecovery}`
- : `The original account is disabled. Enable it, or run /model and choose a new account and model. ${emptyChoiceRecovery}`;
+ return withRecovery(copy.accountDisabled);
}
return undefined;
}
@@ -584,7 +638,7 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise<void> {
const transcript = new MakaTranscriptComponent(state, metadata);
const activityStrip = new MakaActivityStripComponent(metadata);
- const pendingQueue = new MakaPendingQueueComponent(state);
+ const pendingQueue = new MakaPendingQueueComponent(state, locale);
const statusLine = new MakaStatusLineComponent(metadata);
// Use the vendor editor's full 20-item autocomplete capacity. Larger command
// catalogs remain scrollable and keep an exact position/total counter.
@@ -737,35 +791,51 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise<void> {
// autocomplete or picker open.
void listSkillsCached(true);
- const SKILL_INVOCATION_FAILURE_REASON_LABEL: Record<string, string> = {
- not_found: '未找到',
- disabled: '已禁用',
- host_incompatible: '当前主机缺少其依赖的工具',
- invalid_name: '名称无效',
- too_many_requests: '调用请求过多',
- };
+ const skillsCopy = TUI_SKILLS_COPY[locale];
const showSkillInvocation = (skillInvocation: SkillInvocationResult): void => {
const failed = skillInvocation.failed;
const failedLabels = failed.map((entry) =>
entry.reason === 'too_many_requests'
- ? `请求超过 ${entry.requestLimit} 个上限(${SKILL_INVOCATION_FAILURE_REASON_LABEL[entry.reason]})`
- : `/skill:${entry.request}(${SKILL_INVOCATION_FAILURE_REASON_LABEL[entry.reason] ?? entry.reason})`,
+ ? formatUiMessage(
+ skillsCopy.tooManyRequestsItem,
+ { limit: entry.requestLimit, reason: skillsCopy.failureReasons[entry.reason] },
+ locale,
+ )
+ : formatUiMessage(
+ skillsCopy.failedItem,
+ { request: entry.request, reason: skillsCopy.failureReasons[entry.reason] },
+ locale,
+ ),
);
if (failed.length > 0) {
state.entries.push({
kind: 'notice',
level: 'info',
- text: `未能加载技能 ${failedLabels.join('、')};${
- skillInvocation.loaded.length === 0 ? '未发起模型请求。' : '失败的调用标记未发送给模型。'
- }`,
+ text: formatUiMessage(
+ skillsCopy.loadFailed,
+ {
+ failures: failedLabels.join(skillsCopy.listSeparator),
+ outcome:
+ skillInvocation.loaded.length === 0
+ ? skillsCopy.outcomeNoRequest
+ : skillsCopy.outcomeMarkersNotSent,
+ },
+ locale,
+ ),
});
}
if (skillInvocation.loaded.length > 0) {
state.entries.push({
kind: 'notice',
level: 'info',
- text: `已加载技能:${skillInvocation.loaded.map((skill) => skill.name).join('、')}`,
+ text: formatUiMessage(
+ skillsCopy.loaded,
+ {
+ names: skillInvocation.loaded.map((skill) => skill.name).join(skillsCopy.listSeparator),
+ },
+ locale,
+ ),
});
}
requestRender();
@@ -1932,7 +2002,7 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise<void> {
const pendingNotice: (typeof state.entries)[number] = {
kind: 'notice',
level: 'info',
- text: '正在回退到该轮之前…',
+ text: TUI_REWIND_COPY[locale].pending,
};
state.entries.push(pendingNotice);
requestRender();
@@ -1957,9 +2027,7 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise<void> {
state.entries.push({
kind: 'notice',
level: 'info',
- text: refill
- ? '已回退到该轮之前(分支为新任务,原任务保留),该轮 prompt 已回填输入框,可修改后重新发送。'
- : '已回退到该轮之前(分支为新任务,原任务保留)。输入框已有未发送内容,未覆盖;该轮 prompt 已存入输入历史,可按 ↑ 找回。',
+ text: refill ? TUI_REWIND_COPY[locale].doneRefilled : TUI_REWIND_COPY[locale].doneKeptDraft,
});
requestRender();
} catch (error) {
@@ -2486,6 +2554,9 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise<void> {
// prefixed select value so they never collide with Maka session ids. A scan
// error is surfaced (not silently swallowed): degrade to no rows but tell
// the user why, so a real store bug isn't mistaken for "no sessions".
+ // Deliberate #2672 exception: the interpolated detail is a local
+ // file-scan diagnostic (not backend copy) and the TUI has no log channel
+ // to carry it, so dropping it would hide the only debugging signal.
const foreignByValue = new Map<string, ForeignSessionSummary>();
if ('error' in foreignScan) {
const detail =
@@ -2493,7 +2564,11 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise<void> {
state.entries.push({
kind: 'notice',
level: 'error',
- text: `读取外部对话失败:${detail}`,
+ text: formatUiMessage(
+ TUI_SESSION_ACTIONS_COPY[locale].foreignScanFailed,
+ { detail },
+ locale,
+ ),
});
} else {
for (const summary of foreignScan.summaries) {
@@ -2578,7 +2653,7 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise<void> {
state.entries.push({
kind: 'notice',
level: 'info',
- text: '没有可回退的轮次。',
+ text: TUI_REWIND_COPY[locale].noTargets,
});
requestRender();
return;
@@ -2587,9 +2662,10 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise<void> {
value: target.turnId,
label: target.label,
}));
+ const rewindCopy = TUI_REWIND_COPY[locale];
showSelectPicker(
- 'Rewind',
- 'Rewind',
+ rewindCopy.pickerTitle,
+ rewindCopy.pickerTitle,
items,
(item) => {
// runControl drops the action silently when busy is already held (e.g.
@@ -2600,7 +2676,7 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise<void> {
state.entries.push({
kind: 'notice',
level: 'error',
- text: '无法回退:当前有正在进行的操作 — 请等待其完成,或中断(Esc)后重试。',
+ text: rewindCopy.busy,
});
requestRender();
return;
@@ -2610,7 +2686,7 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise<void> {
{
minPrimaryColumnWidth: 24,
maxPrimaryColumnWidth: 48,
- hint: '回到选定轮次之前(丢弃该轮及之后,prompt 回填输入框) · enter 选择 / esc 取消',
+ hint: rewindCopy.pickerHint,
},
);
};
@@ -2618,17 +2694,15 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise<void> {
const newSession = async (): Promise<boolean> => {
try {
await input.driver.startNewSession();
- } catch (error) {
+ } catch {
// The identity swap was aborted driver-side: the previous Session, its
// transcript, and every user-command card stay exactly as they were.
- // Surface why instead of silently stranding the running commands.
+ // Surface the failure instead of silently stranding the running
+ // commands; the driver's raw error text is not product copy (#2672).
state.entries.push({
kind: 'notice',
level: 'error',
- text:
- error instanceof Error && error.message.length > 0
- ? error.message
- : '无法开始新会话:停止本地命令失败,请稍后重试。',
+ text: TUI_SESSION_ACTIONS_COPY[locale].newSessionFailed,
});
requestRender();
return false;
@@ -2792,13 +2866,13 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise<void> {
state.entries.push({
kind: 'notice',
level: 'info',
- text: '当前没有可调用的技能。',
+ text: skillsCopy.noneAvailable,
});
requestRender();
return;
}
showSelectPicker(
- 'Invoke Skill',
+ skillsCopy.pickerTitle,
String(entries.length),
skillPickerItems(entries),
(item) => {
@@ -3341,7 +3415,7 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise<void> {
state.entries.push({
kind: 'notice',
level: 'error',
- text: 'Usage: /skill,或直接在消息中输入 /skill:<name>',
+ text: skillsCopy.usage,
});
requestRender();
return;
diff --git a/packages/cli/src/tui-copy-catalog.ts b/packages/cli/src/tui-copy-catalog.ts
index 6f1302b8a5..28bd48ba35 100644
--- a/packages/cli/src/tui-copy-catalog.ts
+++ b/packages/cli/src/tui-copy-catalog.ts
@@ -59,6 +59,70 @@ export const TUI_COPY_RESOURCES = {
configuredOnly: 'configured only',
configPending: 'config pending',
toolCount: '{count, plural, one {# tool} other {# tools}}',
+ editor: {
+ addTitle: 'Add MCP server',
+ transportTitle: 'Transport',
+ protocolTitle: 'Protocol preference',
+ confirmAddTitle: 'Add this MCP server?',
+ confirmImportTitle: 'Import MCP servers?',
+ confirmRemoveTitle: 'Remove {serverId}?',
+ confirmRemoveCredentialTitle: 'Remove the remote provider credential?',
+ confirmRemoveCredentialDetail:
+ 'This stops publishing MCP tools to the selected Runtime Host.',
+ confirmHint: 'y Confirm · Esc cancel',
+ synchronizing: 'Configuration is synchronizing…',
+ outOfSync: 'Durable configuration and MCP Manager are out of sync.',
+ action: {
+ test: 'Testing MCP server…',
+ reconnect: 'Reconnecting MCP server…',
+ apply: 'Applying MCP configuration…',
+ },
+ inputLabels: {
+ server_id: 'Server ID',
+ command: 'Command',
+ args: 'Arguments',
+ url: 'Streamable HTTP URL',
+ cwd: 'Working directory',
+ env: 'Environment',
+ headers: 'Request headers',
+ edit: 'Edit server JSON',
+ import: 'Paste MCP JSON',
+ publication_credential: 'Provider credential',
+ },
+ hints: {
+ args: 'JSON string array, optional',
+ map: 'JSON string map, optional',
+ optional: 'optional',
+ submit: 'Enter submit · Esc back',
+ credential:
+ 'Stored only in the local credential store; never written to profiles, arguments, or chat · Enter submit · Esc back',
+ },
+ results: {
+ exists: 'That server ID already exists.',
+ stale_config: 'MCP configuration changed; retry the action.',
+ stale_edit: 'This server changed; reopen it before editing.',
+ stale_import: 'An imported entry changed; preview the import again.',
+ missing: 'That server no longer exists.',
+ closed: 'The MCP controller is closed.',
+ 'invalid-config': 'The server configuration is invalid.',
+ 'credential-cleanup-failed':
+ 'Stored credentials could not be removed; the configuration was not changed.',
+ 'publication-credential-failed':
+ 'The provider credential could not be stored or applied.',
+ 'persist-failed': 'The configuration could not be saved.',
+ 'manager-failed': 'The MCP connection action failed.',
+ turn_active: 'MCP cannot be changed while a turn or another control action is running.',
+ invalid: 'Check the value and try again.',
+ published: 'Configuration saved and tools refreshed.',
+ pending_host: 'Configuration saved; publication is waiting for Runtime Host.',
+ sync_failed: 'Configuration saved, but the MCP manager is out of sync.',
+ publication_failed: 'Configuration saved, but capability publication failed.',
+ test_ok: 'Connection test passed.',
+ test_failed: 'Connection test failed.',
+ test_publication_failed: 'Connection test passed, but capability publication failed.',
+ test_pending_host: 'Connection test passed; publication is waiting for Runtime Host.',
+ },
+ },
},
zh: {
title: 'MCP 服务器',
@@ -97,6 +161,66 @@ export const TUI_COPY_RESOURCES = {
configuredOnly: '仅已配置',
configPending: '配置待同步',
toolCount: '{count} 个工具',
+ editor: {
+ addTitle: '添加 MCP 服务器',
+ transportTitle: '传输方式',
+ protocolTitle: '协议偏好',
+ confirmAddTitle: '添加该 MCP 服务器?',
+ confirmImportTitle: '导入 MCP 服务器?',
+ confirmRemoveTitle: '删除 {serverId}?',
+ confirmRemoveCredentialTitle: '删除远程 Provider 凭据?',
+ confirmRemoveCredentialDetail: '这会停止向所选 Runtime Host 发布 MCP 工具。',
+ confirmHint: 'y 确认 · Esc 取消',
+ synchronizing: '配置同步中…',
+ outOfSync: '持久化配置与 MCP Manager 尚未同步。',
+ action: {
+ test: '正在测试 MCP 服务器…',
+ reconnect: '正在重连 MCP 服务器…',
+ apply: '正在应用 MCP 配置…',
+ },
+ inputLabels: {
+ server_id: '服务器 ID',
+ command: '命令',
+ args: '参数',
+ url: 'Streamable HTTP URL',
+ cwd: '工作目录',
+ env: '环境变量',
+ headers: '请求头',
+ edit: '编辑服务器 JSON',
+ import: '粘贴 MCP JSON',
+ publication_credential: 'Provider 凭据',
+ },
+ hints: {
+ args: 'JSON string array, 可留空',
+ map: 'JSON string map, 可留空',
+ optional: '可留空',
+ submit: 'Enter 提交 · Esc 返回',
+ credential: '仅保存到本机凭据存储;不会写入 profile、参数或对话 · Enter 提交 · Esc 返回',
+ },
+ results: {
+ exists: '该服务器 ID 已存在。',
+ stale_config: 'MCP 配置已变化,请重试。',
+ stale_edit: '该服务器已变化,请重新打开后编辑。',
+ stale_import: '导入项已变化,请重新预览。',
+ missing: '该服务器已不存在。',
+ closed: 'MCP 控制器已关闭。',
+ 'invalid-config': '服务器配置无效。',
+ 'credential-cleanup-failed': '无法删除旧凭据,配置未修改。',
+ 'publication-credential-failed': '无法保存或应用 Provider 凭据。',
+ 'persist-failed': '无法保存配置。',
+ 'manager-failed': 'MCP 连接操作失败。',
+ turn_active: 'Turn 或其他控制操作运行期间不能修改 MCP。',
+ invalid: '请检查输入后重试。',
+ published: '配置已保存,工具已刷新。',
+ pending_host: '配置已保存,正等待 Runtime Host 发布。',
+ sync_failed: '配置已保存,但 MCP Manager 尚未同步。',
+ publication_failed: '配置已保存,但 capability 发布失败。',
+ test_ok: '连接测试通过。',
+ test_failed: '连接测试失败。',
+ test_publication_failed: '连接测试通过,但 capability 发布失败。',
+ test_pending_host: '连接测试通过,正等待 Runtime Host 发布。',
+ },
+ },
},
},
pickers: {
@@ -371,6 +495,119 @@ export const TUI_COPY_RESOURCES = {
},
},
},
+ rewind: {
+ en: {
+ pickerTitle: 'Rewind',
+ pending: 'Rewinding to before this turn…',
+ doneRefilled:
+ 'Rewound to before this turn (branched into a new task; the original task is kept). The turn’s prompt was refilled into the input box — edit it and send again.',
+ doneKeptDraft:
+ 'Rewound to before this turn (branched into a new task; the original task is kept). The input box already had unsent content and was left untouched; the turn’s prompt was saved to input history — press ↑ to recall it.',
+ noTargets: 'No turns to rewind to.',
+ busy: 'Cannot rewind: another action is in progress — wait for it to finish, or interrupt (Esc) and retry.',
+ pickerHint:
+ 'Return to before the selected turn (discards it and later turns; the prompt refills the input box) · enter select / esc cancel',
+ },
+ zh: {
+ pickerTitle: '回退',
+ pending: '正在回退到该轮之前…',
+ doneRefilled:
+ '已回退到该轮之前(分支为新任务,原任务保留),该轮 prompt 已回填输入框,可修改后重新发送。',
+ doneKeptDraft:
+ '已回退到该轮之前(分支为新任务,原任务保留)。输入框已有未发送内容,未覆盖;该轮 prompt 已存入输入历史,可按 ↑ 找回。',
+ noTargets: '没有可回退的轮次。',
+ busy: '无法回退:当前有正在进行的操作 — 请等待其完成,或中断(Esc)后重试。',
+ pickerHint: '回到选定轮次之前(丢弃该轮及之后,prompt 回填输入框) · enter 选择 / esc 取消',
+ },
+ },
+ skills: {
+ en: {
+ pickerTitle: 'Invoke Skill',
+ usage: 'Usage: /skill, or type /skill:<name> inline in a message',
+ noneAvailable: 'No skills are available to invoke right now.',
+ loaded: 'Skills loaded: {names}',
+ loadFailed: 'Could not load skills {failures}; {outcome}',
+ outcomeNoRequest: 'no model request was made.',
+ outcomeMarkersNotSent: 'the failed invocation markers were not sent to the model.',
+ failedItem: '/skill:{request} ({reason})',
+ tooManyRequestsItem: 'more than the {limit}-request limit ({reason})',
+ listSeparator: ', ',
+ failureReasons: {
+ invalid_name: 'invalid name',
+ not_found: 'not found',
+ disabled: 'disabled',
+ host_incompatible: 'the current host is missing a tool it depends on',
+ resolution_failed: 'resolution failed',
+ too_many_requests: 'too many requests',
+ },
+ },
+ zh: {
+ pickerTitle: '调用技能',
+ usage: 'Usage: /skill,或直接在消息中输入 /skill:<name>',
+ noneAvailable: '当前没有可调用的技能。',
+ loaded: '已加载技能:{names}',
+ loadFailed: '未能加载技能 {failures};{outcome}',
+ outcomeNoRequest: '未发起模型请求。',
+ outcomeMarkersNotSent: '失败的调用标记未发送给模型。',
+ failedItem: '/skill:{request}({reason})',
+ tooManyRequestsItem: '请求超过 {limit} 个上限({reason})',
+ listSeparator: '、',
+ failureReasons: {
+ invalid_name: '名称无效',
+ not_found: '未找到',
+ disabled: '已禁用',
+ host_incompatible: '当前主机缺少其依赖的工具',
+ resolution_failed: '解析失败',
+ too_many_requests: '调用请求过多',
+ },
+ },
+ },
+ 'connection-identity': {
+ en: {
+ withRecovery: '{notice} {recovery}',
+ emptyChoiceRecovery:
+ 'If /model has no choices, add or enable a connection first (run /setup for API-key connections).',
+ confirmAccount:
+ 'This task comes from an older version and needs a one-time account confirmation. Run /model and choose an existing account and model.',
+ accountDeleted:
+ 'The original account was deleted. Run /model and choose a new account and model to continue.',
+ identityMismatch:
+ 'The saved account identity no longer matches its connection. Run /model and choose an account and model again.',
+ accountDisabled:
+ 'The original account is disabled. Enable it, or run /model and choose a new account and model.',
+ },
+ zh: {
+ withRecovery: '{notice}{recovery}',
+ emptyChoiceRecovery:
+ '如果 /model 没有可选项,请先添加或启用连接(API Key 连接可运行 /setup)。',
+ confirmAccount: '此任务来自旧版本,需要确认一次账号。运行 /model 选择现有账号和模型。',
+ accountDeleted: '原账号已删除;运行 /model 选择新账号和模型后继续。',
+ identityMismatch: '任务保存的账号身份与当前连接不一致;运行 /model 重新选择账号和模型。',
+ accountDisabled: '原账号已停用;请启用该账号,或运行 /model 选择新账号和模型。',
+ },
+ },
+ 'session-actions': {
+ en: {
+ foreignScanFailed: 'Could not read external conversations: {detail}',
+ newSessionFailed: 'Could not start a new session. Try again shortly.',
+ },
+ zh: {
+ foreignScanFailed: '读取外部对话失败:{detail}',
+ newSessionFailed: '无法开始新会话,请稍后重试。',
+ },
+ },
+ 'pending-queue': {
+ en: {
+ steeringLabel: 'Steering:',
+ queuedLabel: 'Queued:',
+ requeueHint: 'Alt+↑ take queued messages back to re-edit',
+ },
+ zh: {
+ steeringLabel: '注入:',
+ queuedLabel: '已排队:',
+ requeueHint: 'Alt+↑ 取回队列以重新编辑',
+ },
+ },
'session-status': {
en: {
running: 'running',
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org