pi-web-ui 0.62.0 → 0.63.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.
Files changed (56) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +504 -497
  3. package/README.zh-CN.md +427 -422
  4. package/bin/pi-web-ui.mjs +1809 -1881
  5. package/deploy/com.xingshuyin.pi-web-ui.plist +48 -48
  6. package/deploy/nginx-subpath.conf +88 -88
  7. package/deploy/pi-web-ui-task.xml +54 -54
  8. package/deploy/pi-web-ui.service +31 -31
  9. package/dist/server/agent-service.js +318 -88
  10. package/dist/server/attachments.js +37 -28
  11. package/dist/server/bg-servers.js +6 -1
  12. package/dist/server/client-state.js +15 -7
  13. package/dist/server/control-socket.js +1 -5
  14. package/dist/server/dsh/dsh-agent-service.js +454 -118
  15. package/dist/server/dsh/dsh-client.js +1 -3
  16. package/dist/server/dsh/dsh-sessions.js +4 -14
  17. package/dist/server/dsh/runtime/cordis.yml +1 -1
  18. package/dist/server/dsh/runtime/goal-rpc.mjs +645 -662
  19. package/dist/server/dsh/runtime/launcher.mjs +8 -18
  20. package/dist/server/dsh/runtime/override.patch.yml +9 -9
  21. package/dist/server/dsh/runtime/runtime-root.mjs +7 -11
  22. package/dist/server/ensure-bash.js +2 -5
  23. package/dist/server/files-service.js +42 -36
  24. package/dist/server/goal-service.js +59 -30
  25. package/dist/server/index.js +17 -10
  26. package/dist/server/mcp-bridge.js +11 -5
  27. package/dist/server/model-admin.js +78 -72
  28. package/dist/server/plugin-facilities.js +2 -4
  29. package/dist/server/plugin-updater.js +1 -1
  30. package/dist/server/plugins.js +26 -8
  31. package/dist/server/process-utils.js +4 -4
  32. package/dist/server/protocol-version.js +1 -1
  33. package/dist/server/scm.js +27 -21
  34. package/dist/server/serialize.js +1 -3
  35. package/dist/server/settings-service.js +63 -21
  36. package/dist/server/slash-commands.js +29 -3
  37. package/dist/server/subagent-templates.js +249 -0
  38. package/dist/server/subagents.js +150 -0
  39. package/dist/server/terminals.js +101 -40
  40. package/dist/server/text-sniff.js +4 -24
  41. package/dist/server/update-check.js +2 -4
  42. package/dist/server/uploads.js +1 -3
  43. package/dist/server/vision-bridge.js +2 -7
  44. package/dist/server/wait-subscription-scan.js +102 -21
  45. package/dist/server/webui-context.js +1 -1
  46. package/extensions/webui.ts +190 -192
  47. package/package.json +109 -103
  48. package/web/dist/assets/TerminalPanel-B9vcI7-_.js +2 -0
  49. package/web/dist/assets/index-B8Wv29p5.css +10 -0
  50. package/web/dist/assets/index-CWUWD0rq.js +323 -0
  51. package/web/dist/favicon.svg +8 -8
  52. package/web/dist/index.html +15 -15
  53. package/web/public/favicon.svg +8 -8
  54. package/web/dist/assets/TerminalPanel-BkCWDsIG.js +0 -2
  55. package/web/dist/assets/index-BRVKed7W.js +0 -321
  56. package/web/dist/assets/index-ORTpR6ZQ.css +0 -10
@@ -32,15 +32,14 @@ import { BgServerTracker } from "../bg-servers.js";
32
32
  import { ClientStateStore } from "../client-state.js";
33
33
  import { FilesService, workspacePath } from "../files-service.js";
34
34
  import { QuiesceRejectedError } from "../agent-service.js";
35
- import { killPidTree } from "../process-utils.js";
36
35
  import { NATIVE_COMMANDS, parseSlash } from "../slash-commands.js";
37
36
  import { TerminalManager, loadCommands, saveCommandsFile } from "../terminals.js";
38
- import { saveUpload, uploadsRoot } from "../uploads.js";
39
- import { checkAll as checkAllUpdates, collectTargets, compareVersions as compareSemver } from "../update-check.js";
37
+ import { saveUpload } from "../uploads.js";
38
+ import { checkAll as checkAllUpdates, collectTargets } from "../update-check.js";
40
39
  import { previewKind } from "../text-sniff.js";
41
- import { DshRuntime, DshTransportError, loadDeepSeekKey } from "./dsh-client.js";
40
+ import { DshRuntime, loadDeepSeekKey } from "./dsh-client.js";
42
41
  import { DshStreamAccumulator, assistantMessageEventToUiMessage, toolResultEventToUiMessage, userMessageEventToUiMessage, } from "./dsh-serialize.js";
43
- import { firstUserText, findSessionFilesForCwd, projectKey, readSessionLog, replayEventsToMessages, } from "./dsh-sessions.js";
42
+ import { firstUserText, findSessionFilesForCwd, readSessionLog, replayEventsToMessages } from "./dsh-sessions.js";
44
43
  const SNAPSHOT_INTERVAL_MS = 60;
45
44
  const MAX_OPEN_CONVERSATIONS = 8;
46
45
  const DEFAULT_CONV_TITLE = "新对话";
@@ -67,7 +66,7 @@ export function dshSessionRoot(dataDir) {
67
66
  return join(dataDir, "dsh-sessions");
68
67
  }
69
68
  function estimateCost(t) {
70
- if ((t.input + t.output) === 0)
69
+ if (t.input + t.output === 0)
71
70
  return 0;
72
71
  return (((t.input + (t.cacheWrite ?? 0)) * DSH_PRICE_INPUT +
73
72
  (t.cacheRead ?? 0) * DSH_PRICE_CACHE_READ +
@@ -250,6 +249,7 @@ export class DshClientSession {
250
249
  type: "notice",
251
250
  level: "error",
252
251
  text: `DSH 运行时启动失败:${err.message}。请检查 DeepSeek API key(~/.pi/agent/auth.json)与 dsh 依赖安装。`,
252
+ textEn: `DSH runtime failed to start: ${err.message}. Check the DeepSeek API key (~/.pi/agent/auth.json) and dsh dependencies.`,
253
253
  });
254
254
  });
255
255
  // P0-3 convs 内存回收定时器(unref:不阻止进程退出)。
@@ -303,13 +303,24 @@ export class DshClientSession {
303
303
  type: "notice",
304
304
  level: "error",
305
305
  text: "DSH 运行时反复崩溃,已停止自动重启。请检查 DeepSeek API key 与 dsh 依赖安装。",
306
+ textEn: "DSH runtime keeps crashing; auto-restart stopped. Check the DeepSeek API key and dsh dependencies.",
306
307
  });
307
308
  this.flushSnapshot();
308
309
  return;
309
310
  }
310
- this.emit({ type: "notice", level: "warning", text: "DSH 运行时意外退出,正在自动重启…" });
311
+ this.emit({
312
+ type: "notice",
313
+ level: "warning",
314
+ text: "DSH 运行时意外退出,正在自动重启…",
315
+ textEn: "DSH runtime exited unexpectedly; restarting…",
316
+ });
311
317
  void this.startWithRetry().catch((err) => {
312
- this.emit({ type: "notice", level: "error", text: `自动重启失败:${err.message}` });
318
+ this.emit({
319
+ type: "notice",
320
+ level: "error",
321
+ text: `自动重启失败:${err.message}`,
322
+ textEn: `Auto-restart failed: ${err.message}`,
323
+ });
313
324
  });
314
325
  this.flushSnapshot();
315
326
  }
