@keyou007/agent-bot 0.1.12 → 0.1.13-alpha.1

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 (91) hide show
  1. package/.env.example +2 -2
  2. package/CHANGELOG.md +18 -0
  3. package/README.md +32 -13
  4. package/README.zh.md +32 -13
  5. package/config.example.yaml +0 -24
  6. package/dist/cli/AgentPrerequisites.js +262 -0
  7. package/dist/cli/AgentPrerequisites.js.map +1 -0
  8. package/dist/cli/DefaultAgentSelection.js +47 -0
  9. package/dist/cli/DefaultAgentSelection.js.map +1 -0
  10. package/dist/cli/Initializer.js +165 -2
  11. package/dist/cli/Initializer.js.map +1 -1
  12. package/dist/cli/ServerStarter.js +17 -2
  13. package/dist/cli/ServerStarter.js.map +1 -1
  14. package/dist/cli/controlProtocol.js.map +1 -1
  15. package/dist/cli/help.js +16 -6
  16. package/dist/cli/help.js.map +1 -1
  17. package/dist/cli.js +233 -4
  18. package/dist/cli.js.map +1 -1
  19. package/dist/codex/AppServerConnection.js +3 -3
  20. package/dist/codex/AppServerConnection.js.map +1 -1
  21. package/dist/codex/CodexProcessManager.js +4 -4
  22. package/dist/codex/CodexProcessManager.js.map +1 -1
  23. package/dist/codex/CodexRuntime.js +28 -16
  24. package/dist/codex/CodexRuntime.js.map +1 -1
  25. package/dist/commands/CommandRouter.js +5 -0
  26. package/dist/commands/CommandRouter.js.map +1 -1
  27. package/dist/config/FeishuUserOpenIdStore.js +76 -0
  28. package/dist/config/FeishuUserOpenIdStore.js.map +1 -0
  29. package/dist/config/loadConfig.js +3 -24
  30. package/dist/config/loadConfig.js.map +1 -1
  31. package/dist/console/ConsoleConnector.js +2 -2
  32. package/dist/console/ConsoleConnector.js.map +1 -1
  33. package/dist/feishu/CardRenderer.js +90 -30
  34. package/dist/feishu/CardRenderer.js.map +1 -1
  35. package/dist/index.js +104 -24
  36. package/dist/index.js.map +1 -1
  37. package/dist/presentation/turnGraph.js +36 -0
  38. package/dist/presentation/turnGraph.js.map +1 -0
  39. package/dist/proxy/ProxySessionController.js +321 -98
  40. package/dist/proxy/ProxySessionController.js.map +1 -1
  41. package/dist/runtime/AcpRuntimeAdapter.js +3 -2
  42. package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
  43. package/dist/startup/StartupNotifier.js +16 -7
  44. package/dist/startup/StartupNotifier.js.map +1 -1
  45. package/dist/startup/startFeishu.js +2 -2
  46. package/dist/startup/startFeishu.js.map +1 -1
  47. package/dist/state/StateStore.js +169 -0
  48. package/dist/state/StateStore.js.map +1 -1
  49. package/dist/state/migrations.js +21 -0
  50. package/dist/state/migrations.js.map +1 -1
  51. package/dist/supervision/SafeRestartNotifier.js +46 -3
  52. package/dist/supervision/SafeRestartNotifier.js.map +1 -1
  53. package/dist/supervision/SafeRestartScheduler.js +65 -6
  54. package/dist/supervision/SafeRestartScheduler.js.map +1 -1
  55. package/dist/supervision/replacementSupervisor.js +27 -2
  56. package/dist/supervision/replacementSupervisor.js.map +1 -1
  57. package/dist/supervisor.js +27 -2
  58. package/dist/supervisor.js.map +1 -1
  59. package/docs/technical-reference.md +42 -20
  60. package/docs/technical-reference.zh.md +42 -20
  61. package/npm-shrinkwrap.json +2 -2
  62. package/package.json +7 -3
  63. package/skills/agent-bot/SKILL.md +21 -9
  64. package/src/cli/AgentPrerequisites.ts +377 -0
  65. package/src/cli/DefaultAgentSelection.ts +56 -0
  66. package/src/cli/Initializer.ts +204 -3
  67. package/src/cli/ServerStarter.ts +25 -2
  68. package/src/cli/controlProtocol.ts +6 -1
  69. package/src/cli/help.ts +16 -6
  70. package/src/cli.ts +350 -5
  71. package/src/codex/AppServerConnection.ts +3 -3
  72. package/src/codex/CodexProcessManager.ts +4 -4
  73. package/src/codex/CodexRuntime.ts +38 -18
  74. package/src/commands/CommandRouter.ts +4 -0
  75. package/src/commands/commandTypes.ts +1 -0
  76. package/src/config/FeishuUserOpenIdStore.ts +81 -0
  77. package/src/config/loadConfig.ts +3 -24
  78. package/src/console/ConsoleConnector.ts +2 -2
  79. package/src/feishu/CardRenderer.ts +117 -30
  80. package/src/index.ts +162 -22
  81. package/src/presentation/turnGraph.ts +48 -0
  82. package/src/proxy/ProxySessionController.ts +375 -107
  83. package/src/runtime/AcpRuntimeAdapter.ts +3 -1
  84. package/src/startup/StartupNotifier.ts +23 -7
  85. package/src/startup/startFeishu.ts +3 -2
  86. package/src/state/StateStore.ts +221 -0
  87. package/src/state/migrations.ts +21 -0
  88. package/src/supervision/SafeRestartNotifier.ts +53 -4
  89. package/src/supervision/SafeRestartScheduler.ts +78 -7
  90. package/src/supervision/replacementSupervisor.ts +27 -1
  91. package/src/supervisor.ts +33 -2
