dgxay-code-cli 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli/repl.js CHANGED
@@ -16,8 +16,9 @@ const fs = require('fs');
16
16
  const os = require('os');
17
17
  const path = require('path');
18
18
 
19
- const { COLORS, SIGN, termWidth, setColorMode, colorEnabled, displayWidth } = require('./ansi');
20
- const { LineEditor, createLineReader } = require('./editor');
19
+ const { COLORS, SIGN, termWidth, setColorMode, colorEnabled, displayWidth, clearScreen, useUnicode } = require('./ansi');
20
+ const { t } = require('./i18n');
21
+ const { LineEditor, createLineReader, detectCapabilities, wrapDisplay } = require('./editor');
21
22
  const { parseArgs } = require('./args');
22
23
  const print = require('./print');
23
24
  const { askApproval } = require('./approval');
@@ -73,7 +74,7 @@ function createRuntime(options = {}) {
73
74
  if (options.workspace) {
74
75
  const resolved = path.resolve(options.workspace);
75
76
  if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) st.cwd = resolved;
76
- else throw Object.assign(new Error(`工作空间不存在:${options.workspace}`), { userError: true });
77
+ else throw Object.assign(new Error(`Workspace not found: ${options.workspace}`), { userError: true });
77
78
  } else if (config.work?.workspace && fs.existsSync(config.work.workspace)) {
78
79
  st.cwd = config.work.workspace;
79
80
  } else {
@@ -82,7 +83,7 @@ function createRuntime(options = {}) {
82
83
 
83
84
  if (options.resume) {
84
85
  st.chat = sessions.getChat(String(options.resume));
85
- if (!st.chat) throw Object.assign(new Error(`找不到会话:${options.resume}`), { userError: true });
86
+ if (!st.chat) throw Object.assign(new Error(`Conversation not found: ${options.resume}`), { userError: true });
86
87
  } else if (options.continueLast) {
87
88
  st.chat = sessions.listChats()[0] || null;
88
89
  }
@@ -94,21 +95,21 @@ function createRuntime(options = {}) {
94
95
  /* ------------------------------ 展示 ------------------------------ */
95
96
 
96
97
  function permissionBadge(st) {
97
- const meta = settings.PERMISSIONS[st.runtime.permission] || settings.PERMISSIONS.normal;
98
- const tone = st.runtime.permission === 'full' ? COLORS.red : st.runtime.permission === 'auto' ? COLORS.green : COLORS.faint;
99
- return tone(meta.label);
98
+ const tone =
99
+ st.runtime.permission === 'full' ? COLORS.red : st.runtime.permission === 'auto' ? COLORS.green : COLORS.faint;
100
+ return tone(t(`perm_${st.runtime.permission}`));
100
101
  }
101
102
 
102
103
  function modeBadge(st) {
103
- return st.runtime.mode === 'work' ? COLORS.accent('工作') : COLORS.faint('对话');
104
+ return st.runtime.mode === 'work' ? COLORS.accent(t('mode_work')) : COLORS.faint(t('mode_chat'));
104
105
  }
105
106
 
107
+ /** 输入框上方的一行说明(模型 / 技能) */
106
108
  function promptFor(st) {
107
109
  const model = settings.activeModel(settings.readConfig());
108
- const name = model?.name || '未选择模型';
109
- const marker = st.runtime.mode === 'work' ? COLORS.accent('❯') : COLORS.faint('❯');
110
- const skillChip = st.runtime.skill ? ` ${COLORS.accent(`⛭ ${st.runtime.skill}`)}` : '';
111
- return `${marker} ${COLORS.faint(name)}${skillChip} `
110
+ const name = model?.name || t('no_model');
111
+ const skill = st.runtime.skill ? ` · ⛭ ${st.runtime.skill}` : '';
112
+ return `${name}${skill}`;
112
113
  }
113
114
 
114
115
  function showStatus(st, { withRing = true } = {}) {
@@ -119,12 +120,12 @@ function showStatus(st, { withRing = true } = {}) {
119
120
  modeBadge(st),
120
121
  permissionBadge(st),
121
122
  state.workspaceLabel(st.cwd),
122
- `${chat ? chat.messages.filter((m) => m.role !== 'terminal').length : 0} 条消息`
123
+ `${chat ? chat.messages.filter((m) => m.role !== 'terminal').length : 0} ${t('messages')}`
123
124
  ];
124
125
  const left = parts.filter(Boolean).join(COLORS.faint(' · '));
125
126
  const right = withRing ? ringLine(info) : '';
126
127
  const width = termWidth();
127
- const gap = Math.max(2, width - COLORS.dim('').length - stripLen(left) - stripLen(right) - 2);
128
+ const gap = Math.max(2, width - stripLen(left) - stripLen(right) - 2);
128
129
  process.stdout.write(`\n${left}${' '.repeat(gap)}${right}\n\n`);
129
130
  }
130
131
 
@@ -138,20 +139,24 @@ function showWelcome(st) {
138
139
  const model = settings.activeModel(config);
139
140
  const chat = st.chat;
140
141
  if (chat && chat.messages.length) {
141
- print.hint(`已恢复会话「${chat.title}」(${chat.messages.filter((m) => m.role !== 'terminal').length} 条消息)`);
142
+ print.hint(
143
+ `${t('restored')} "${chat.title}" (${chat.messages.filter((m) => m.role !== 'terminal').length} ${t('messages')})`
144
+ );
142
145
  }
143
146
  print.keyValue([
144
- { label: '模型', value: model ? `${model.providerName} / ${model.name}` : COLORS.yellow('未配置,运行 dgxay config init') },
145
- { label: '工作空间', value: st.cwd },
146
- { label: '权限', value: `${settings.PERMISSIONS[st.runtime.permission].label} · ${settings.PERMISSIONS[st.runtime.permission].desc}` },
147
- { label: '模式', value: st.runtime.mode === 'work' ? '工作模式' : '对话模式' }
147
+ {
148
+ label: t('status_model'),
149
+ value: model ? `${model.providerName} / ${model.name}` : COLORS.yellow(t('no_model'))
150
+ },
151
+ { label: t('status_workspace'), value: st.cwd },
152
+ {
153
+ label: t('status_permission'),
154
+ value: `${t(`perm_${st.runtime.permission}`)} · ${settings.PERMISSIONS[st.runtime.permission].desc}`
155
+ },
156
+ { label: t('status_mode'), value: st.runtime.mode === 'work' ? t('mode_work') : t('mode_chat') }
148
157
  ]);
149
- if (!model) {
150
- print.blank();
151
- print.warn('还没有可用模型,请输入 /config 或退出后运行 dgxay config init');
152
- }
153
158
  print.blank();
154
- print.hint('提示:/ 查看命令 · ! 执行终端命令 · \\ 结尾换行 · Ctrl+C 中断 · Ctrl+D 退出');
159
+ print.hint(t('hint_start'));
155
160
  print.blank();
156
161
  void chat;
157
162
  }
@@ -174,7 +179,7 @@ function persist(st) {
174
179
  if (!st.chat) return;
175
180
  const chat = st.chat;
176
181
  if (!chat.messages.length) return;
177
- if (!chat.title || chat.title === '新对话') {
182
+ if (!chat.title || chat.title === 'New conversation') {
178
183
  const firstUser = chat.messages.find((m) => m.role === 'user' && m.content);
179
184
  if (firstUser) chat.title = sessions.titleFrom(firstUser.content);
180
185
  }
@@ -190,6 +195,33 @@ function historyForAgent(st) {
190
195
  return sessions.trimHistory(sessions.buildHistory(st.chat), HISTORY_LIMIT);
191
196
  }
192
197
 
198
+ /**
199
+ * 回显用户消息:与输入框同样的边框样式,让对话记录看起来是「框」。
200
+ */
201
+ function printUserMessage(text) {
202
+ const width = Math.max(20, Math.min(termWidth() - 1, 100));
203
+ const inner = width - 2;
204
+ const unicode = useUnicode();
205
+ const box = unicode
206
+ ? { tl: '┌', tr: '┐', bl: '└', br: '┘', h: '─', v: '│' }
207
+ : { tl: '+', tr: '+', bl: '+', br: '+', h: '-', v: '|' };
208
+
209
+ const lines = [];
210
+ for (const raw of String(text).split('\n')) {
211
+ for (const piece of wrapDisplay(raw, Math.max(4, inner - 2))) lines.push(piece);
212
+ }
213
+ if (!lines.length) lines.push('');
214
+
215
+ const label = ` ${t('you')} `;
216
+ const topFill = Math.max(0, width - 2 - displayWidth(label));
217
+ process.stdout.write(`\n${COLORS.accent(box.tl + label + box.h.repeat(topFill) + box.tr)}\n`);
218
+ for (const line of lines) {
219
+ const pad = Math.max(0, inner - displayWidth(line) - 1);
220
+ process.stdout.write(`${COLORS.accent(box.v)} ${line}${' '.repeat(pad)}${COLORS.accent(box.v)}\n`);
221
+ }
222
+ process.stdout.write(`${COLORS.accent(box.bl + box.h.repeat(width - 2) + box.br)}\n`);
223
+ }
224
+
193
225
  /* ------------------------------ 对话 ------------------------------ */
194
226
 
195
227
  async function sendMessage(st, editor, text, options = {}) {
@@ -202,7 +234,7 @@ async function sendMessage(st, editor, text, options = {}) {
202
234
  activeModelId: st.runtime.modelId || config.activeModelId
203
235
  });
204
236
  if (!model) {
205
- print.warn('还没有可用模型:先运行 dgxay config init 配置供应商');
237
+ print.warn(t('no_model'));
206
238
  return 1;
207
239
  }
208
240
 
@@ -211,6 +243,12 @@ async function sendMessage(st, editor, text, options = {}) {
211
243
  chat.skill = st.runtime.skill;
212
244
  pushMessage(chat, { role: 'user', content: promptText, mode: st.runtime.mode, permission: st.runtime.permission });
213
245
  persist(st);
246
+ // 记住上一条提示词,供输入框按 ↑ 回填
247
+ st.lastUserPrompt = promptText;
248
+ if (editor) editor.lastUserPrompt = promptText;
249
+
250
+ // 把用户消息按输入框的样式回显一遍,形成对话记录
251
+ printUserMessage(promptText);
214
252
 
215
253
  const jsonMode = String(options.output || 'text') === 'json';
216
254
  const renderer = new StreamRenderer({
@@ -226,7 +264,7 @@ async function sendMessage(st, editor, text, options = {}) {
226
264
  if (!st.streaming || st.abortRequested) return false;
227
265
  st.abortRequested = true;
228
266
  controller.abort();
229
- process.stdout.write(`${COLORS.yellow(`${SIGN.bullet} 正在中断…`)}\n`);
267
+ process.stdout.write(`${COLORS.yellow(`${SIGN.bullet} ${t('interrupted')}`)}\n`);
230
268
  return true;
231
269
  });
232
270
 
@@ -275,7 +313,7 @@ async function sendMessage(st, editor, text, options = {}) {
275
313
  elapsedMs: Date.now() - startedAt
276
314
  };
277
315
  pushMessage(chat, assistantMessage);
278
- if (chat.title === '新对话') chat.title = sessions.titleFrom(promptText);
316
+ if (chat.title === 'New conversation') chat.title = sessions.titleFrom(promptText);
279
317
  persist(st);
280
318
  st.lastResponse = result;
281
319
  updateBusState(st);
@@ -289,17 +327,17 @@ async function sendMessage(st, editor, text, options = {}) {
289
327
  async function runTerminal(st, editor, rawCommand) {
290
328
  const command = String(rawCommand || '').trim();
291
329
  if (!command) {
292
- print.warn('请输入要执行的终端命令');
330
+ print.warn('Enter a command first');
293
331
  return 1;
294
332
  }
295
333
  if (st.terminalBusy) {
296
- print.warn('已有命令正在执行');
334
+ print.warn('A command is already running');
297
335
  return 1;
298
336
  }
299
337
  const config = settings.readConfig();
300
338
  const permission = st.runtime.permission;
301
339
  if (permission === 'none') {
302
- print.warn('无权限模式禁止执行终端命令');
340
+ print.warn('No permission to run shell commands');
303
341
  return 1;
304
342
  }
305
343
 
@@ -307,9 +345,9 @@ async function runTerminal(st, editor, rawCommand) {
307
345
  editor.suspend();
308
346
  const allowed = await askApproval(
309
347
  {
310
- title: 'AI 请求执行终端命令',
348
+ title: t('approval_terminal'),
311
349
  operation: 'run_terminal',
312
- detail: `命令:${command}`,
350
+ detail: `Command: ${command}`,
313
351
  command,
314
352
  workspace: st.cwd,
315
353
  review: '',
@@ -352,14 +390,14 @@ async function runTerminal(st, editor, rawCommand) {
352
390
  message.exitCode = Number.isInteger(result.code) ? result.code : null;
353
391
  message.status = result.aborted ? 'done' : result.code === 0 ? 'done' : 'error';
354
392
  if (result.error) message.error = result.error;
355
- if (chat.title === '新对话') chat.title = sessions.titleFrom(`终端 · ${command}`);
393
+ if (chat.title === 'New conversation') chat.title = sessions.titleFrom(`Terminal · ${command}`);
356
394
  chat.updatedAt = Date.now();
357
395
  persist(st);
358
396
 
359
397
  const code = message.exitCode;
360
- if (result.aborted) print.hint('已停止');
361
- else if (code === 0) process.stdout.write(COLORS.green(` ${SIGN.ok} 完成\n`));
362
- else process.stdout.write(COLORS.red(` ${SIGN.fail} 退出码 ${code ?? '未知'}\n`));
398
+ if (result.aborted) print.hint(t('stopped'));
399
+ else if (code === 0) process.stdout.write(COLORS.green(` ${SIGN.ok} done\n`));
400
+ else process.stdout.write(COLORS.red(` ${SIGN.fail} ${t('exit_code')} ${code ?? '?'}\n`));
363
401
  editor.resume();
364
402
  return code === 0 ? 0 : 1;
365
403
  }
@@ -394,7 +432,7 @@ async function handleSlash(st, editor, input) {
394
432
  const command = findCommand(head);
395
433
 
396
434
  if (!command) {
397
- print.warn(`未知命令:${head},输入 /help 查看全部命令`);
435
+ print.warn(`Unknown command: ${head}. Type /help to list commands`);
398
436
  return;
399
437
  }
400
438
 
@@ -406,7 +444,7 @@ async function handleSlash(st, editor, input) {
406
444
  persist(st);
407
445
  st.chat = sessions.createChat({ mode: st.runtime.mode, skill: st.runtime.skill });
408
446
  updateBusState(st);
409
- print.ok('已开始新对话');
447
+ print.ok(t('new_chat'));
410
448
  return;
411
449
  }
412
450
  case '/list':
@@ -415,7 +453,7 @@ async function handleSlash(st, editor, input) {
415
453
  case '/resume': {
416
454
  const chats = sessions.listChats();
417
455
  if (!chats.length) {
418
- print.warn('还没有历史对话');
456
+ print.warn(t('no_chats'));
419
457
  return;
420
458
  }
421
459
  let target;
@@ -426,13 +464,13 @@ async function handleSlash(st, editor, input) {
426
464
  return;
427
465
  }
428
466
  if (!target) {
429
- print.fail(`找不到会话:${rest}`);
467
+ print.fail(`Conversation not found: ${rest}`);
430
468
  return;
431
469
  }
432
470
  persist(st);
433
471
  st.chat = target;
434
472
  updateBusState(st);
435
- print.ok(`已恢复「${target.title}」(${target.messages.filter((m) => m.role !== 'terminal').length} 条消息)`);
473
+ print.ok(`Restored "${target.title}" (${target.messages.filter((m) => m.role !== 'terminal').length} messages)`);
436
474
  printThread(st);
437
475
  return;
438
476
  }
@@ -440,27 +478,27 @@ async function handleSlash(st, editor, input) {
440
478
  const chats = sessions.listChats();
441
479
  const target = rest ? sessions.resolveChat(rest, chats) : st.chat;
442
480
  if (!target) {
443
- print.fail(`找不到会话:${rest}`);
481
+ print.fail(`Conversation not found: ${rest}`);
444
482
  return;
445
483
  }
446
484
  sessions.deleteChat(target.id);
447
485
  if (st.chat?.id === target.id) st.chat = sessions.listChats()[0] || null;
448
486
  updateBusState(st);
449
- print.ok(`已删除「${target.title}」`);
487
+ print.ok(`Deleted "${target.title}"`);
450
488
  return;
451
489
  }
452
490
  case '/rename': {
453
491
  if (!st.chat) {
454
- print.warn('当前没有会话');
492
+ print.warn('No active conversation');
455
493
  return;
456
494
  }
457
495
  if (!rest) {
458
- print.fail('用法:/rename <新标题>');
496
+ print.fail('Usage: /rename <title>');
459
497
  return;
460
498
  }
461
499
  st.chat.title = rest;
462
500
  persist(st);
463
- print.ok(`已重命名为「${rest}」`);
501
+ print.ok(`Renamed to "${rest}"`);
464
502
  return;
465
503
  }
466
504
  case '/compress':
@@ -512,13 +550,13 @@ async function handleSlash(st, editor, input) {
512
550
  st.exitRequested = true;
513
551
  return;
514
552
  default:
515
- print.warn(`命令尚未实现:${command.name}`);
553
+ print.warn(`Not implemented yet: ${command.name}`);
516
554
  }
517
555
  }
518
556
 
519
557
  function showHelp() {
520
558
  print.blank();
521
- print.heading('斜杠命令');
559
+ print.heading(t('menu_title'));
522
560
  // 命令名列按显示宽度对齐(中文别名占两格,不能用 padEnd)
523
561
  const nameWidth = Math.max(...SLASH_COMMANDS.map((cmd) => displayWidth(cmd.name))) + 2;
524
562
  for (const cmd of SLASH_COMMANDS) {
@@ -527,20 +565,20 @@ function showHelp() {
527
565
  if (cmd.usage !== cmd.name) print.write(` ${' '.repeat(nameWidth)}${COLORS.faint(cmd.usage)}`);
528
566
  }
529
567
  print.blank();
530
- print.heading('输入约定');
568
+ print.heading('Keys');
531
569
  const tips = [
532
- ['! 命令', '执行终端命令,输出进入对话上下文(如 !git status)'],
533
- ['\\ 结尾', '续行输入多行消息'],
534
- ['Tab', '在 / 命令后循环选择补全项'],
535
- ['Ctrl+C', '生成中中断;空闲时退出;输入框有内容时清空'],
536
- ['Ctrl+D', '退出'],
537
- ['↑ / ↓', '浏览历史输入']
570
+ ['! cmd', 'Run a shell command; output joins the context (e.g. !git status)'],
571
+ ['\\', 'Continue on the next line (multi-line input)'],
572
+ ['Tab', 'Open / cycle the completion menu'],
573
+ ['Ctrl+C', 'Interrupt generation · exit when idle · clear input'],
574
+ ['Ctrl+D', 'Exit'],
575
+ ['↑ / ↓', 'Recalls the previous prompt']
538
576
  ];
539
577
  for (const [key, desc] of tips) {
540
578
  print.write(` ${COLORS.cyan(key)} ${desc}`);
541
579
  }
542
580
  print.blank();
543
- print.hint(`其他操作也可以直接用子命令:${BIN} provider list / ${BIN} mcp start ...`);
581
+ print.hint(`Everything is also available as a subcommand: ${BIN} provider list · ${BIN} mcp start`);
544
582
  print.blank();
545
583
  }
546
584
 
@@ -548,10 +586,10 @@ function listChats(st) {
548
586
  const chats = sessions.listChats();
549
587
  print.blank();
550
588
  if (!chats.length) {
551
- print.hint('还没有历史对话');
589
+ print.hint(t('no_chats'));
552
590
  return;
553
591
  }
554
- print.heading(`历史对话(${chats.length})`);
592
+ print.heading(`Conversations (${chats.length})`);
555
593
  const rows = chats.map((chat, index) => {
556
594
  const messages = chat.messages.filter((m) => m.role !== 'terminal');
557
595
  const last = messages[messages.length - 1];
@@ -562,10 +600,10 @@ function listChats(st) {
562
600
  return [
563
601
  chat.id === st.chat?.id ? COLORS.accent('*') : ' ',
564
602
  String(index + 1),
565
- chat.title || '新对话',
603
+ chat.title || 'New conversation',
566
604
  String(messages.length),
567
605
  stamp,
568
- chat.mode === 'work' ? '工作' : '对话',
606
+ chat.mode === 'work' ? t('mode_work') : t('mode_chat'),
569
607
  last?.content ? last.content.slice(0, 40) : ''
570
608
  ];
571
609
  });
@@ -573,16 +611,16 @@ function listChats(st) {
573
611
  [
574
612
  { title: '', max: 1 },
575
613
  { title: '#', max: 3 },
576
- { title: '标题', max: 30 },
577
- { title: '消息', max: 4 },
578
- { title: '更新时间', max: 12 },
579
- { title: '模式', max: 4 },
580
- { title: '最后内容', max: 40 }
614
+ { title: 'Title', max: 30 },
615
+ { title: 'Msgs', max: 4 },
616
+ { title: 'Updated', max: 12 },
617
+ { title: 'Mode', max: 4 },
618
+ { title: 'Last message', max: 40 }
581
619
  ],
582
620
  rows
583
621
  );
584
622
  print.blank();
585
- print.hint('/resume <序号> 恢复 · /delete <序号> 删除');
623
+ print.hint('/resume <n> restore · /delete <n> remove');
586
624
  }
587
625
 
588
626
  function printThread(st) {
@@ -590,7 +628,7 @@ function printThread(st) {
590
628
  if (!chat?.messages?.length) return;
591
629
  for (const message of chat.messages) {
592
630
  if (message.role === 'user') {
593
- process.stdout.write(`\n${COLORS.faint('› 你')}\n${message.content}\n`);
631
+ process.stdout.write(`\n${COLORS.faint('› You')}\n${message.content}\n`);
594
632
  } else if (message.role === 'assistant') {
595
633
  process.stdout.write(`\n${COLORS.accent('•')} ${COLORS.bold('DgxayCode')}\n`);
596
634
  if (message.content) process.stdout.write(require('./ansi').renderMarkdown(message.content, { width: termWidth() }));
@@ -606,23 +644,23 @@ function printThread(st) {
606
644
 
607
645
  async function compress(st, editor) {
608
646
  if (st.streaming) {
609
- print.warn('请等待当前回复结束');
647
+ print.warn(t('wait_current'));
610
648
  return;
611
649
  }
612
650
  const chat = st.chat;
613
651
  if (!chat?.messages?.length) {
614
- print.warn('当前对话还没有可压缩的内容');
652
+ print.warn(t('compress_nothing'));
615
653
  return;
616
654
  }
617
655
  const config = settings.readConfig();
618
656
  const model = settings.activeModel(config);
619
657
  if (!model) {
620
- print.warn('请先配置模型');
658
+ print.warn(t('no_model'));
621
659
  return;
622
660
  }
623
661
  const before = sessions.estimateChatTokens(chat);
624
662
  editor.suspend();
625
- process.stdout.write(`${COLORS.faint(`${SIGN.run} 正在压缩上下文…`)}`);
663
+ process.stdout.write(`${COLORS.faint(`${SIGN.run} Compressing context…`)}`);
626
664
  try {
627
665
  const result = await compressMessages({
628
666
  config,
@@ -630,19 +668,19 @@ async function compress(st, editor) {
630
668
  modelId: st.runtime.modelId || config.activeModelId,
631
669
  messages: sessions.buildHistory(chat)
632
670
  });
633
- if (!result.summary) throw new Error('模型没有返回摘要');
671
+ if (!result.summary) throw new Error('the model returned no summary');
634
672
  chat.messages = [
635
- { role: 'assistant', content: `> 上下文已压缩,仅保留重要信息。\n\n${result.summary}`, ts: Date.now(), compressed: true }
673
+ { role: 'assistant', content: `> Context compressed; only key information kept.\n\n${result.summary}`, ts: Date.now(), compressed: true }
636
674
  ];
637
675
  chat.updatedAt = Date.now();
638
676
  persist(st);
639
677
  const after = sessions.estimateChatTokens(chat);
640
678
  process.stdout.write('\r\x1b[2K');
641
- print.ok(`上下文已压缩:${sessions.formatTokens(before)} → ${sessions.formatTokens(after)} tokens(估算)`);
679
+ print.ok(`Context compressed: ${sessions.formatTokens(before)} → ${sessions.formatTokens(after)} tokens (estimated)`);
642
680
  process.stdout.write(`\n${require('./ansi').renderMarkdown(result.summary, { width: termWidth() })}\n`);
643
681
  } catch (err) {
644
682
  process.stdout.write('\r\x1b[2K');
645
- print.fail(err.message || '压缩失败');
683
+ print.fail(err.message || t('compress_failed'));
646
684
  } finally {
647
685
  editor.resume();
648
686
  }
@@ -651,30 +689,30 @@ async function compress(st, editor) {
651
689
  async function handleSkillCommand(st, rest) {
652
690
  if (!rest) {
653
691
  print.blank();
654
- print.heading(`技能(${settings.homeRelative(cfg.SKILLS_DIR)})`);
692
+ print.heading(`Skills (${settings.homeRelative(cfg.SKILLS_DIR)})`);
655
693
  const list = skills.listSkills();
656
694
  if (!list.length) {
657
- print.hint(' 还没有技能。在 ~/.dcb/skills 下放一个 name.md 或 <目录>/SKILL.md');
695
+ print.hint(' No skills yet. Put a name.md or <dir>/SKILL.md into ~/.dcb/skills');
658
696
  print.blank();
659
697
  return;
660
698
  }
661
699
  const rows = list.map((skill, index) => [
662
700
  String(index + 1),
663
701
  skill.name,
664
- st.runtime.skill === skill.name ? COLORS.green('已启用') : '',
702
+ st.runtime.skill === skill.name ? COLORS.green('enabled') : '',
665
703
  skill.description
666
704
  ]);
667
705
  print.table(
668
706
  [
669
707
  { title: '#', max: 3 },
670
- { title: '名称', max: 22 },
671
- { title: '状态', max: 8 },
672
- { title: '描述', max: 60 }
708
+ { title: 'Name', max: 22 },
709
+ { title: 'Status', max: 8 },
710
+ { title: 'Description', max: 60 }
673
711
  ],
674
712
  rows
675
713
  );
676
714
  print.blank();
677
- print.hint('/skill off 关闭 · /skill <名称> [提示词] 启用(本次对话)');
715
+ print.hint('/skill off to disable · /skill <name> [prompt] to enable for this conversation');
678
716
  print.blank();
679
717
  return;
680
718
  }
@@ -682,7 +720,7 @@ async function handleSkillCommand(st, rest) {
682
720
  st.runtime.skill = '';
683
721
  if (st.chat) st.chat.skill = '';
684
722
  updateBusState(st);
685
- print.ok('已关闭技能');
723
+ print.ok(t('skills_off'));
686
724
  return;
687
725
  }
688
726
  const list = skills.listSkills();
@@ -690,13 +728,13 @@ async function handleSkillCommand(st, rest) {
690
728
  .filter((item) => rest === item.name || rest.startsWith(`${item.name} `))
691
729
  .sort((a, b) => b.name.length - a.name.length)[0];
692
730
  if (!picked) {
693
- print.warn(`未找到技能:${rest}。输入 /skill 查看列表`);
731
+ print.warn(`Skill not found: ${rest}. Type /skill to list them`);
694
732
  return;
695
733
  }
696
734
  st.runtime.skill = picked.name;
697
735
  if (st.chat) st.chat.skill = picked.name;
698
736
  updateBusState(st);
699
- print.ok(`已在本次对话启用技能:${picked.name}`);
737
+ print.ok(`Skill enabled for this conversation: ${picked.name}`);
700
738
  const prompt = rest.slice(picked.name.length).trim();
701
739
  if (prompt) return prompt;
702
740
  return undefined;
@@ -707,13 +745,13 @@ function handleModelCommand(st, rest) {
707
745
  const provider = settings.findProvider(st.runtime.providerId) || settings.activeProvider(config);
708
746
  const models = provider?.models || [];
709
747
  if (!models.length) {
710
- print.warn('当前供应商没有模型,先运行 dgxay provider add');
748
+ print.warn('This provider has no models. Run dgxay provider add');
711
749
  return;
712
750
  }
713
751
  const current = models.find((m) => m.id === st.runtime.modelId) || models[0];
714
752
  if (!rest) {
715
753
  print.blank();
716
- print.heading(`模型(${provider.name})`);
754
+ print.heading(`Models (${provider.name})`);
717
755
  models.forEach((model, index) => {
718
756
  const active = model.id === current?.id;
719
757
  print.write(
@@ -725,7 +763,7 @@ function handleModelCommand(st, rest) {
725
763
  );
726
764
  });
727
765
  print.blank();
728
- print.hint('/model <序号|ID> 切换 · 该选择会写入配置');
766
+ print.hint('/model <n|id> to switch · saved to config');
729
767
  print.blank();
730
768
  return;
731
769
  }
@@ -735,39 +773,39 @@ function handleModelCommand(st, rest) {
735
773
  models.find((m) => m.id === rest) ||
736
774
  models.find((m) => m.id.includes(rest));
737
775
  if (!target) {
738
- print.fail(`找不到模型:${rest}`);
776
+ print.fail(`Model not found: ${rest}`);
739
777
  return;
740
778
  }
741
779
  settings.setActiveModel(provider.id, target.id);
742
780
  st.runtime.modelId = target.id;
743
781
  st.runtime.providerId = provider.id;
744
782
  updateBusState(st);
745
- print.ok(`已切换到 ${target.name || target.id}`);
783
+ print.ok(`Switched to ${target.name || target.id}`);
746
784
  }
747
785
 
748
786
  function handleProviderCommand(st, rest) {
749
787
  const config = settings.readConfig();
750
788
  const providers = config.providers || [];
751
789
  if (!providers.length) {
752
- print.warn('还没有供应商,运行 dgxay config init');
790
+ print.warn('No providers yet. Run dgxay config init');
753
791
  return;
754
792
  }
755
793
  const current = settings.findProvider(st.runtime.providerId) || settings.activeProvider(config);
756
794
  if (!rest) {
757
795
  print.blank();
758
- print.heading('供应商');
796
+ print.heading(t('provider_list_title'));
759
797
  providers.forEach((provider, index) => {
760
798
  const active = provider.id === current?.id;
761
799
  print.write(
762
800
  ` ${active ? COLORS.accent('❯') : ' '} ${COLORS.faint(String(index + 1).padStart(2))}. ${
763
801
  active ? COLORS.accent(provider.name) : provider.name
764
802
  } ${COLORS.faint(settings.PROTOCOL_META[provider.protocol || 'openai'].short)} ${COLORS.faint(
765
- `${(provider.models || []).length} 个模型`
803
+ `${(provider.models || []).length} models`
766
804
  )}`
767
805
  );
768
806
  });
769
807
  print.blank();
770
- print.hint('/provider <序号|名称> 切换');
808
+ print.hint('/provider <n|name> to switch');
771
809
  print.blank();
772
810
  return;
773
811
  }
@@ -776,62 +814,62 @@ function handleProviderCommand(st, rest) {
776
814
  (Number.isInteger(index) && index >= 1 && index <= providers.length ? providers[index - 1] : null) ||
777
815
  settings.findProvider(rest);
778
816
  if (!target) {
779
- print.fail(`找不到供应商:${rest}`);
817
+ print.fail(`Provider not found: ${rest}`);
780
818
  return;
781
819
  }
782
820
  const model = target.models?.[0];
783
821
  if (!model) {
784
- print.fail(`供应商 ${target.name} 没有模型`);
822
+ print.fail(`Provider ${target.name} has no models`);
785
823
  return;
786
824
  }
787
825
  settings.saveConfig({ activeProviderId: target.id, activeModelId: model.id });
788
826
  st.runtime.providerId = target.id;
789
827
  st.runtime.modelId = model.id;
790
828
  updateBusState(st);
791
- print.ok(`已切换到 ${target.name} / ${model.name || model.id}`);
829
+ print.ok(`Switched to ${target.name} / ${model.name || model.id}`);
792
830
  }
793
831
 
794
832
  function handleModeCommand(st, rest) {
795
- const next = rest ? (rest === 'work' || rest === '工作' ? 'work' : 'chat') : st.runtime.mode === 'work' ? 'chat' : 'work';
833
+ const next = rest ? (rest === 'work' ? 'work' : 'chat') : st.runtime.mode === 'work' ? 'chat' : 'work';
796
834
  st.runtime.mode = next;
797
835
  if (st.chat) st.chat.mode = next;
798
836
  updateBusState(st);
799
- print.ok(next === 'work' ? '已切换到工作模式' : '已切换到对话模式');
800
- if (next === 'work' && !st.cwd) print.hint('当前没有工作空间,请用 /cd <目录> 选择');
837
+ print.ok(next === 'work' ? `${t('mode_set')}: ${t('mode_work')}` : `${t('mode_set')}: ${t('mode_chat')}`);
838
+ if (next === 'work' && !st.cwd) print.hint('No workspace selected. Use /cd <dir>');
801
839
  }
802
840
 
803
841
  function handlePermissionCommand(st, rest) {
804
842
  const config = settings.readConfig();
805
843
  if (!rest) {
806
844
  print.blank();
807
- print.heading('权限');
845
+ print.heading(t('perms_title'));
808
846
  const available = settings.availablePermissions(config);
809
847
  Object.entries(settings.PERMISSIONS).forEach(([key, meta]) => {
810
848
  const usable = available.includes(key);
811
849
  const active = st.runtime.permission === key;
812
850
  print.write(
813
851
  ` ${active ? COLORS.accent('❯') : ' '} ${COLORS.faint(key.padEnd(8))} ${
814
- usable ? meta.label : COLORS.faint(`${meta.label}(不可用)`)
852
+ usable ? meta.label : COLORS.faint(`${meta.label} (unavailable)`)
815
853
  } ${COLORS.faint(meta.desc)}`
816
854
  );
817
855
  });
818
- if (!available.includes('auto')) print.hint('自动审查需要供应商配置有效的审查模型');
856
+ if (!available.includes('auto')) print.hint('Auto review needs a valid review model');
819
857
  print.blank();
820
858
  return;
821
859
  }
822
860
  if (!settings.PERMISSIONS[rest]) {
823
- print.fail('权限只能是 none / normal / auto / full');
861
+ print.fail('Level must be none / normal / auto / full');
824
862
  return;
825
863
  }
826
864
  if (rest === 'auto' && !settings.hasReviewModel(config)) {
827
- print.fail('自动审查不可用:当前或默认供应商没有配置有效的审查模型');
865
+ print.fail(t('perm_auto_unavailable'));
828
866
  return;
829
867
  }
830
868
  st.runtime.permission = rest;
831
869
  settings.setPermission(rest);
832
870
  updateBusState(st);
833
- print.ok(`权限已切换为:${settings.PERMISSIONS[rest].label}`);
834
- if (rest === 'full') print.warn('完全权限下 AI 可以直接执行任意命令、读写任意文件,请确认这是你想要的');
871
+ print.ok(`Permission set to: ${settings.PERMISSIONS[rest].label}`);
872
+ if (rest === 'full') print.warn(t('perm_full_warning'));
835
873
  }
836
874
 
837
875
  function handleCd(st, rest) {
@@ -844,17 +882,17 @@ function handleCd(st, rest) {
844
882
  try {
845
883
  stat = fs.statSync(target);
846
884
  } catch {
847
- print.fail(`目录不存在:${target}`);
885
+ print.fail(`Directory not found: ${target}`);
848
886
  return;
849
887
  }
850
888
  if (!stat.isDirectory()) {
851
- print.fail(`不是文件夹:${target}`);
889
+ print.fail(`Not a directory: ${target}`);
852
890
  return;
853
891
  }
854
892
  st.cwd = target;
855
893
  settings.setWorkspace(target);
856
894
  updateBusState(st);
857
- print.ok(`工作空间:${target}`);
895
+ print.ok(`Workspace: ${target}`);
858
896
  }
859
897
 
860
898
  function handleLs(st, rest) {
@@ -863,7 +901,7 @@ function handleLs(st, rest) {
863
901
  try {
864
902
  entries = fs.readdirSync(target, { withFileTypes: true });
865
903
  } catch (err) {
866
- print.fail(`无法读取 ${target}:${err.code || err.message}`);
904
+ print.fail(`Cannot read ${target}:${err.code || err.message}`);
867
905
  return;
868
906
  }
869
907
  entries.sort((a, b) => Number(b.isDirectory()) - Number(a.isDirectory()) || a.name.localeCompare(b.name));
@@ -891,7 +929,7 @@ async function handleMcpCommand(st, rest) {
891
929
  const list = mcp.listServers();
892
930
  print.blank();
893
931
  if (!list.length) {
894
- print.hint(`还没有 MCP server,用 /mcp add 或 ${BIN} mcp add 添加`);
932
+ print.hint(`No MCP servers. Use /mcp add or ${BIN} mcp add`);
895
933
  print.blank();
896
934
  return;
897
935
  }
@@ -899,46 +937,46 @@ async function handleMcpCommand(st, rest) {
899
937
  const rows = list.map((server, index) => [
900
938
  String(index + 1),
901
939
  server.name,
902
- server.status === 'running' ? COLORS.green('运行中') : server.status === 'error' ? COLORS.red('异常') : '已停止',
940
+ server.status === 'running' ? COLORS.green('running') : server.status === 'error' ? COLORS.red('error') : 'stopped',
903
941
  server.pid ? String(server.pid) : '',
904
942
  `${server.command} ${(server.args || []).join(' ')}`
905
943
  ]);
906
944
  print.table(
907
945
  [
908
946
  { title: '#', max: 3 },
909
- { title: '名称', max: 18 },
910
- { title: '状态', max: 8 },
947
+ { title: 'Name', max: 18 },
948
+ { title: 'Status', max: 8 },
911
949
  { title: 'PID', max: 8 },
912
- { title: '命令', max: 50 }
950
+ { title: 'Command', max: 50 }
913
951
  ],
914
952
  rows
915
953
  );
916
954
  print.blank();
917
- print.hint('/mcp start <名称> · /mcp stop <名称> · /mcp logs <名称> · /mcp add name command args');
955
+ print.hint('/mcp start <name> · /mcp stop <name> · /mcp logs <name> · /mcp add name command args');
918
956
  print.blank();
919
957
  return;
920
958
  }
921
959
  const server = mcp.findServer(arg);
922
960
  if (action === 'start') {
923
- if (!server) return print.fail(`找不到 MCP:${arg}`);
961
+ if (!server) return print.fail(`MCP not found: ${arg}`);
924
962
  try {
925
963
  const result = mcp.startServer(server.id);
926
- print.ok(`已启动 ${server.name}(pid ${result.pid})`);
964
+ print.ok(`Started ${server.name}(pid ${result.pid})`);
927
965
  } catch (err) {
928
966
  print.fail(err.message);
929
967
  }
930
968
  return;
931
969
  }
932
970
  if (action === 'stop') {
933
- if (!server) return print.fail(`找不到 MCP:${arg}`);
971
+ if (!server) return print.fail(`MCP not found: ${arg}`);
934
972
  mcp.stopServer(server.id);
935
- print.ok(`已停止 ${server.name}`);
973
+ print.ok(`Stopped ${server.name}`);
936
974
  return;
937
975
  }
938
976
  if (action === 'logs') {
939
- if (!server) return print.fail(`找不到 MCP:${arg}`);
977
+ if (!server) return print.fail(`MCP not found: ${arg}`);
940
978
  const logs = mcp.statusOf(server.id).logs || [];
941
- if (!logs.length) return print.hint('暂无日志');
979
+ if (!logs.length) return print.hint('No logs yet');
942
980
  for (const entry of logs) {
943
981
  print.write(`${entry.stream === 'stderr' ? COLORS.red(entry.text) : entry.text}`);
944
982
  }
@@ -946,33 +984,33 @@ async function handleMcpCommand(st, rest) {
946
984
  }
947
985
  if (action === 'add') {
948
986
  const [name, command, ...args] = others;
949
- if (!name || !command) return print.fail('用法:/mcp add <名称> <命令> [参数…]');
987
+ if (!name || !command) return print.fail('Usage: /mcp add <name> <command> [args...]');
950
988
  const created = mcp.addServer({ name, command, args });
951
- print.ok(`已添加 MCP「${created.name}」,用 /mcp start ${created.name} 启动`);
989
+ print.ok(`Added MCP "${created.name}" — start it with /mcp start ${created.name}`);
952
990
  return;
953
991
  }
954
992
  if (action === 'rm' || action === 'remove') {
955
- if (!server) return print.fail(`找不到 MCP:${arg}`);
993
+ if (!server) return print.fail(`MCP not found: ${arg}`);
956
994
  mcp.removeServer(server.id);
957
- print.ok(`已删除 ${server.name}`);
995
+ print.ok(`Deleted ${server.name}`);
958
996
  return;
959
997
  }
960
- print.fail(`未知的 /mcp 子命令:${action}`);
998
+ print.fail(`Unknown /mcp action: ${action}`);
961
999
  }
962
1000
 
963
1001
  function handleTheme(rest) {
964
1002
  const mode = String(rest || '').toLowerCase();
965
1003
  if (mode === 'on' || mode === 'color') {
966
1004
  setColorMode('ansi256');
967
- print.ok('已开启彩色输出');
1005
+ print.ok('Color output on');
968
1006
  return;
969
1007
  }
970
1008
  if (mode === 'off' || mode === 'plain') {
971
1009
  setColorMode('none');
972
- print.ok('已关闭彩色输出');
1010
+ print.ok('Color output off');
973
1011
  return;
974
1012
  }
975
- print.write(`彩色输出:${colorEnabled() ? '开启' : '关闭'}(/theme on | off)`);
1013
+ print.write(`Color output: ${colorEnabled() ? 'on' : 'off'} (/theme on | off)`);
976
1014
  }
977
1015
 
978
1016
  /* ------------------------------ 输入分发 ------------------------------ */
@@ -1022,7 +1060,7 @@ async function singleShot(options) {
1022
1060
  if (options.output) argv.push('--output', options.output);
1023
1061
  if (options.yes) argv.push('--yes');
1024
1062
  if (!prompt) {
1025
- print.fail('单次运行需要提示词');
1063
+ print.fail('A prompt is required');
1026
1064
  return 2;
1027
1065
  }
1028
1066
  return require('../commands/run').run({ argv, runtime: options });
@@ -1039,9 +1077,9 @@ async function startInteractive(options) {
1039
1077
  const config = settings.readConfig();
1040
1078
  if (!config.providers?.length) {
1041
1079
  // 还没有任何供应商:交互终端里直接进入配置引导
1042
- print.warn('还没有配置任何供应商');
1080
+ print.warn('No provider configured yet');
1043
1081
  if (!process.stdin.isTTY) {
1044
- print.hint('运行 dgxay config init 完成首次配置');
1082
+ print.hint('Run dgxay config init to set one up');
1045
1083
  return 1;
1046
1084
  }
1047
1085
  const result = await require('../commands/config').run({
@@ -1052,12 +1090,29 @@ async function startInteractive(options) {
1052
1090
  }
1053
1091
 
1054
1092
  const st = createRuntime(options);
1055
- const editor = process.stdin.isTTY && process.stdout.isTTY
1093
+ const interactive = process.stdin.isTTY && process.stdout.isTTY;
1094
+
1095
+ // 启动时先清屏,再渲染界面(管道模式不清屏,保持输出干净)
1096
+ if (interactive) {
1097
+ clearScreen(process.stdout);
1098
+ process.stdout.write('\x1b[3J');
1099
+ }
1100
+
1101
+ // 历史:优先把当前会话里用户说过的话填进去,↑ 可以直接回填
1102
+ const previousPrompts = (st.chat?.messages || [])
1103
+ .filter((message) => message.role === 'user' && typeof message.content === 'string' && message.content.trim())
1104
+ .map((message) => message.content.trim());
1105
+ const lastUserPrompt = previousPrompts[previousPrompts.length - 1] || '';
1106
+
1107
+ const editor = interactive
1056
1108
  ? new LineEditor({
1057
1109
  prompt: promptFor(st),
1058
- continuationPrompt: COLORS.faint(' … '),
1110
+ placeholder: t('box_placeholder'),
1059
1111
  completer,
1060
- multiline: true
1112
+ multiline: true,
1113
+ capabilities: detectCapabilities(process.stdout),
1114
+ history: previousPrompts,
1115
+ lastUserPrompt
1061
1116
  })
1062
1117
  : createLineReader(process.stdin);
1063
1118
 
@@ -1113,7 +1168,7 @@ async function startInteractive(options) {
1113
1168
  if (st.streaming && !st.abortRequested) {
1114
1169
  st.abortRequested = true;
1115
1170
  st.requestController?.abort();
1116
- process.stdout.write(`${COLORS.yellow(`${SIGN.bullet} 正在中断…`)}\n`);
1171
+ process.stdout.write(`${COLORS.yellow(`${SIGN.bullet} ${t('interrupted')}`)}\n`);
1117
1172
  return true;
1118
1173
  }
1119
1174
  if (st.terminalBusy) return true;
@@ -1122,7 +1177,16 @@ async function startInteractive(options) {
1122
1177
 
1123
1178
  try {
1124
1179
  while (!st.exitRequested) {
1125
- editor.prompt = promptFor(st);
1180
+ // 输入框上方的一行说明:模型 / 技能
1181
+ if (interactive) editor.prompt = promptFor(st);
1182
+ // ↑ 回填:始终指向最近一条用户提示词
1183
+ if (interactive) {
1184
+ const prompts = (st.chat?.messages || []).filter(
1185
+ (message) => message.role === 'user' && typeof message.content === 'string' && message.content.trim()
1186
+ );
1187
+ editor.lastUserPrompt = prompts.length ? prompts[prompts.length - 1].content.trim() : '';
1188
+ editor.history = prompts.map((message) => message.content.trim());
1189
+ }
1126
1190
  const answer = await editor.question();
1127
1191
  if (answer.type === 'eof' || answer.type === 'cancel') break;
1128
1192
  try {
@@ -1150,7 +1214,7 @@ async function startInteractive(options) {
1150
1214
  if (typeof process.stdin.pause === 'function') process.stdin.pause();
1151
1215
  }
1152
1216
 
1153
- process.stdout.write(`${COLORS.faint('再见。用 dgxay -c 继续上一次对话。')}\n`);
1217
+ process.stdout.write(`${COLORS.faint(t('bye'))}\n`);
1154
1218
  return 0;
1155
1219
  }
1156
1220