@@ -327,9 +338,7 @@ export class DshClientSession {
327
338
  if (conv.terminals.list().length > 0)
328
339
  continue;
329
340
  const idle = now - conv.lastEventAt;
330
- const limit = conv.listed
331
- ? DshClientSession.CONV_RECLAIM_LISTED_IDLE_MS
332
- : DshClientSession.CONV_RECLAIM_IDLE_MS;
341
+ const limit = conv.listed ? DshClientSession.CONV_RECLAIM_LISTED_IDLE_MS : DshClientSession.CONV_RECLAIM_IDLE_MS;
333
342
  if (idle > limit) {
334
343
  this.removeConversation(id);
335
344
  changed = true;
@@ -435,10 +444,21 @@ export class DshClientSession {
435
444
  questions: (params0.questions ?? []).map((q) => ({
436
445
  id: String(q.id ?? ""),
437
446
  question: String(q.question ?? ""),
438
- ...(typeof q.detail === "string" ? { detail: q.detail } : {}),
439
- ...(typeof q.header === "string" ? { header: q.header } : {}),
440
- ...(Array.isArray(q.options) ? { options: q.options.map((o) => ({ label: String(o.label ?? ""), ...(typeof o.description === "string" ? { description: o.description } : {}) })) } : {}),
441
- ...q.multiSelect ? { multiSelect: true } : {},
447
+ ...(typeof q.detail === "string"
448
+ ? { detail: q.detail }
449
+ : {}),
450
+ ...(typeof q.header === "string"
451
+ ? { header: q.header }
452
+ : {}),
453
+ ...(Array.isArray(q.options)
454
+ ? {
455
+ options: q.options.map((o) => ({
456
+ label: String(o.label ?? ""),
457
+ ...(typeof o.description === "string" ? { description: o.description } : {}),
458
+ })),
459
+ }
460
+ : {}),
461
+ ...(q.multiSelect ? { multiSelect: true } : {}),
442
462
  })),
443
463
  });
444
464
  }
@@ -458,7 +478,12 @@ export class DshClientSession {
458
478
  await this.runtime.answerQuestion(id, answers, cancelled);
459
479
  }
460
480
  catch (err) {
461
- this.emit({ type: "notice", level: "error", text: `回答失败:${err.message}` });
481
+ this.emit({
482
+ type: "notice",
483
+ level: "error",
484
+ text: `回答失败:${err.message}`,
485
+ textEn: `Answer failed: ${err.message}`,
486
+ });
462
487
  }
463
488
  }
464
489
  // -----------------------------------------------------------------------