@@ -163,7 +163,7 @@ export class CodexRuntime implements AgentRuntime {
163
163
  if (!isUnsupportedExcludeTurnsError(error)) throw error;
164
164
  this.logger.warn(
165
165
  { error },
166
- "Codex App Server does not support thread/fork excludeTurns; retrying without it.",
166
+ "App Server does not support thread/fork excludeTurns; retrying without it.",
167
167
  );
168
168
  response = await client.request<ThreadResponse>("thread/fork", forkParams, FORK_REQUEST_TIMEOUT_MS);
169
169
  }
@@ -343,7 +343,7 @@ export class CodexRuntime implements AgentRuntime {
343
343
  );
344
344
  this.logger.info(
345
345
  { ...(sessionId ? { sessionId } : {}), threadId: remoteSessionId, turnId },
346
- "Codex accepted the turn interrupt request.",
346
+ "App Server accepted the turn interrupt request.",
347
347
  );
348
348
  }
349
349
 
@@ -554,7 +554,7 @@ export class CodexRuntime implements AgentRuntime {
554
554
  const session = [...this.sessions.values()].find((candidate) => candidate.remoteSessionId === threadId);
555
555
  if (session?.activeTurnId && status && status !== "active") {
556
556
  void this.synchronizeSession(session.localSessionId).catch((error: unknown) => {
557
- this.logger.warn({ error, sessionId: session.localSessionId, status }, "Failed to reconcile Codex thread status.");
557
+ this.logger.warn({ error, sessionId: session.localSessionId, status }, "Failed to reconcile App Server thread status.");
558
558
  });
559
559
  }
560
560
  return;
@@ -566,7 +566,7 @@ export class CodexRuntime implements AgentRuntime {
566
566
  if (session.terminalTurnIds.has(mapped.turnId)) {
567
567
  this.logger.debug(
568
568
  { sessionId: session.localSessionId, turnId: mapped.turnId, method },
569
- "Ignoring a replayed terminal Codex turn event.",
569
+ "Ignoring a replayed terminal App Server turn event.",
570
570
  );
571
571
  return;
572
572
  }
@@ -586,7 +586,7 @@ export class CodexRuntime implements AgentRuntime {
586
586
  notificationTurnId: mapped.turnId,
587
587
  notificationStartedAt: mapped.startedAt,
588
588
  },
589
- "Ignoring a replayed historical Codex turn start.",
589
+ "Ignoring a replayed historical App Server turn start.",
590
590
  );
591
591
  return;
592
592
  }
@@ -597,10 +597,10 @@ export class CodexRuntime implements AgentRuntime {
597
597
  if (!session.activeTurnId || session.activeTurnId !== mapped.turnId) {
598
598
  this.logger.debug(
599
599
  { sessionId: session.localSessionId, activeTurnId: session.activeTurnId, notificationTurnId: mapped.turnId, method },
600
- "Ignoring out-of-order Codex notification and scheduling reconciliation.",
600
+ "Ignoring out-of-order App Server notification and scheduling reconciliation.",
601
601
  );
602
602
  void this.synchronizeSession(session.localSessionId).catch((error: unknown) => {
603
- this.logger.warn({ error, sessionId: session.localSessionId }, "Failed to reconcile out-of-order Codex notification.");
603
+ this.logger.warn({ error, sessionId: session.localSessionId }, "Failed to reconcile out-of-order App Server notification.");
604
604
  });
605
605
  return;
606
606
  }
@@ -671,9 +671,12 @@ export class CodexRuntime implements AgentRuntime {
671
671
  if (mapped.status === "cancelled") {
672
672
  this.emit({ type: "turn_cancelled", sessionId, turnId: mapped.turnId });
673
673
  } else if (mapped.status === "failed") {
674
- this.emit({ type: "turn_failed", sessionId, turnId: mapped.turnId, message: mapped.error ?? "Codex turn failed." });
674
+ this.emit({ type: "turn_failed", sessionId, turnId: mapped.turnId, message: mapped.error ?? "App Server turn failed." });
675
675
  } else {
676
- const finalResponse = appendGeneratedImageMarkdown(session.finalText, session.generatedImagePaths);
676
+ const finalResponse = appendModelSwitchGuidance(
677
+ appendGeneratedImageMarkdown(session.finalText, session.generatedImagePaths),
678
+ session.model,
679
+ );
677
680
  this.emit({
678
681
  type: "turn_completed",
679
682
  sessionId,
@@ -703,7 +706,7 @@ export class CodexRuntime implements AgentRuntime {
703
706
  turnId,
704
707
  request: {
705
708
  id: requestId,
706
- title: stringValue(params.command) ?? "Codex approval request",
709
+ title: stringValue(params.command) ?? "Agent approval request",
707
710
  command: stringValue(params.command),
708
711
  reason: stringValue(params.reason),
709
712
  options: [
@@ -781,7 +784,7 @@ export class CodexRuntime implements AgentRuntime {
781
784
  type: "turn_failed",
782
785
  sessionId: session.localSessionId,
783
786
  turnId: activeTurnId,
784
- message: "Codex no longer reports this execution in the thread history.",
787
+ message: "The App Server no longer reports this execution in the thread history.",
785
788
  });
786
789
  }
787
790
  return;
@@ -828,7 +831,7 @@ export class CodexRuntime implements AgentRuntime {
828
831
  type: "turn_failed",
829
832
  sessionId: session.localSessionId,
830
833
  turnId: turn.id,
831
- message: turn.error?.message ?? "Codex turn failed.",
834
+ message: turn.error?.message ?? "App Server turn failed.",
832
835
  });
833
836
  return;
834
837
  }
@@ -836,9 +839,12 @@ export class CodexRuntime implements AgentRuntime {
836
839
  ...session.generatedImagePaths,
837
840
  ...extractGeneratedImagePaths(turn),
838
841
  ]);
839
- const finalResponse = appendGeneratedImageMarkdown(
840
- extractFinalResponse(turn) || session.finalText,
841
- generatedImagePaths,
842
+ const finalResponse = appendModelSwitchGuidance(
843
+ appendGeneratedImageMarkdown(
844
+ extractFinalResponse(turn) || session.finalText,
845
+ generatedImagePaths,
846
+ ),
847
+ session.model,
842
848
  );
843
849
  session.finalText = finalResponse;
844
850
  session.generatedImagePaths = [];
@@ -888,13 +894,13 @@ export class CodexRuntime implements AgentRuntime {
888
894
 
889
895
  private requireSession(sessionId: string): CodexSession {
890
896
  const session = this.sessions.get(sessionId);
891
- if (!session) throw new Error(`Unknown Codex session: ${sessionId}`);
897
+ if (!session) throw new Error(`Unknown App Server session: ${sessionId}`);
892
898
  return session;
893
899
  }
894
900
 
895
901
  private requireActiveTurn(sessionId: string, turnId: string): CodexSession {
896
902
  const session = this.requireSession(sessionId);
897
- if (session.activeTurnId !== turnId) throw new Error(`Codex turn is no longer active: ${turnId}`);
903
+ if (session.activeTurnId !== turnId) throw new Error(`App Server turn is no longer active: ${turnId}`);
898
904
  return session;
899
905
  }
900
906
 
@@ -917,7 +923,7 @@ export class CodexRuntime implements AgentRuntime {
917
923
  type: "turn_failed",
918
924
  sessionId: session.localSessionId,
919
925
  turnId,
920
- message: `Codex App Server disconnected: ${error.message}`,
926
+ message: `App Server disconnected: ${error.message}`,
921
927
  });
922
928
  }
923
929
  for (const [requestId, pending] of this.approvals) {
@@ -978,6 +984,20 @@ function permissionParams(mode: PermissionMode): { approvalPolicy: "never" | "on
978
984
  : { approvalPolicy: "on-request", sandbox: "workspace-write" };
979
985
  }
980
986
 
987
+ const MODEL_SWITCH_GUIDANCE = "请发送 `/model` 切换到其他模型后重试。";
988
+
989
+ function appendModelSwitchGuidance(response: string, model?: string): string {
990
+ if (!hasKnownCodeModeFailure(response) || response.includes(MODEL_SWITCH_GUIDANCE)) return response;
991
+ const modelLabel = model?.trim() ? ` \`${model.trim()}\`` : "";
992
+ const guidance = `> 当前模型${modelLabel} 的本地工具执行通道异常。${MODEL_SWITCH_GUIDANCE}`;
993
+ return response.trimEnd() ? `${response.trimEnd()}\n\n${guidance}` : guidance;
994
+ }
995
+
996
+ function hasKnownCodeModeFailure(response: string): boolean {
997
+ return /code-mode host closed its stdout/i.test(response)
998
+ || /exec expects an object containing raw JavaScript in [`'"]?input[`'"]?/i.test(response);
999
+ }
1000
+
981
1001
  function threadLifecycleParams(cwd: string): {
982
1002
  developerInstructions: string;
983
1003
  } {
@@ -20,6 +20,7 @@ const COMMAND_NAMES = [
20
20
  "switch",
21
21
  "thinking",
22
22
  "title",
23
+ "turns",
23
24
  ] as const;
24
25
 
25
26
  type CommandName = (typeof COMMAND_NAMES)[number];
@@ -93,6 +94,9 @@ export class CommandRouter {
93
94
  if (args.length === 0) return { type: "restart" };
94
95
  if (args.length === 1 && args[0] === "--force") return { type: "restart", force: true };
95
96
  throw new Error("/restart 只接受一个可选的 --force 参数。");
97
+ case "turns":
98
+ if (args.length > 0) throw new Error("/turns 不接受参数,请在历史轮次卡片中选择 turn。");
99
+ return { type: "turns" };
96
100
  case "model":
97
101
  return settingsCommand("/model", args, { type: "model" });
98
102
  case "provider":
@@ -15,6 +15,7 @@ export type Command =
15
15
  | { type: "goal"; action: "set" | "edit"; objective: string }
16
16
  | { type: "goal"; action: "pause" | "resume" | "clear" }
17
17
  | { type: "restart"; force?: boolean }
18
+ | { type: "turns" }
18
19
  | { type: "model" }
19
20
  | { type: "provider" }
20
21
  | { type: "thinking" }
@@ -0,0 +1,81 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { parse as parseDotEnv } from "dotenv";
4
+
5
+ const USER_OPEN_ID_KEY = "FEISHU_USER_OPEN_ID";
6
+
7
+ export interface FeishuUserOpenIdPersistenceResult {
8
+ status: "stored" | "existing";
9
+ userOpenId: string;
10
+ }
11
+
12
+ export function persistFeishuUserOpenIdIfMissing(
13
+ envPath: string,
14
+ userOpenId: string,
15
+ ): FeishuUserOpenIdPersistenceResult {
16
+ if (!/^ou_[A-Za-z0-9_-]+$/u.test(userOpenId)) {
17
+ throw new Error("Cannot persist an invalid Lark user Open ID.");
18
+ }
19
+
20
+ const original = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf8") : "";
21
+ const existing = parseDotEnv(original)[USER_OPEN_ID_KEY]?.trim();
22
+ if (existing) return { status: "existing", userOpenId: existing };
23
+
24
+ fs.mkdirSync(path.dirname(envPath), { recursive: true });
25
+ writeFileAtomically(envPath, upsertDotEnvValue(original, USER_OPEN_ID_KEY, userOpenId));
26
+ return { status: "stored", userOpenId };
27
+ }
28
+
29
+ function upsertDotEnvValue(contents: string, key: string, value: string): string {
30
+ const newline = contents.includes("\r\n") ? "\r\n" : "\n";
31
+ const lines = contents.length > 0 ? contents.split(/\r?\n/u) : [];
32
+ const matcher = new RegExp(`^\\s*(?:export\\s+)?${key}\\s*=`);
33
+ let replaced = false;
34
+ const updated = lines.map((line) => {
35
+ if (!matcher.test(line)) return line;
36
+ replaced = true;
37
+ return `${key}=${value}`;
38
+ });
39
+ while (updated.length > 0 && updated.at(-1) === "") updated.pop();
40
+ if (!replaced) updated.push(`${key}=${value}`);
41
+ return `${updated.join(newline)}${newline}`;
42
+ }
43
+
44
+ function writeFileAtomically(filePath: string, contents: string): void {
45
+ const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
46
+ let temporaryFile: number | undefined;
47
+ try {
48
+ const mode = fs.statSync(filePath, { throwIfNoEntry: false })?.mode ?? 0o600;
49
+ temporaryFile = fs.openSync(temporaryPath, "wx", mode);
50
+ fs.writeFileSync(temporaryFile, contents, "utf8");
51
+ fs.fsyncSync(temporaryFile);
52
+ fs.closeSync(temporaryFile);
53
+ temporaryFile = undefined;
54
+ fs.renameSync(temporaryPath, filePath);
55
+ restrictFilePermissions(filePath);
56
+ syncDirectory(path.dirname(filePath));
57
+ } finally {
58
+ if (temporaryFile !== undefined) fs.closeSync(temporaryFile);
59
+ fs.rmSync(temporaryPath, { force: true });
60
+ }
61
+ }
62
+
63
+ function restrictFilePermissions(filePath: string): void {
64
+ try {
65
+ fs.chmodSync(filePath, 0o600);
66
+ } catch {
67
+ // Windows and some mounted filesystems do not implement POSIX modes.
68
+ }
69
+ }
70
+
71
+ function syncDirectory(directoryPath: string): void {
72
+ let directory: number | undefined;
73
+ try {
74
+ directory = fs.openSync(directoryPath, "r");
75
+ fs.fsyncSync(directory);
76
+ } catch {
77
+ // Directory fsync is not supported on every Windows or mounted filesystem.
78
+ } finally {
79
+ if (directory !== undefined) fs.closeSync(directory);
80
+ }
81
+ }
@@ -43,30 +43,6 @@ agents:
43
43
  - "stdio://"
44
44
  env: {}
45
45
 
46
- coco:
47
- title: "Coco"
48
- command: "coco"
49
- args:
50
- - "acp"
51
- - "serve"
52
- env: {}
53
-
54
- coco-yolo:
55
- title: "Coco YOLO"
56
- command: "coco"
57
- args:
58
- - "acp"
59
- - "serve"
60
- - "--yolo"
61
- env: {}
62
-
63
- example:
64
- title: "Example ACP Agent"
65
- command: "node"
66
- args:
67
- - "./examples/example-acp-agent.js"
68
- env: {}
69
-
70
46
  defaults:
71
47
  agent: "codex"
72
48
  cwd: "."
@@ -82,6 +58,9 @@ logging:
82
58
  export function loadConfig(
83
59
  configPath?: string,
84
60
  ): AppConfig {
61
+ if (process.env.FEISHU_USER_OPEN_ID !== undefined && !process.env.FEISHU_USER_OPEN_ID.trim()) {
62
+ delete process.env.FEISHU_USER_OPEN_ID;
63
+ }
85
64
  loadDotEnv({
86
65
  path: defaultDotEnvPath(),
87
66
  quiet: true,
@@ -12,8 +12,8 @@ export class ConsoleConnector {
12
12
 
13
13
  start(): void {
14
14
  if (this.reader) return;
15
- this.logger.info("Starting local Codex test console.");
16
- console.log("Codex console ready. Type /help or enter a prompt.");
15
+ this.logger.info("Starting the local Agent Bot console.");
16
+ console.log("Agent Bot console ready. Type /help or enter a prompt.");
17
17
  const reader = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: "agentbot> " });
18
18
  this.reader = reader;
19
19
  reader.prompt();
@@ -142,6 +142,19 @@ export interface TaskListCardAction {
142
142
  export interface TaskListCardEntry {
143
143
  lines: string[];
144
144
  actions?: TaskListCardAction[];
145
+ current?: boolean;
146
+ }
147
+
148
+ export interface ResetHistoryCardEntry extends TaskListCardEntry {
149
+ sequence: number;
150
+ graphNodeLine: string;
151
+ graphConnectorLine?: string;
152
+ }
153
+
154
+ export interface ResetHistoryCardView {
155
+ entries: ResetHistoryCardEntry[];
156
+ footerLines: string[];
157
+ pageActions: TaskListCardAction[];
145
158
  }
146
159
 
147
160
  export class CardRenderer {
@@ -163,7 +176,7 @@ export class CardRenderer {
163
176
  : `**默认 Agent**:${inlineCode(view.currentAgent)}`,
164
177
  ...(view.runtimeSettingsAvailable
165
178
  ? [
166
- `**Provider / 模型**:${inlineCode(view.currentProvider ?? "Codex 默认")} / ${inlineCode(view.currentModel ?? "默认")}`,
179
+ `**Provider / 模型**:${inlineCode(view.currentProvider ?? "Agent 默认")} / ${inlineCode(view.currentModel ?? "默认")}`,
167
180
  `**思考强度 / 权限**:${inlineCode(view.currentEffort ?? "自动")} / ${inlineCode(permissionModeLabel(view.currentPermissionMode))}`,
168
181
  ]
169
182
  : []),
@@ -190,7 +203,7 @@ export class CardRenderer {
190
203
  if (!view.providerSupported) {
191
204
  elements.push(markdown("当前运行时不支持 Provider 切换;其他设置仍可通过上方 tab 修改。"));
192
205
  } else if (view.providers.length === 0) {
193
- elements.push(markdown("Codex 配置中没有可用的 Provider。"));
206
+ elements.push(markdown("当前 Agent 配置中没有可用的 Provider。"));
194
207
  } else {
195
208
  elements.push(...view.providers.map((provider) => settingsOptionRow({
196
209
  label: [
@@ -270,14 +283,14 @@ export class CardRenderer {
270
283
  renderProviderSelector(view: ProviderSelectorCardView): Record<string, unknown> {
271
284
  const elements: Record<string, unknown>[] = [
272
285
  markdown([
273
- `**当前 Provider**:${inlineCode(view.currentProvider ?? "Codex 默认")}`,
286
+ `**当前 Provider**:${inlineCode(view.currentProvider ?? "Agent 默认")}`,
274
287
  `**模型 / 思考强度 / 权限**:${inlineCode(view.currentModel ?? "默认")} / ${inlineCode(view.reasoningEffort ?? "自动")} / ${inlineCode(permissionModeLabel(view.permissionMode))}`,
275
288
  ...(view.notice ? [view.notice] : []),
276
289
  ].join("\n")),
277
290
  { tag: "hr" },
278
291
  ];
279
292
  if (view.providers.length === 0) {
280
- elements.push(markdown("Codex 配置中没有可用的 Provider。"));
293
+ elements.push(markdown("当前 Agent 配置中没有可用的 Provider。"));
281
294
  } else {
282
295
  elements.push(...view.providers.map((provider) => ({
283
296
  tag: "column_set",
@@ -635,14 +648,14 @@ export class CardRenderer {
635
648
  ? [
636
649
  `**当前任务**:${inlineCode(view.currentTask.title ?? view.currentTask.id)}`,
637
650
  workspaceLine,
638
- `**Provider / 模型 / 思考强度 / 权限**:${inlineCode(view.currentTask.modelProvider ?? "Codex 默认")} / ${inlineCode(view.currentTask.model ?? "默认")} / ${inlineCode(view.currentTask.reasoningEffort ?? "自动")} / ${inlineCode(permissionModeLabel(view.currentTask.permissionMode))}`,
651
+ `**Provider / 模型 / 思考强度 / 权限**:${inlineCode(view.currentTask.modelProvider ?? "Agent 默认")} / ${inlineCode(view.currentTask.model ?? "默认")} / ${inlineCode(view.currentTask.reasoningEffort ?? "自动")} / ${inlineCode(permissionModeLabel(view.currentTask.permissionMode))}`,
639
652
  `**任务状态 / Agent**:${persistedTaskStatus(view.currentTask.sessionStatus, view.currentTask.lastTurnStatus)} / ${inlineCode(view.currentTask.agentName)}`,
640
653
  `**任务 ID**:${inlineCode(view.currentTask.id)}`,
641
654
  ]
642
655
  : [
643
656
  "**当前任务**:无,下一条普通消息会创建新任务",
644
657
  workspaceLine,
645
- `**Provider / 模型 / 思考强度 / 权限**:${inlineCode("Codex 默认")} / ${inlineCode("默认")} / ${inlineCode("自动")} / ${inlineCode(permissionModeLabel())}`,
658
+ `**Provider / 模型 / 思考强度 / 权限**:${inlineCode("Agent 默认")} / ${inlineCode("默认")} / ${inlineCode("自动")} / ${inlineCode(permissionModeLabel())}`,
646
659
  `**默认 Agent**:${view.defaultAgentTitle} (${inlineCode(view.defaultAgentName)})`,
647
660
  ];
648
661
  lines.push(
@@ -662,6 +675,18 @@ export class CardRenderer {
662
675
  type: "danger",
663
676
  value: { action: "turn_cancel", sessionId: state.sessionId, turnId: state.turnId },
664
677
  }]));
678
+ } else if (state.status === "completed") {
679
+ elements.push(
680
+ { tag: "hr" },
681
+ taskActionRow([{
682
+ text: "Reset",
683
+ value: { action: "turn_reset", sessionId: state.sessionId, turnId: state.turnId },
684
+ }]),
685
+ {
686
+ ...markdown("<font color='grey'>Reset 会将当前任务的对话上下文恢复到本轮完成时;不会回退本地文件。</font>"),
687
+ text_size: "notation",
688
+ },
689
+ );
665
690
  }
666
691
  return turnCard(
667
692
  turnTitle(state.status, state.prompt ?? state.taskTitle),
@@ -673,8 +698,8 @@ export class CardRenderer {
673
698
 
674
699
  renderTurnDetails(state: TurnViewState): Record<string, unknown> {
675
700
  const title = state.taskTitle
676
- ? `Codex 执行详情:${truncateText(state.taskTitle.replace(/\s+/g, " ").trim(), 60)}`
677
- : "Codex 执行详情";
701
+ ? `任务执行详情:${truncateText(state.taskTitle.replace(/\s+/g, " ").trim(), 60)}`
702
+ : "任务执行详情";
678
703
  return turnCard(title, "blue", renderTurnElements(state, "always"), renderTurnSubtitle(state));
679
704
  }
680
705
 
@@ -774,7 +799,7 @@ export class CardRenderer {
774
799
  sectionTitle: string,
775
800
  entries: TaskListCardEntry[],
776
801
  footerLines: string[],
777
- footerAction?: TaskListCardAction,
802
+ footerActions: TaskListCardAction[] = [],
778
803
  ): Record<string, unknown> {
779
804
  const elements: Record<string, unknown>[] = [markdown(`**${sectionTitle}**`)];
780
805
  if (entries.length === 0) {
@@ -791,27 +816,7 @@ export class CardRenderer {
791
816
  if (footerLines.length > 0) {
792
817
  elements.push({ tag: "hr" }, markdown(footerLines.join("\n")));
793
818
  }
794
- if (footerAction) {
795
- elements.push({
796
- tag: "column_set",
797
- flex_mode: "none",
798
- columns: [{
799
- tag: "column",
800
- width: "weighted",
801
- weight: 1,
802
- elements: [{
803
- tag: "button",
804
- text: { tag: "plain_text", content: footerAction.text },
805
- type: footerAction.type ?? "default",
806
- width: "fill",
807
- behaviors: [{
808
- type: "callback",
809
- value: footerAction.value,
810
- }],
811
- }],
812
- }],
813
- });
814
- }
819
+ if (footerActions.length > 0) elements.push(taskActionRow(footerActions));
815
820
  return {
816
821
  schema: "2.0",
817
822
  config: {
@@ -831,6 +836,39 @@ export class CardRenderer {
831
836
  };
832
837
  }
833
838
 
839
+ renderResetHistoryCard(view: ResetHistoryCardView): Record<string, unknown> {
840
+ const elements: Record<string, unknown>[] = [
841
+ {
842
+ ...markdown("<font color='grey'>Reset 会将当前任务的对话上下文恢复到所选轮次完成时;不会回退本地文件。</font>"),
843
+ text_size: "notation",
844
+ },
845
+ { tag: "hr" },
846
+ markdown("**历史对话轮次**"),
847
+ ];
848
+ if (view.entries.length === 0) {
849
+ elements.push(markdown("当前任务还没有成功完成的 turn。"));
850
+ } else {
851
+ view.entries.forEach((entry) => {
852
+ const action = entry.actions?.[0];
853
+ elements.push(resetHistoryEntryRow(
854
+ entry.graphNodeLine,
855
+ entry.graphConnectorLine,
856
+ entry.lines,
857
+ action,
858
+ entry.current === true,
859
+ ));
860
+ });
861
+ }
862
+ if (view.footerLines.length > 0) {
863
+ elements.push({ tag: "hr" }, {
864
+ ...markdown(view.footerLines.join("\n")),
865
+ text_size: "notation",
866
+ });
867
+ }
868
+ if (view.pageActions.length > 0) elements.push(taskActionRow(view.pageActions));
869
+ return sectionCard("历史对话轮次", elements);
870
+ }
871
+
834
872
  private baseCard(title: string, template: string, elements: unknown[]): Record<string, unknown> {
835
873
  return {
836
874
  config: {
@@ -1470,6 +1508,55 @@ function settingsOptionRow(input: {
1470
1508
  };
1471
1509
  }
1472
1510
 
1511
+ function resetHistoryEntryRow(
1512
+ graphNodeLine: string,
1513
+ graphConnectorLine: string | undefined,
1514
+ lines: string[],
1515
+ action: TaskListCardAction | undefined,
1516
+ current: boolean,
1517
+ ): Record<string, unknown> {
1518
+ const graph = [
1519
+ `<font color='${current ? "green" : "blue"}'>${escapeCardHtml(graphNodeLine)}</font>`,
1520
+ ...(graphConnectorLine
1521
+ ? [`<font color='grey'>${escapeCardHtml(graphConnectorLine)}</font>`]
1522
+ : []),
1523
+ ].join("\n");
1524
+ return {
1525
+ tag: "column_set",
1526
+ flex_mode: "none",
1527
+ horizontal_spacing: "12px",
1528
+ vertical_align: "top",
1529
+ columns: [
1530
+ {
1531
+ tag: "column",
1532
+ width: "auto",
1533
+ vertical_align: "top",
1534
+ elements: [{
1535
+ ...markdown(graph),
1536
+ text_align: "center",
1537
+ }],
1538
+ },
1539
+ {
1540
+ tag: "column",
1541
+ width: "weighted",
1542
+ weight: 1,
1543
+ vertical_align: "top",
1544
+ elements: [markdown(lines.join("\n"))],
1545
+ },
1546
+ {
1547
+ tag: "column",
1548
+ width: "auto",
1549
+ vertical_align: "top",
1550
+ elements: current
1551
+ ? [markdown("✅ 当前")]
1552
+ : action
1553
+ ? [taskActionElement(action)]
1554
+ : [],
1555
+ },
1556
+ ],
1557
+ };
1558
+ }
1559
+
1473
1560
  function taskActionRow(actions: TaskListCardAction[]): Record<string, unknown> {
1474
1561
  return {
1475
1562
  tag: "column_set",