@@ -468,12 +493,17 @@ export class DshClientSession {
468
493
  /** 桥接的插件工具最小形状(对齐 plugins.ts 的 PluginAgentTool,仅取桥接所需字段)。 */
469
494
  bridgedTool(t) {
470
495
  const tool = t;
471
- if (!tool || typeof tool.name !== "string" || typeof tool.description !== "string" || typeof tool.execute !== "function")
496
+ if (!tool ||
497
+ typeof tool.name !== "string" ||
498
+ typeof tool.description !== "string" ||
499
+ typeof tool.execute !== "function")
472
500
  return undefined;
473
501
  return {
474
502
  name: tool.name,
475
503
  description: tool.description,
476
- ...(tool.parameters && typeof tool.parameters === "object" ? { parameters: tool.parameters } : {}),
504
+ ...(tool.parameters && typeof tool.parameters === "object"
505
+ ? { parameters: tool.parameters }
506
+ : {}),
477
507
  execute: tool.execute,
478
508
  };
479
509
  }
@@ -654,9 +684,11 @@ export class DshClientSession {
654
684
  conv.goal.reviewing = true;
655
685
  const max = conv.goal.maxRounds || conv.dsGoal?.maxGoalRounds || 0;
656
686
  conv.goal.status =
687
+ max > 0 && round >= max ? `已达轮数上限(${round}/${max}),目标未完成` : `目标进行中(第 ${round} 轮)…`;
688
+ conv.goal.statusEn =
657
689
  max > 0 && round >= max
658
- ? `已达轮数上限(${round}/${max}),目标未完成`
659
- : `目标进行中(第 ${round} 轮)…`;
690
+ ? `Round cap reached (${round}/${max}), goal incomplete`
691
+ : `Goal in progress (round ${round})…`;
660
692
  this.emitGoalStatus();
661
693
  }
662
694
  break;
@@ -667,8 +699,7 @@ export class DshClientSession {
667
699
  // 重复文本(DSH 有时重放同一用户消息)→ 去重。
668
700
  const text = msg.content.map((c) => ("text" in c ? c.text : "")).join("");
669
701
  if (text &&
670
- conv.messages.some((m) => m.role === "user" &&
671
- m.content.map((c) => ("text" in c ? c.text : "")).join("") === text)) {
702
+ conv.messages.some((m) => m.role === "user" && m.content.map((c) => ("text" in c ? c.text : "")).join("") === text)) {
672
703
  break;
673
704
  }
674
705
  this.appendMessage(conv, msg);
@@ -682,8 +713,9 @@ export class DshClientSession {
682
713
  void this.hydrateImageBlocks(conv, msg, imgRefs);
683
714
  }
684
715
  if (conv.title === DEFAULT_CONV_TITLE) {
685
- const t = conv.messages.find((m) => m.role === "user")?.content
686
- ?.map((c) => ("text" in c ? c.text : ""))
716
+ const t = conv.messages
717
+ .find((m) => m.role === "user")
718
+ ?.content?.map((c) => ("text" in c ? c.text : ""))
687
719
  .join(" ")
688
720
  .trim();
689
721
  if (t)
@@ -748,8 +780,7 @@ export class DshClientSession {
748
780
  // message_delta 实时通道:本机快速完成时 60ms 延迟快照总被
749
781
  // assistant/message 抢跑(streaming 从未被捕捉)——delta 直接
750
782
  // 走独立通道,保证逐 token 渲染(前端 patch streamingMessage)。
751
- if (conv.id === this.conv.id &&
752
- (chunk.type === "text-delta" || chunk.type === "reasoning-delta")) {
783
+ if (conv.id === this.conv.id && (chunk.type === "text-delta" || chunk.type === "reasoning-delta")) {
753
784
  this.emit({
754
785
  type: "message_delta",
755
786
  conversationId: conv.id,
@@ -810,9 +841,7 @@ export class DshClientSession {
810
841
  case "agent/inbox/spliced": {
811
842
  // 用户 prompt 注入 → 清理 followUp 队列中已消费的文本。
812
843
  const inserted = ev.data?.inserted ?? [];
813
- const texts = inserted
814
- .map((m) => m.content?.find((c) => c.type === "text")?.text ?? "")
815
- .filter(Boolean);
844
+ const texts = inserted.map((m) => m.content?.find((c) => c.type === "text")?.text ?? "").filter(Boolean);
816
845
  if (texts.length > 0) {
817
846
  for (const t of texts) {
818
847
  const i = conv.queue.followUp.indexOf(t);
@@ -900,10 +929,7 @@ export class DshClientSession {
900
929
  const prev = this.emittedMessages;
901
930
  const rev = ++this.snapRev;
902
931
  // 增量:同一 conversation 且纯 append 时发 snapshot_delta。
903
- let incremental = !forceFull &&
904
- prev !== null &&
905
- this.emittedRev > 0 &&
906
- prev.length <= cur.length;
932
+ let incremental = !forceFull && prev !== null && this.emittedRev > 0 && prev.length <= cur.length;
907
933
  if (incremental && prev) {
908
934
  for (let i = 0; i < prev.length; i++) {
909
935
  if (prev[i] !== cur[i]) {
@@ -958,9 +984,7 @@ export class DshClientSession {
958
984
  sessionId: conv.sessionId,
959
985
  conversationId: this.activeId,
960
986
  rev,
961
- streamingMessage: conv.streaming
962
- ? conv.streaming.toUiMessage(conv.lastEventAt, this.model, "deepseek")
963
- : null,
987
+ streamingMessage: conv.streaming ? conv.streaming.toUiMessage(conv.lastEventAt, this.model, "deepseek") : null,
964
988
  isStreaming: conv.isStreaming,
965
989
  model: {
966
990
  id: this.model,
@@ -1001,6 +1025,7 @@ export class DshClientSession {
1001
1025
  emit(msg) {
1002
1026
  if (this.disposed)
1003
1027
  return;
1028
+ // eslint-disable-next-line unicorn/no-useless-spread -- snapshot: handlers may unsubscribe mid-emit
1004
1029
  for (const sink of [...this.sinks])
1005
1030
  sink(msg);
1006
1031
  }
@@ -1046,6 +1071,7 @@ export class DshClientSession {
1046
1071
  cwd: conv.cwd,
1047
1072
  messageCount: conv.messages.length,
1048
1073
  isStreaming: conv.isStreaming,
1074
+ isSubagent: false,
1049
1075
  });
1050
1076
  }
1051
1077
  this.emit({ type: "conversations", conversations: list, activeId: this.activeId });
@@ -1073,6 +1099,7 @@ export class DshClientSession {
1073
1099
  type: "notice",
1074
1100
  level: "warning",
1075
1101
  text: `当前项目运行的对话已达上限(${MAX_OPEN_CONVERSATIONS} 个)`,
1102
+ textEn: `This project already has the max open conversations (${MAX_OPEN_CONVERSATIONS}).`,
1076
1103
  });
1077
1104
  return;
1078
1105
  }
@@ -1109,6 +1136,7 @@ export class DshClientSession {
1109
1136
  type: "notice",
1110
1137
  level: "error",
1111
1138
  text: `切换工作区后重启运行时失败:${err.message}`,
1139
+ textEn: `Failed to restart the runtime after switching workspace: ${err.message}`,
1112
1140
  });
1113
1141
  });
1114
1142
  }
@@ -1180,6 +1208,7 @@ export class DshClientSession {
1180
1208
  type: "notice",
1181
1209
  level: "error",
1182
1210
  text: "服务器正在排空存量工作(quiesce),已拒绝新的对话/消息/编辑。存量运行会继续跑完;用 pi-web-ui server unquiesce 可恢复。",
1211
+ textEn: "Server is draining (quiesce); new chats/messages/edits rejected. Existing runs continue; resume with pi-web-ui server unquiesce.",
1183
1212
  });
1184
1213
  this.flushSnapshot();
1185
1214
  return true;
@@ -1213,7 +1242,7 @@ export class DshClientSession {
1213
1242
  /* 补图失败保持占位 */
1214
1243
  }
1215
1244
  }
1216
- async promptConv(conv, text, attachments, queue = false) {
1245
+ async promptConv(conv, text, attachments, _queue = false) {
1217
1246
  try {
1218
1247
  if (this.quiesceBlocked())
1219
1248
  return;
@@ -1227,9 +1256,7 @@ export class DshClientSession {
1227
1256
  content: [
1228
1257
  { type: "text", text },
1229
1258
  ...(Array.isArray(attachments)
1230
- ? attachments
1231
- .filter((a) => a.imageData)
1232
- .map((a) => ({ type: "image", dataUrl: a.imageData }))
1259
+ ? attachments.filter((a) => a.imageData).map((a) => ({ type: "image", dataUrl: a.imageData }))
1233
1260
  : []),
1234
1261
  ],
1235
1262
  timestamp: Date.now(),
@@ -1252,6 +1279,7 @@ export class DshClientSession {
1252
1279
  type: "notice",
1253
1280
  level: "error",
1254
1281
  text: `提示发送失败:${err.message}`,
1282
+ textEn: `Failed to send prompt: ${err.message}`,
1255
1283
  });
1256
1284
  }
1257
1285
  this.flushSnapshot();
@@ -1292,19 +1320,27 @@ export class DshClientSession {
1292
1320
  for (let i = conv.messages.length - 1; i >= 0; i--) {
1293
1321
  const m = conv.messages[i];
1294
1322
  if (m.role === "user") {
1295
- lastUser = m.content.map((c) => ("text" in c ? c.text : "")).join("").trim();
1323
+ lastUser = m.content
1324
+ .map((c) => ("text" in c ? c.text : ""))
1325
+ .join("")
1326
+ .trim();
1296
1327
  if (lastUser)
1297
1328
  break;
1298
1329
  }
1299
1330
  }
1300
1331
  const hist = this.histToContext(conv);
1301
- const fork = this.forkConversation(conv);
1332
+ this.forkConversation(conv);
1302
1333
  const text = lastUser
1303
1334
  ? hist.trim()
1304
1335
  ? `${lastUser}\n\n(以下为原对话上下文,仅作参考):\n${hist}`
1305
1336
  : lastUser
1306
1337
  : "请继续";
1307
- this.emit({ type: "notice", level: "info", text: "已自动重发(原会话不可续聊)" });
1338
+ this.emit({
1339
+ type: "notice",
1340
+ level: "info",
1341
+ text: "已自动重发(原会话不可续聊)",
1342
+ textEn: "Auto-resent (the original session cannot be resumed)",
1343
+ });
1308
1344
  void this.prompt(text);
1309
1345
  }
1310
1346
  /** 附件 → DSH contentBlocks(v1 简化:文本内联 / 路径引用 / 图片占位)。 */
@@ -1329,7 +1365,10 @@ export class DshClientSession {
1329
1365
  blocks.push({ type: "image", attachment: saved.ref });
1330
1366
  }
1331
1367
  catch (err) {
1332
- blocks.push({ type: "text", text: `\n[图片附件: ${a.name ?? "image"}(保存失败 ${err.message})]` });
1368
+ blocks.push({
1369
+ type: "text",
1370
+ text: `\n[图片附件: ${a.name ?? "image"}(保存失败 ${err.message})]`,
1371
+ });
1333
1372
  }
1334
1373
  }
1335
1374
  else if (a.fileData) {
@@ -1339,7 +1378,10 @@ export class DshClientSession {
1339
1378
  blocks.push({ type: "text", text: `\n[上传文件: ${saved.abs}]` });
1340
1379
  }
1341
1380
  catch (err) {
1342
- blocks.push({ type: "text", text: `\n[上传文件: ${a.name ?? "upload"}(落盘失败 ${err.message})]` });
1381
+ blocks.push({
1382
+ type: "text",
1383
+ text: `\n[上传文件: ${a.name ?? "upload"}(落盘失败 ${err.message})]`,
1384
+ });
1343
1385
  }
1344
1386
  }
1345
1387
  else if (resolved) {
@@ -1431,20 +1473,36 @@ export class DshClientSession {
1431
1473
  conv.goal.verdict = "pending";
1432
1474
  conv.goal.feedback = undefined;
1433
1475
  conv.goal.status = "已手动停止,目标已中止";
1476
+ conv.goal.statusEn = "Stopped manually, goal aborted";
1434
1477
  this.emitGoalStatus();
1435
1478
  }
1436
- this.emit({ type: "notice", level: "info", text: "已停止(DSH 中止 = 重启运行时,进行中的其他对话也会停止)" });
1479
+ this.emit({
1480
+ type: "notice",
1481
+ level: "info",
1482
+ text: "已停止(DSH 中止 = 重启运行时,进行中的其他对话也会停止)",
1483
+ textEn: "Stopped (DSH abort restarts the runtime; other running conversations stop too)",
1484
+ });
1437
1485
  try {
1438
1486
  await this.runtime.restart(this.model);
1439
1487
  }
1440
1488
  catch (err) {
1441
- this.emit({ type: "notice", level: "error", text: `中止后重启失败:${err.message}` });
1489
+ this.emit({
1490
+ type: "notice",
1491
+ level: "error",
1492
+ text: `中止后重启失败:${err.message}`,
1493
+ textEn: `Failed to restart after abort: ${err.message}`,
1494
+ });
1442
1495
  }
1443
1496
  this.flushSnapshot(true);
1444
1497
  }
1445
1498
  async abortBash() {
1446
1499
  // DSH 无 per-tool 取消;bash 工具由运行时管理,超时策略在运行时侧。
1447
- this.emit({ type: "notice", level: "info", text: "DSH 引擎暂不支持单独中止 bash(可整体停止对话)" });
1500
+ this.emit({
1501
+ type: "notice",
1502
+ level: "info",
1503
+ text: "DSH 引擎暂不支持单独中止 bash(可整体停止对话)",
1504
+ textEn: "The DSH engine cannot stop bash alone (stop the whole conversation instead)",
1505
+ });
1448
1506
  }
1449
1507
  // -----------------------------------------------------------------------
1450
1508
  // 后台任务
@@ -1522,16 +1580,56 @@ export class DshClientSession {
1522
1580
  const { rmSync } = await import("node:fs");
1523
1581
  const abs = resolve(path);
1524
1582
  if (!abs.startsWith(this.sessionRoot + sep)) {
1525
- this.emit({ type: "notice", level: "error", text: "拒绝删除会话目录之外的路径" });
1583
+ this.emit({
1584
+ type: "notice",
1585
+ level: "error",
1586
+ text: "拒绝删除会话目录之外的路径",
1587
+ textEn: "Refusing to delete a path outside the session directory",
1588
+ });
1526
1589
  return;
1527
1590
  }
1528
1591
  rmSync(abs, { recursive: true, force: true });
1529
1592
  await this.pushSessions();
1530
- this.emit({ type: "notice", level: "info", text: "会话已删除" });
1593
+ this.emit({ type: "notice", level: "info", text: "会话已删除", textEn: "Session deleted" });
1531
1594
  }
1532
1595
  catch (err) {
1533
- this.emit({ type: "notice", level: "error", text: `删除失败:${err.message}` });
1596
+ this.emit({
1597
+ type: "notice",
1598
+ level: "error",
1599
+ text: `删除失败:${err.message}`,
1600
+ textEn: `Delete failed: ${err.message}`,
1601
+ });
1602
+ }
1603
+ }
1604
+ async dismissConversation(id) {
1605
+ const conv = this.convs.get(id);
1606
+ if (!conv) {
1607
+ this.emit({ type: "notice", level: "warning", text: "该对话不存在或已关闭" });
1608
+ return;
1609
+ }
1610
+ if (id === this.activeId) {
1611
+ this.emit({ type: "notice", level: "warning", text: "当前对话不能直接移出,请先切换到其他对话" });
1612
+ return;
1613
+ }
1614
+ if (!conv.listed) {
1615
+ this.emitConversations();
1616
+ return;
1617
+ }
1618
+ if (conv.isStreaming) {
1619
+ this.emit({
1620
+ type: "notice",
1621
+ level: "warning",
1622
+ text: `对话「${conv.title}」仍在运行中,请先等待结束或停止后再移出`,
1623
+ });
1624
+ return;
1625
+ }
1626
+ if (conv.terminals.list().length > 0) {
1627
+ this.emit({ type: "notice", level: "warning", text: `对话「${conv.title}」还有未关闭的终端` });
1628
+ return;
1534
1629
  }
1630
+ this.removeConversation(id);
1631
+ this.emitConversations();
1632
+ this.flushSnapshot();
1535
1633
  }
1536
1634
  /** 切换会话:读 JSONL 回放 → 新建 conversation(同一 sessionId 续聊)。 */
1537
1635
  async switchSession(path) {
@@ -1555,7 +1653,12 @@ export class DshClientSession {
1555
1653
  this.flushSnapshot(true);
1556
1654
  }
1557
1655
  catch (err) {
1558
- this.emit({ type: "notice", level: "error", text: `切换会话失败:${err.message}` });
1656
+ this.emit({
1657
+ type: "notice",
1658
+ level: "error",
1659
+ text: `切换会话失败:${err.message}`,
1660
+ textEn: `Failed to switch session: ${err.message}`,
1661
+ });
1559
1662
  }
1560
1663
  }
1561
1664
  // -----------------------------------------------------------------------
@@ -1572,7 +1675,7 @@ export class DshClientSession {
1572
1675
  try {
1573
1676
  const { readdirSync } = await import("node:fs");
1574
1677
  for (const name of readdirSync(dir)) {
1575
- if (!/^\./u.test(name) && /\.ya?ml$/iu.test(name)) {
1678
+ if (!name.startsWith(".") && /\.ya?ml$/iu.test(name)) {
1576
1679
  try {
1577
1680
  const st = statSync(join(dir, name));
1578
1681
  if (st.isFile()) {
@@ -1597,10 +1700,20 @@ export class DshClientSession {
1597
1700
  if (this.runtime.alive) {
1598
1701
  await this.runtime.restart(this.model);
1599
1702
  }
1600
- this.emit({ type: "notice", level: "info", text: "已重扫用户 patch 并重启运行时" });
1703
+ this.emit({
1704
+ type: "notice",
1705
+ level: "info",
1706
+ text: "已重扫用户 patch 并重启运行时",
1707
+ textEn: "Rescanned user patches and restarted the runtime",
1708
+ });
1601
1709
  }
1602
1710
  catch (err) {
1603
- this.emit({ type: "notice", level: "error", text: `重扫用户 patch 失败:${err.message}` });
1711
+ this.emit({
1712
+ type: "notice",
1713
+ level: "error",
1714
+ text: `重扫用户 patch 失败:${err.message}`,
1715
+ textEn: `Failed to rescan user patches: ${err.message}`,
1716
+ });
1604
1717
  }
1605
1718
  await this.listDshPatches();
1606
1719
  }
@@ -1683,7 +1796,6 @@ export class DshClientSession {
1683
1796
  return;
1684
1797
  }
1685
1798
  try {
1686
- const { readFileSync: readSessionFile } = await import("node:fs");
1687
1799
  const files = findSessionFilesForCwd(this.sessionRoot, this.cwd);
1688
1800
  const results = [];
1689
1801
  for (const file of files) {
@@ -1697,15 +1809,13 @@ export class DshClientSession {
1697
1809
  const blocks = ev.data?.message?.content ??
1698
1810
  ev.data?.content ??
1699
1811
  [];
1700
- let text = blocks
1701
- .map((b) => (b?.type === "text" ? b.text ?? "" : ""))
1702
- .join("\n");
1812
+ let text = blocks.map((b) => (b?.type === "text" ? (b.text ?? "") : "")).join("\n");
1703
1813
  if (ev.type === "tool/result") {
1704
1814
  // 工具输出在嵌套 content[](tool-result 块内),一并纳入索引。
1705
1815
  const nested = blocks
1706
1816
  .filter((b) => b?.type === "tool-result")
1707
1817
  .flatMap((b) => b.content ?? [])
1708
- .map((b) => (b?.type === "text" ? b.text ?? "" : ""))
1818
+ .map((b) => (b?.type === "text" ? (b.text ?? "") : ""))
1709
1819
  .join("\n");
1710
1820
  if (nested)
1711
1821
  text = `${text}\n${nested}`;
@@ -1792,8 +1902,7 @@ export class DshClientSession {
1792
1902
  continue;
1793
1903
  seen.add(m.id);
1794
1904
  const local = known.get(m.id);
1795
- const vision = local?.vision ??
1796
- (m.inputModalities?.includes("image") ?? false);
1905
+ const vision = local?.vision ?? m.inputModalities?.includes("image") ?? false;
1797
1906
  if (!local)
1798
1907
  this.dynamicModels.add(m.id);
1799
1908
  models.push({
@@ -1823,7 +1932,12 @@ export class DshClientSession {
1823
1932
  // 校验:本地表 + 运行时动态目录(P2-17)。
1824
1933
  const known = DSH_MODELS.some((m) => m.id === modelId) || this.dynamicModels.has(modelId);
1825
1934
  if (!known) {
1826
- this.emit({ type: "notice", level: "warning", text: `未知模型:${modelId}` });
1935
+ this.emit({
1936
+ type: "notice",
1937
+ level: "warning",
1938
+ text: `未知模型:${modelId}`,
1939
+ textEn: `Unknown model: ${modelId}`,
1940
+ });
1827
1941
  return;
1828
1942
  }
1829
1943
  if (modelId === this.model)
@@ -1834,15 +1948,21 @@ export class DshClientSession {
1834
1948
  type: "notice",
1835
1949
  level: "warning",
1836
1950
  text: "有对话正在运行,切换模型将中止当前所有运行(DSH 换模型 = 重启运行时)",
1951
+ textEn: "Conversations are running; switching models aborts all of them (DSH model switch restarts the runtime)",
1837
1952
  });
1838
1953
  }
1839
1954
  this.model = modelId;
1840
1955
  try {
1841
1956
  await this.runtime.restart(modelId);
1842
- this.emit({ type: "notice", level: "info", text: `已切换到 ${modelId}` });
1957
+ this.emit({ type: "notice", level: "info", text: `已切换到 ${modelId}`, textEn: `Switched to ${modelId}` });
1843
1958
  }
1844
1959
  catch (err) {
1845
- this.emit({ type: "notice", level: "error", text: `切换模型失败:${err.message}` });
1960
+ this.emit({
1961
+ type: "notice",
1962
+ level: "error",
1963
+ text: `切换模型失败:${err.message}`,
1964
+ textEn: `Failed to switch model: ${err.message}`,
1965
+ });
1846
1966
  }
1847
1967
  this.flushSnapshot(true);
1848
1968
  }
@@ -1853,7 +1973,12 @@ export class DshClientSession {
1853
1973
  }
1854
1974
  setThinking(level) {
1855
1975
  if (level !== "high") {
1856
- this.emit({ type: "notice", level: "info", text: "DeepSeek V4 仅支持高思考强度" });
1976
+ this.emit({
1977
+ type: "notice",
1978
+ level: "info",
1979
+ text: "DeepSeek V4 仅支持高思考强度",
1980
+ textEn: "DeepSeek V4 only supports high thinking intensity",
1981
+ });
1857
1982
  return;
1858
1983
  }
1859
1984
  this.thinkingLevel = level;
@@ -1861,7 +1986,12 @@ export class DshClientSession {
1861
1986
  }
1862
1987
  cycleThinking() {
1863
1988
  // DSH 固定 high。
1864
- this.emit({ type: "notice", level: "info", text: "DeepSeek V4 仅支持高思考强度" });
1989
+ this.emit({
1990
+ type: "notice",
1991
+ level: "info",
1992
+ text: "DeepSeek V4 仅支持高思考强度",
1993
+ textEn: "DeepSeek V4 only supports high thinking intensity",
1994
+ });
1865
1995
  }
1866
1996
  // -----------------------------------------------------------------------
1867
1997
  // 设置 / 系统提示词
@@ -1892,6 +2022,8 @@ export class DshClientSession {
1892
2022
  reviewSkills: [],
1893
2023
  extensions: [],
1894
2024
  presets: this.stateStore.getPresets(this.clientId),
2025
+ subagentTemplates: [],
2026
+ subagentDefaultTemplates: [],
1895
2027
  };
1896
2028
  this.emit({ type: "settings_state", settings });
1897
2029
  }
@@ -1962,7 +2094,12 @@ export class DshClientSession {
1962
2094
  }
1963
2095
  async reloadExtensions() {
1964
2096
  // DSH 引擎无 pi 扩展体系。
1965
- this.emit({ type: "notice", level: "info", text: "DSH 引擎不支持 pi 扩展热重载" });
2097
+ this.emit({
2098
+ type: "notice",
2099
+ level: "info",
2100
+ text: "DSH 引擎不支持 pi 扩展热重载",
2101
+ textEn: "The DSH engine does not support pi extension hot-reload",
2102
+ });
1966
2103
  }
1967
2104
  async savePreset(name) {
1968
2105
  const presets = this.stateStore.getPresets(this.clientId);
@@ -1982,13 +2119,23 @@ export class DshClientSession {
1982
2119
  reviewDisabledSkills: [],
1983
2120
  };
1984
2121
  this.stateStore.savePresets(this.clientId, existing ? presets.map((p) => (p.name === name ? preset : p)) : [...presets, preset]);
1985
- this.emit({ type: "notice", level: "info", text: `预设「${name}」已保存` });
2122
+ this.emit({
2123
+ type: "notice",
2124
+ level: "info",
2125
+ text: `预设「${name}」已保存`,
2126
+ textEn: `Preset "${name}" saved`,
2127
+ });
1986
2128
  this.pushSettings();
1987
2129
  }
1988
2130
  async applyPreset(name) {
1989
2131
  const preset = this.stateStore.getPresets(this.clientId).find((p) => p.name === name);
1990
2132
  if (!preset) {
1991
- this.emit({ type: "notice", level: "warning", text: `预设「${name}」不存在` });
2133
+ this.emit({
2134
+ type: "notice",
2135
+ level: "warning",
2136
+ text: `预设「${name}」不存在`,
2137
+ textEn: `Preset "${name}" does not exist`,
2138
+ });
1992
2139
  return;
1993
2140
  }
1994
2141
  this.settings.promptMode = preset.promptMode;
@@ -2022,6 +2169,25 @@ export class DshClientSession {
2022
2169
  this.pushSettings();
2023
2170
  }
2024
2171
  // -----------------------------------------------------------------------
2172
+ // 子代理模板(DSH 引擎无子代理概念 —— 设置面板分区由前端 isDsh 隐藏)
2173
+ // -----------------------------------------------------------------------
2174
+ async saveSubagentTemplate() {
2175
+ this.emit({
2176
+ type: "notice",
2177
+ level: "info",
2178
+ text: "DSH 引擎不支持子代理模板(请在 pi 引擎中使用)",
2179
+ textEn: "Subagent templates are not supported by the DSH engine (use the pi engine instead)",
2180
+ });
2181
+ }
2182
+ async deleteSubagentTemplate() {
2183
+ this.emit({
2184
+ type: "notice",
2185
+ level: "info",
2186
+ text: "DSH 引擎不支持子代理模板(请在 pi 引擎中使用)",
2187
+ textEn: "Subagent templates are not supported by the DSH engine (use the pi engine instead)",
2188
+ });
2189
+ }
2190
+ // -----------------------------------------------------------------------
2025
2191
  // 目标(DSH 原生 goal 域:goal/change 事件驱动,round-driver 自动轮次)
2026
2192
  // -----------------------------------------------------------------------
2027
2193
  emitGoalStatus() {
@@ -2041,6 +2207,7 @@ export class DshClientSession {
2041
2207
  g.verdict = "pending";
2042
2208
  g.feedback = undefined;
2043
2209
  g.status = "";
2210
+ g.statusEn = "";
2044
2211
  }
2045
2212
  else if (data.goal?.objective) {
2046
2213
  conv.dsGoal = {
@@ -2070,22 +2237,29 @@ export class DshClientSession {
2070
2237
  max > 0 && rounds >= max
2071
2238
  ? `已达轮数上限(${rounds}/${max}),目标未完成`
2072
2239
  : `目标进行中(第 ${rounds + 1} 轮)…`;
2240
+ g.statusEn =
2241
+ max > 0 && rounds >= max
2242
+ ? `Round cap reached (${rounds}/${max}), goal incomplete`
2243
+ : `Goal in progress (round ${rounds + 1})…`;
2073
2244
  }
2074
2245
  else if (phase === "complete") {
2075
2246
  g.reviewing = false;
2076
2247
  g.verdict = "pass";
2077
2248
  g.status = "✅ 目标已达成";
2249
+ g.statusEn = "✅ Goal achieved";
2078
2250
  }
2079
2251
  else if (phase === "blocked") {
2080
2252
  g.reviewing = false;
2081
2253
  g.verdict = "fail";
2082
2254
  g.feedback = data.goal.blockedReason ?? "(模型报告受阻)";
2083
2255
  g.status = "目标受阻";
2256
+ g.statusEn = "Goal blocked";
2084
2257
  }
2085
2258
  else if (phase === "paused") {
2086
2259
  g.reviewing = false;
2087
2260
  g.verdict = "pending";
2088
2261
  g.status = "目标已暂停";
2262
+ g.statusEn = "Goal paused";
2089
2263
  }
2090
2264
  else {
2091
2265
  g.reviewing = false;
@@ -2120,6 +2294,7 @@ export class DshClientSession {
2120
2294
  g.verdict = "pending";
2121
2295
  g.feedback = undefined;
2122
2296
  g.status = "目标已设,等待生成…";
2297
+ g.statusEn = "Goal set, waiting to generate…";
2123
2298
  this.goalPrefs = { reviewModel: g.reviewModel, maxRounds: g.maxRounds, locked: g.locked };
2124
2299
  this.stateStore.saveGoalPrefs(this.clientId, {
2125
2300
  reviewModel: g.reviewModel,
@@ -2127,7 +2302,12 @@ export class DshClientSession {
2127
2302
  locked: g.locked,
2128
2303
  });
2129
2304
  this.emitGoalStatus();
2130
- this.emit({ type: "notice", level: "info", text: `🎯 已设目标:${text.slice(0, 80)}${text.length > 80 ? "…" : ""}` });
2305
+ this.emit({
2306
+ type: "notice",
2307
+ level: "info",
2308
+ text: `🎯 已设目标:${text.slice(0, 80)}${text.length > 80 ? "…" : ""}`,
2309
+ textEn: `🎯 Goal set: ${text.slice(0, 80)}${text.length > 80 ? "…" : ""}`,
2310
+ });
2131
2311
  try {
2132
2312
  // goal/set 创建 + arm;round-driver 在 agent idle 时自动续轮,无需额外 prompt。
2133
2313
  const res = (await this.runtime.goalSet(conv.sessionId, text, effectiveMax > 0 ? effectiveMax : undefined));
@@ -2137,7 +2317,13 @@ export class DshClientSession {
2137
2317
  catch (err) {
2138
2318
  g.reviewing = false;
2139
2319
  g.status = `目标设置失败:${err.message}`;
2140
- this.emit({ type: "notice", level: "error", text: `目标设置失败:${err.message}` });
2320
+ g.statusEn = `Goal setup failed: ${err.message}`;
2321
+ this.emit({
2322
+ type: "notice",
2323
+ level: "error",
2324
+ text: `目标设置失败:${err.message}`,
2325
+ textEn: `Goal setup failed: ${err.message}`,
2326
+ });
2141
2327
  }
2142
2328
  this.emitGoalStatus();
2143
2329
  this.flushSnapshot();
@@ -2166,6 +2352,7 @@ export class DshClientSession {
2166
2352
  g.verdict = "pending";
2167
2353
  g.feedback = undefined;
2168
2354
  g.status = "";
2355
+ g.statusEn = "";
2169
2356
  this.emitGoalStatus();
2170
2357
  this.flushSnapshot();
2171
2358
  }
@@ -2180,7 +2367,12 @@ export class DshClientSession {
2180
2367
  return;
2181
2368
  const g = conv.goal;
2182
2369
  if (g.goal || g.reviewing || g.wizard.active) {
2183
- this.emit({ type: "notice", level: "warning", text: "已有目标或调研进行中,请先完成或清除" });
2370
+ this.emit({
2371
+ type: "notice",
2372
+ level: "warning",
2373
+ text: "已有目标或调研进行中,请先完成或清除",
2374
+ textEn: "A goal or survey is already active — finish or clear it first",
2375
+ });
2184
2376
  return;
2185
2377
  }
2186
2378
  g.wizard.active = true;
@@ -2189,12 +2381,15 @@ export class DshClientSession {
2189
2381
  g.wizard.step = 0;
2190
2382
  g.wizard.maxSteps = 6;
2191
2383
  g.wizard.status = "调研中…";
2384
+ g.wizard.statusEn = "Scoping…";
2192
2385
  g.status = "目标调研中…";
2386
+ g.statusEn = "Scoping the goal…";
2193
2387
  this.emitGoalStatus();
2194
2388
  this.emit({
2195
2389
  type: "notice",
2196
2390
  level: "info",
2197
2391
  text: `🔍 正在围绕需求展开调研:${draft.slice(0, 60)}${draft.length > 60 ? "…" : ""}`,
2392
+ textEn: `🔍 Surveying the requirement: ${draft.slice(0, 60)}${draft.length > 60 ? "…" : ""}`,
2198
2393
  });
2199
2394
  try {
2200
2395
  const waiter = new Promise((resolve, reject) => {
@@ -2215,26 +2410,42 @@ export class DshClientSession {
2215
2410
  await waiter;
2216
2411
  }
2217
2412
  catch (err) {
2218
- this.emit({ type: "notice", level: "warning", text: `目标调研中断:${err.message}` });
2413
+ this.emit({
2414
+ type: "notice",
2415
+ level: "warning",
2416
+ text: `目标调研中断:${err.message}`,
2417
+ textEn: `Goal survey interrupted: ${err.message}`,
2418
+ });
2219
2419
  }
2220
2420
  conv.turnWaiter = undefined;
2221
2421
  g.wizard.active = false;
2222
2422
  g.wizard.step = 0;
2223
2423
  g.wizard.status = "";
2424
+ g.wizard.statusEn = "";
2224
2425
  // 解析模型最终输出中的 GOAL: 行。
2225
2426
  const finalText = this.lastAssistantText(conv);
2226
2427
  const goalMatch = finalText.match(/GOAL\s*[::]\s*([\s\S]*)/i);
2227
2428
  let refined = goalMatch ? goalMatch[1].trim() : "";
2228
2429
  if (!refined) {
2229
2430
  // 未按格式:取最后一段非空文本(截断防污染)。
2230
- refined = finalText.split("\n").filter((l) => l.trim()).pop()?.trim() ?? "";
2431
+ refined =
2432
+ finalText
2433
+ .split("\n")
2434
+ .filter((l) => l.trim())
2435
+ .pop()
2436
+ ?.trim() ?? "";
2231
2437
  if (refined.length > 300)
2232
2438
  refined = refined.slice(0, 300);
2233
2439
  }
2234
2440
  if (!refined && g.goal) {
2235
2441
  // 模型可能直接用了 create_goal —— 目标已在运行时,镜像它即可。
2236
2442
  this.emitGoalStatus();
2237
- this.emit({ type: "notice", level: "info", text: "🎯 调研完成,目标已由模型创建" });
2443
+ this.emit({
2444
+ type: "notice",
2445
+ level: "info",
2446
+ text: "🎯 调研完成,目标已由模型创建",
2447
+ textEn: "🎯 Survey done; the model created the goal",
2448
+ });
2238
2449
  return;
2239
2450
  }
2240
2451
  this.emitGoalStatus();
@@ -2248,10 +2459,16 @@ export class DshClientSession {
2248
2459
  type: "notice",
2249
2460
  level: "info",
2250
2461
  text: `🎯 调研完成,目标已设为:${refined.slice(0, 80)}${refined.length > 80 ? "…" : ""}`,
2462
+ textEn: `🎯 Survey done, goal set: ${refined.slice(0, 80)}${refined.length > 80 ? "…" : ""}`,
2251
2463
  });
2252
2464
  }
2253
2465
  else {
2254
- this.emit({ type: "notice", level: "warning", text: "调研未产出有效目标,请重试" });
2466
+ this.emit({
2467
+ type: "notice",
2468
+ level: "warning",
2469
+ text: "调研未产出有效目标,请重试",
2470
+ textEn: "The survey produced no usable goal — retry",
2471
+ });
2255
2472
  }
2256
2473
  }
2257
2474
  /** 调研向导指令(主会话 prompt):先提问收敛,最后只输出 GOAL: 行。 */
@@ -2304,10 +2521,15 @@ export class DshClientSession {
2304
2521
  async saveCommands(commands) {
2305
2522
  const { path, error } = await saveCommandsFile(this.cwd, commands);
2306
2523
  if (error) {
2307
- this.emit({ type: "notice", level: "error", text: `保存命令失败:${error}` });
2524
+ this.emit({
2525
+ type: "notice",
2526
+ level: "error",
2527
+ text: `保存命令失败:${error}`,
2528
+ textEn: `Failed to save command: ${error}`,
2529
+ });
2308
2530
  }
2309
2531
  else {
2310
- this.emit({ type: "notice", level: "info", text: `命令已保存(${path})` });
2532
+ this.emit({ type: "notice", level: "info", text: `命令已保存(${path})`, textEn: `Command saved (${path})` });
2311
2533
  }
2312
2534
  }
2313
2535
  // -----------------------------------------------------------------------
@@ -2343,7 +2565,12 @@ export class DshClientSession {
2343
2565
  return true;
2344
2566
  case "model": {
2345
2567
  if (!args.trim()) {
2346
- this.emit({ type: "notice", level: "info", text: `当前模型:${this.model}。用法:/model <名称>` });
2568
+ this.emit({
2569
+ type: "notice",
2570
+ level: "info",
2571
+ text: `当前模型:${this.model}。用法:/model <名称>`,
2572
+ textEn: `Current model: ${this.model}. Usage: /model <name>`,
2573
+ });
2347
2574
  return true;
2348
2575
  }
2349
2576
  const q = args.trim().toLowerCase();
@@ -2357,13 +2584,19 @@ export class DshClientSession {
2357
2584
  type: "notice",
2358
2585
  level: "error",
2359
2586
  text: `没有匹配到模型:${args.trim()}(可用模型见顶栏模型列表)`,
2587
+ textEn: `No matching model: ${args.trim()} (see the model list in the top bar)`,
2360
2588
  });
2361
2589
  }
2362
2590
  return true;
2363
2591
  }
2364
2592
  case "cwd": {
2365
2593
  if (!args.trim()) {
2366
- this.emit({ type: "notice", level: "info", text: `当前工作目录:${this.cwd}` });
2594
+ this.emit({
2595
+ type: "notice",
2596
+ level: "info",
2597
+ text: `当前工作目录:${this.cwd}`,
2598
+ textEn: `Current directory: ${this.cwd}`,
2599
+ });
2367
2600
  return true;
2368
2601
  }
2369
2602
  await this.setCwd(args.trim());
@@ -2377,14 +2610,29 @@ export class DshClientSession {
2377
2610
  case "copy":
2378
2611
  return true; // 前端本地实现
2379
2612
  case "reload":
2380
- this.emit({ type: "notice", level: "info", text: "已重新加载(DSH 引擎无扩展/技能热重载,运行时能力内置)" });
2613
+ this.emit({
2614
+ type: "notice",
2615
+ level: "info",
2616
+ text: "已重新加载(DSH 引擎无扩展/技能热重载,运行时能力内置)",
2617
+ textEn: "Reloaded (the DSH engine has no extension/skill hot-reload; runtime capabilities are built in)",
2618
+ });
2381
2619
  await this.pushSlashCommands();
2382
2620
  return true;
2383
2621
  case "compact":
2384
- this.emit({ type: "notice", level: "info", text: "DSH 引擎不支持上下文压缩(运行时自动管理)" });
2622
+ this.emit({
2623
+ type: "notice",
2624
+ level: "info",
2625
+ text: "DSH 引擎不支持上下文压缩(运行时自动管理)",
2626
+ textEn: "The DSH engine does not support context compaction (the runtime manages it)",
2627
+ });
2385
2628
  return true;
2386
2629
  case "thinking":
2387
- this.emit({ type: "notice", level: "info", text: "DeepSeek V4 仅支持高思考强度" });
2630
+ this.emit({
2631
+ type: "notice",
2632
+ level: "info",
2633
+ text: "DeepSeek V4 仅支持高思考强度",
2634
+ textEn: "DeepSeek V4 only supports high thinking intensity",
2635
+ });
2388
2636
  return true;
2389
2637
  case "pi-web-ui:quit":
2390
2638
  this.onQuit?.();
@@ -2404,6 +2652,7 @@ export class DshClientSession {
2404
2652
  type: "notice",
2405
2653
  level: "error",
2406
2654
  text: `插件命令 /${name} 执行失败:${err.message}`,
2655
+ textEn: `Plugin command /${name} failed: ${err.message}`,
2407
2656
  });
2408
2657
  }
2409
2658
  return true;
@@ -2424,7 +2673,9 @@ export class DshClientSession {
2424
2673
  }
2425
2674
  async checkUpdate() {
2426
2675
  try {
2427
- const latest = await checkAllUpdates([{ name: "pi-web-ui", version: DshClientSession.currentAppVersion(), kind: "webui" }]);
2676
+ const latest = await checkAllUpdates([
2677
+ { name: "pi-web-ui", version: DshClientSession.currentAppVersion(), kind: "webui" },
2678
+ ]);
2428
2679
  const item = latest[0];
2429
2680
  this.emit({
2430
2681
  type: "update_status",
@@ -2436,7 +2687,14 @@ export class DshClientSession {
2436
2687
  });
2437
2688
  }
2438
2689
  catch (err) {
2439
- this.emit({ type: "update_status", current: DshClientSession.currentAppVersion(), latest: null, latestPublishedAt: null, upToDate: true, error: err.message });
2690
+ this.emit({
2691
+ type: "update_status",
2692
+ current: DshClientSession.currentAppVersion(),
2693
+ latest: null,
2694
+ latestPublishedAt: null,
2695
+ upToDate: true,
2696
+ error: err.message,
2697
+ });
2440
2698
  }
2441
2699
  }
2442
2700
  async checkUpdatesAll(force = false) {
@@ -2460,7 +2718,7 @@ export class DshClientSession {
2460
2718
  })),
2461
2719
  });
2462
2720
  }
2463
- catch (err) {
2721
+ catch {
2464
2722
  this.emit({ type: "update_status_all", items: [] });
2465
2723
  }
2466
2724
  }
@@ -2473,7 +2731,7 @@ export class DshClientSession {
2473
2731
  async setProviderApiKey(provider, apiKey) {
2474
2732
  const key = apiKey.trim();
2475
2733
  if (!key) {
2476
- this.emit({ type: "notice", level: "error", text: "请填写 API 密钥" });
2734
+ this.emit({ type: "notice", level: "error", text: "请填写 API 密钥", textEn: "Enter an API key" });
2477
2735
  return;
2478
2736
  }
2479
2737
  try {
@@ -2489,13 +2747,23 @@ export class DshClientSession {
2489
2747
  auth[provider.trim()] = { type: "api_key", key };
2490
2748
  mkdirSync(dirname(authPath), { recursive: true });
2491
2749
  writeFileSync(authPath, JSON.stringify(auth, null, 2) + "\n");
2492
- this.emit({ type: "notice", level: "info", text: `✅ 已保存 ${provider.trim()} 的 API 密钥` });
2750
+ this.emit({
2751
+ type: "notice",
2752
+ level: "info",
2753
+ text: `✅ 已保存 ${provider.trim()} 的 API 密钥`,
2754
+ textEn: `Saved API key`,
2755
+ });
2493
2756
  if (this.runtime.alive)
2494
2757
  await this.runtime.restart(this.model);
2495
2758
  this.flushSnapshot();
2496
2759
  }
2497
2760
  catch (err) {
2498
- this.emit({ type: "notice", level: "error", text: `保存 key 失败:${err.message}` });
2761
+ this.emit({
2762
+ type: "notice",
2763
+ level: "error",
2764
+ text: `保存 key 失败:${err.message}`,
2765
+ textEn: `Failed to save key: ${err.message}`,
2766
+ });
2499
2767
  }
2500
2768
  }
2501
2769
  async clearProviderApiKey(provider) {
@@ -2504,28 +2772,48 @@ export class DshClientSession {
2504
2772
  const authPath = join(this.agentDir, "auth.json");
2505
2773
  const auth = JSON.parse(readFileSync(authPath, "utf8"));
2506
2774
  if (!(pid in auth)) {
2507
- this.emit({ type: "notice", level: "info", text: `${pid} 没有已保存的密钥` });
2775
+ this.emit({ type: "notice", level: "info", text: `${pid} 没有已保存的密钥`, textEn: `No saved key` });
2508
2776
  return;
2509
2777
  }
2510
2778
  delete auth[pid];
2511
2779
  writeFileSync(authPath, JSON.stringify(auth, null, 2) + "\n");
2512
- this.emit({ type: "notice", level: "info", text: `🗑 已清除 ${pid} 的密钥,该服务商回到未配置状态` });
2780
+ this.emit({
2781
+ type: "notice",
2782
+ level: "info",
2783
+ text: `🗑 已清除 ${pid} 的密钥,该服务商回到未配置状态`,
2784
+ textEn: "Cleared",
2785
+ });
2513
2786
  if (this.runtime.alive && pid === "deepseek")
2514
2787
  await this.runtime.restart(this.model);
2515
2788
  this.flushSnapshot();
2516
2789
  }
2517
2790
  catch (err) {
2518
- this.emit({ type: "notice", level: "error", text: `清除失败:${err.message}` });
2791
+ this.emit({
2792
+ type: "notice",
2793
+ level: "error",
2794
+ text: `清除失败:${err.message}`,
2795
+ textEn: `Clear failed: ${err.message}`,
2796
+ });
2519
2797
  }
2520
2798
  }
2521
2799
  async listModelsConfig() {
2522
2800
  this.emit({ type: "models_config", providers: [] });
2523
2801
  }
2524
- async saveModelConfig(providerId, config) {
2525
- this.emit({ type: "notice", level: "warning", text: "DSH 引擎使用内置 DeepSeek 模型,不支持自定义模型配置" });
2802
+ async saveModelConfig(_providerId, _config) {
2803
+ this.emit({
2804
+ type: "notice",
2805
+ level: "warning",
2806
+ text: "DSH 引擎使用内置 DeepSeek 模型,不支持自定义模型配置",
2807
+ textEn: "The DSH engine uses the built-in DeepSeek model",
2808
+ });
2526
2809
  }
2527
- async deleteModelConfig(providerId) {
2528
- this.emit({ type: "notice", level: "warning", text: "DSH 引擎不支持自定义模型配置" });
2810
+ async deleteModelConfig(_providerId) {
2811
+ this.emit({
2812
+ type: "notice",
2813
+ level: "warning",
2814
+ text: "DSH 引擎不支持自定义模型配置",
2815
+ textEn: "The DSH engine does not support custom model configs",
2816
+ });
2529
2817
  }
2530
2818
  async listProviders() {
2531
2819
  this.emit({
@@ -2543,22 +2831,37 @@ export class DshClientSession {
2543
2831
  listProviderKeys() {
2544
2832
  this.emit({ type: "provider_keys", keys: {} });
2545
2833
  }
2546
- async addProviderKey(provider, apiKey, name) {
2547
- this.emit({ type: "notice", level: "warning", text: "DSH 引擎不支持多密钥" });
2834
+ async addProviderKey(_provider, _apiKey, _name) {
2835
+ this.emit({
2836
+ type: "notice",
2837
+ level: "warning",
2838
+ text: "DSH 引擎不支持多密钥",
2839
+ textEn: "The DSH engine does not support multiple keys",
2840
+ });
2548
2841
  }
2549
- async activateProviderKey(provider, keyName) {
2550
- this.emit({ type: "notice", level: "warning", text: "DSH 引擎不支持多密钥" });
2842
+ async activateProviderKey(_provider, _keyName) {
2843
+ this.emit({
2844
+ type: "notice",
2845
+ level: "warning",
2846
+ text: "DSH 引擎不支持多密钥",
2847
+ textEn: "The DSH engine does not support multiple keys",
2848
+ });
2551
2849
  }
2552
- async removeProviderKey(provider, keyName) {
2553
- this.emit({ type: "notice", level: "warning", text: "DSH 引擎不支持多密钥" });
2850
+ async removeProviderKey(_provider, _keyName) {
2851
+ this.emit({
2852
+ type: "notice",
2853
+ level: "warning",
2854
+ text: "DSH 引擎不支持多密钥",
2855
+ textEn: "The DSH engine does not support multiple keys",
2856
+ });
2554
2857
  }
2555
- async fetchModelsList(reqId, baseUrl, apiKey, authHeader, api) {
2858
+ async fetchModelsList(reqId, _baseUrl, _apiKey, _authHeader, _api) {
2556
2859
  this.emit({ type: "fetch_models_result", reqId, ok: false, error: "DSH 引擎不支持自定义 provider 探测" });
2557
2860
  }
2558
- async refreshProviderModels(providerId, reqId) {
2861
+ async refreshProviderModels(_providerId, reqId) {
2559
2862
  this.emit({ type: "refresh_provider_result", reqId, ok: false, error: "DSH 引擎不支持自定义 provider" });
2560
2863
  }
2561
- async cloneProvider(provider, reqId) {
2864
+ async cloneProvider(_provider, reqId) {
2562
2865
  const error = "DSH 引擎不支持自定义 provider";
2563
2866
  this.emit({ type: "notice", level: "error", text: error });
2564
2867
  this.emit({ type: "clone_provider_result", reqId, ok: false, error });
@@ -2566,7 +2869,7 @@ export class DshClientSession {
2566
2869
  // -----------------------------------------------------------------------
2567
2870
  // 其他
2568
2871
  // -----------------------------------------------------------------------
2569
- resolveDialog(id, value) {
2872
+ resolveDialog(_id, _value) {
2570
2873
  // DSH 引擎无扩展 UI 桥(dialog 由插件宿主走,v1 忽略)。
2571
2874
  }
2572
2875
  async editMessage(messageId, text, attachments) {
@@ -2575,7 +2878,12 @@ export class DshClientSession {
2575
2878
  const conv = this.conv;
2576
2879
  const idx = conv.messages.findIndex((m) => m.id === messageId);
2577
2880
  if (idx < 0) {
2578
- this.emit({ type: "notice", level: "warning", text: "找不到要编辑的消息" });
2881
+ this.emit({
2882
+ type: "notice",
2883
+ level: "warning",
2884
+ text: "找不到要编辑的消息",
2885
+ textEn: "Message to edit not found",
2886
+ });
2579
2887
  return;
2580
2888
  }
2581
2889
  // 截断到编辑点之前的所有消息 + 用编辑后的文本 prompt。
@@ -2603,14 +2911,24 @@ export class DshClientSession {
2603
2911
  this.flushSnapshot(true);
2604
2912
  }
2605
2913
  catch (err) {
2606
- this.emit({ type: "notice", level: "error", text: `编辑重问失败:${err.message}` });
2914
+ this.emit({
2915
+ type: "notice",
2916
+ level: "error",
2917
+ text: `编辑重问失败:${err.message}`,
2918
+ textEn: `Edit-and-reask failed: ${err.message}`,
2919
+ });
2607
2920
  }
2608
2921
  }
2609
2922
  async setCwd(newCwd) {
2610
2923
  try {
2611
2924
  const abs = resolve(newCwd);
2612
2925
  if (!existsSync(abs) || !statSync(abs).isDirectory()) {
2613
- this.emit({ type: "notice", level: "error", text: `切换工作目录失败:目录不存在:${newCwd}` });
2926
+ this.emit({
2927
+ type: "notice",
2928
+ level: "error",
2929
+ text: `切换工作目录失败:目录不存在:${newCwd}`,
2930
+ textEn: `Failed to switch directory; does not exist: ${newCwd}`,
2931
+ });
2614
2932
  return;
2615
2933
  }
2616
2934
  if (abs === this.cwd)
@@ -2622,20 +2940,23 @@ export class DshClientSession {
2622
2940
  await this.runtime.restart(this.model);
2623
2941
  }
2624
2942
  catch (err) {
2625
- this.emit({ type: "notice", level: "error", text: `切换工作区后重启运行时失败:${err.message}` });
2943
+ this.emit({
2944
+ type: "notice",
2945
+ level: "error",
2946
+ text: `切换工作区后重启运行时失败:${err.message}`,
2947
+ textEn: `Failed to restart the runtime after switching workspace: ${err.message}`,
2948
+ });
2626
2949
  }
2627
2950
  // 旧 active 会话:活跃的(流式/有终端/跑过)标 listed → 后台运行可见
2628
2951
  // (与 pi 的 displaceActive 语义一致);空白的直接弃(不列)。
2629
2952
  const prev = this.conv;
2630
2953
  prev.listed =
2631
- prev.isStreaming ||
2632
- prev.terminals.list().length > 0 ||
2633
- prev.promptedSinceActive ||
2634
- prev.messages.length > 0;
2954
+ prev.isStreaming || prev.terminals.list().length > 0 || prev.promptedSinceActive || prev.messages.length > 0;
2635
2955
  // 新项目 → 新会话。
2636
2956
  this.activeId = this.addConversation(`web-${randomUUID().slice(0, 12)}`, abs, false).id;
2637
2957
  // 旧项目非活跃 conversation 回收(pi 的 displaceActive 语义:切走后
2638
2958
  // 非 streaming / 无终端 / 未列出的旧会话从内存移除,磁盘 JSONL 可回放恢复)。
2959
+ // eslint-disable-next-line unicorn/no-useless-spread -- snapshot: handlers may unsubscribe mid-emit
2639
2960
  for (const [id, c] of [...this.convs]) {
2640
2961
  if (id === this.activeId)
2641
2962
  continue;
@@ -2650,7 +2971,12 @@ export class DshClientSession {
2650
2971
  this.removeConversation(id);
2651
2972
  }
2652
2973
  this.onCwdChanged?.(abs);
2653
- this.emit({ type: "notice", level: "info", text: `已切换到工作目录:${abs}` });
2974
+ this.emit({
2975
+ type: "notice",
2976
+ level: "info",
2977
+ text: `已切换到工作目录:${abs}`,
2978
+ textEn: `Switched to directory: ${abs}`,
2979
+ });
2654
2980
  this.emitConversations();
2655
2981
  void this.pushSessions();
2656
2982
  void this.pushProjects();
@@ -2660,7 +2986,12 @@ export class DshClientSession {
2660
2986
  this.flushSnapshot(true);
2661
2987
  }
2662
2988
  catch (err) {
2663
- this.emit({ type: "notice", level: "error", text: `切换目录失败:${err.message}` });
2989
+ this.emit({
2990
+ type: "notice",
2991
+ level: "error",
2992
+ text: `切换目录失败:${err.message}`,
2993
+ textEn: `Failed to switch directory: ${err.message}`,
2994
+ });
2664
2995
  }
2665
2996
  }
2666
2997
  async dispose() {
@@ -2786,7 +3117,12 @@ export class DshAgentService {
2786
3117
  this.clients.set(clientId, cs);
2787
3118
  this.stateStore.remember(clientId, cwd);
2788
3119
  if (cwd !== this.cwd) {
2789
- send({ type: "notice", level: "info", text: `已恢复上次的工作目录:${cwd}` });
3120
+ send({
3121
+ type: "notice",
3122
+ level: "info",
3123
+ text: `已恢复上次的工作目录:${cwd}`,
3124
+ textEn: `Restored the last working directory: ${cwd}`,
3125
+ });
2790
3126
  }
2791
3127
  }
2792
3128
  cs.attachSink(send);