@keyou007/agent-bot 0.1.24-alpha.0 → 0.1.24-alpha.2

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 (48) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +24 -4
  3. package/README.zh.md +24 -4
  4. package/dist/cli/SelfUpdateRunner.js +2 -0
  5. package/dist/cli/SelfUpdateRunner.js.map +1 -1
  6. package/dist/codex/CodexEventMapper.js +90 -2
  7. package/dist/codex/CodexEventMapper.js.map +1 -1
  8. package/dist/codex/CodexRuntime.js +160 -12
  9. package/dist/codex/CodexRuntime.js.map +1 -1
  10. package/dist/commands/CommandRouter.js +14 -1
  11. package/dist/commands/CommandRouter.js.map +1 -1
  12. package/dist/feishu/CardRenderer.js +42 -11
  13. package/dist/feishu/CardRenderer.js.map +1 -1
  14. package/dist/feishu/FeishuMarkdown.js +70 -4
  15. package/dist/feishu/FeishuMarkdown.js.map +1 -1
  16. package/dist/feishu/FeishuTurnPresenter.js +2 -1
  17. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  18. package/dist/index.js +5 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/local-files/TurnPreviewPage.js +16 -1
  21. package/dist/local-files/TurnPreviewPage.js.map +1 -1
  22. package/dist/presentation/TurnStateReducer.js +37 -7
  23. package/dist/presentation/TurnStateReducer.js.map +1 -1
  24. package/dist/proxy/ProxySessionController.js +58 -38
  25. package/dist/proxy/ProxySessionController.js.map +1 -1
  26. package/dist/supervision/SafeRestartNotifier.js +2 -0
  27. package/dist/supervision/SafeRestartNotifier.js.map +1 -1
  28. package/dist/supervision/SafeRestartScheduler.js +7 -0
  29. package/dist/supervision/SafeRestartScheduler.js.map +1 -1
  30. package/npm-shrinkwrap.json +2 -2
  31. package/package.json +1 -1
  32. package/skills/agent-bot/SKILL.md +12 -0
  33. package/src/cli/SelfUpdateRunner.ts +2 -0
  34. package/src/codex/CodexEventMapper.ts +97 -3
  35. package/src/codex/CodexRuntime.ts +163 -13
  36. package/src/commands/CommandRouter.ts +13 -2
  37. package/src/commands/commandTypes.ts +2 -2
  38. package/src/feishu/CardRenderer.ts +48 -12
  39. package/src/feishu/FeishuMarkdown.ts +69 -3
  40. package/src/feishu/FeishuTurnPresenter.ts +1 -0
  41. package/src/index.ts +4 -0
  42. package/src/local-files/TurnPreviewPage.ts +16 -1
  43. package/src/presentation/TurnStateReducer.ts +49 -9
  44. package/src/presentation/turnViewTypes.ts +4 -0
  45. package/src/proxy/ProxySessionController.ts +61 -40
  46. package/src/runtime/types.ts +13 -1
  47. package/src/supervision/SafeRestartNotifier.ts +2 -0
  48. package/src/supervision/SafeRestartScheduler.ts +7 -0
@@ -8,6 +8,7 @@ import type {
8
8
  ConversationTurn,
9
9
  ForkRuntimeSessionInput,
10
10
  ModelOption,
11
+ ModelListResult,
11
12
  ModelProviderOption,
12
13
  PermissionMode,
13
14
  RemoteSessionActivity,
@@ -28,7 +29,7 @@ import type {
28
29
  import { appendGeneratedImageMarkdown } from "../utils/generatedImageMarkdown.js";
29
30
  import { normalizeTaskTitle } from "../utils/taskTitle.js";
30
31
  import { AppServerRequestError } from "./AppServerConnection.js";
31
- import { mapCodexNotification } from "./CodexEventMapper.js";
32
+ import { formatCodexError, mapCodexNotification } from "./CodexEventMapper.js";
32
33
  import { CodexLocalActivityDetector } from "./CodexLocalActivityDetector.js";
33
34
  import { detectProjectlessWorkspace } from "./ProjectlessWorkspace.js";
34
35
  import { threadWriterLockPath } from "./ThreadWriterProcess.js";
@@ -51,6 +52,7 @@ const FORK_SOURCE_TURN_PAGE_SIZE = 20;
51
52
  const FORK_SOURCE_REQUEST_TIMEOUT_MS = 15_000;
52
53
  const USER_RESUMABLE_THREAD_SOURCE_KINDS = ["cli", "vscode", "exec", "appServer"] as const;
53
54
  const BUILT_IN_CODEX_PROVIDER_ID = "openai";
55
+ const BUILT_IN_TRAEX_PROVIDER_ID = "trae";
54
56
  // A timed-out fork keeps running in App Server and can create an orphan thread.
55
57
  // Wait for its response; connection closure still rejects the request.
56
58
  const FORK_REQUEST_TIMEOUT_MS = 0;
@@ -79,9 +81,12 @@ export interface AppServerClientProvider {
79
81
  interface CodexSession extends RuntimeSession {
80
82
  activeTurnStartedAt?: number;
81
83
  terminalTurnIds: Set<string>;
84
+ modeChangeRequestIds: Set<string>;
82
85
  finalText: string;
83
86
  generatedImagePaths: string[];
84
87
  messagePhases: Map<string, "commentary" | "final_answer">;
88
+ unphasedMessages: Map<string, string>;
89
+ unphasedFinalMessageId?: string;
85
90
  needsResume: boolean;
86
91
  canReplaceEmptyThread: boolean;
87
92
  settingsRecoveryError?: string;
@@ -96,11 +101,20 @@ interface PendingApproval {
96
101
  resolve: (value: { decision: ApprovalDecision }) => void;
97
102
  }
98
103
 
104
+ interface PendingModeChange {
105
+ sessionId: string;
106
+ turnId: string;
107
+ requestId: string;
108
+ targetMode: "plan" | "default";
109
+ responding: boolean;
110
+ }
111
+
99
112
  export class CodexRuntime implements AgentRuntime {
100
113
  readonly kind = "codex" as const;
101
114
  private readonly sessions = new Map<string, CodexSession>();
102
115
  private readonly listeners = new Set<(event: RuntimeEvent) => void>();
103
116
  private readonly approvals = new Map<string, PendingApproval>();
117
+ private readonly modeChanges = new Map<string, PendingModeChange>();
104
118
  private attachedClient?: AppServerClient;
105
119
  private unsubscribe?: () => void;
106
120
  private readonly unsubscribeDisconnect?: () => void;
@@ -832,6 +846,34 @@ export class CodexRuntime implements AgentRuntime {
832
846
  requestId: string,
833
847
  decision: ApprovalDecision,
834
848
  ): Promise<void> {
849
+ const modeChange = this.modeChanges.get(requestId);
850
+ if (modeChange) {
851
+ if (modeChange.sessionId !== sessionId
852
+ || this.sessions.get(sessionId)?.activeTurnId !== modeChange.turnId) {
853
+ throw new Error("Approval request is no longer pending.");
854
+ }
855
+ if (decision !== "accept" && decision !== "decline" && decision !== "cancel") {
856
+ throw new Error("Invalid mode change decision.");
857
+ }
858
+ if (modeChange.responding) throw new Error("Approval response is already in progress.");
859
+ modeChange.responding = true;
860
+ try {
861
+ const client = await this.client();
862
+ if (this.modeChanges.get(requestId) !== modeChange) throw new Error("Approval request is no longer pending.");
863
+ await client.request("mode/change/respond", {
864
+ threadId: this.requireSession(sessionId).remoteSessionId,
865
+ requestId: modeChange.requestId,
866
+ decision: decision === "accept" ? "approved" : decision === "decline" ? "rejected" : "cancelled",
867
+ }, CONTROL_REQUEST_TIMEOUT_MS);
868
+ if (this.modeChanges.get(requestId) === modeChange) {
869
+ this.modeChanges.delete(requestId);
870
+ this.emit({ type: "approval_resolved", sessionId, turnId: modeChange.turnId, requestId, decision });
871
+ }
872
+ } finally {
873
+ modeChange.responding = false;
874
+ }
875
+ return;
876
+ }
835
877
  const pending = this.approvals.get(requestId);
836
878
  if (!pending || pending.sessionId !== sessionId) throw new Error("Approval request is no longer pending.");
837
879
  this.approvals.delete(requestId);
@@ -840,10 +882,16 @@ export class CodexRuntime implements AgentRuntime {
840
882
  }
841
883
 
842
884
  async listModels(modelProvider?: string, fallbackModel?: string): Promise<ModelOption[]> {
885
+ return (await this.listModelsWithStatus(modelProvider, fallbackModel)).models;
886
+ }
887
+
888
+ async listModelsWithStatus(modelProvider?: string, fallbackModel?: string): Promise<ModelListResult> {
843
889
  const requestedProvider = modelProvider?.trim();
890
+ const agentFamily = this.provider.getAgentFamily?.();
844
891
  if (!requestedProvider
845
- || (requestedProvider === BUILT_IN_CODEX_PROVIDER_ID && this.provider.getAgentFamily?.() !== "traex")) {
846
- return this.listCatalogModels();
892
+ || (requestedProvider === BUILT_IN_CODEX_PROVIDER_ID && agentFamily !== "traex")
893
+ || (requestedProvider === BUILT_IN_TRAEX_PROVIDER_ID && agentFamily === "traex")) {
894
+ return { models: await this.listCatalogModels() };
847
895
  }
848
896
  const config = await this.readConfig();
849
897
  const configuredProviders = isRecord(config.model_providers) ? config.model_providers : {};
@@ -859,19 +907,23 @@ export class CodexRuntime implements AgentRuntime {
859
907
  ? stringValue(config.model)?.trim()
860
908
  : undefined;
861
909
  try {
862
- return await fetchProviderModels({
910
+ const models = await fetchProviderModels({
863
911
  providerId: requestedProvider,
864
912
  config: (isRecord(providerConfig) ? providerConfig : {}) as ProviderModelConfig,
865
913
  catalog,
866
914
  configuredDefaultModel,
867
915
  environmentValue: (name) => this.provider.getEnvironmentVariable?.(name) ?? process.env[name],
868
916
  });
917
+ return { models };
869
918
  } catch (error) {
870
919
  const fallback = fallbackProviderModel(catalog, fallbackModel, configuredDefaultModel);
871
920
  if (!fallback) throw error;
872
921
  this.logger.warn({ error, modelProvider: requestedProvider, fallbackModel: fallback.id },
873
922
  "Provider model discovery failed; using the current or configured model as a fallback.");
874
- return [fallback];
923
+ return {
924
+ models: [fallback],
925
+ warning: `${error instanceof Error ? error.message : "Provider 模型列表读取失败。"} 暂时仅显示当前或已配置模型,服务可用性未确认。`,
926
+ };
875
927
  }
876
928
  }
877
929
 
@@ -971,6 +1023,7 @@ export class CodexRuntime implements AgentRuntime {
971
1023
  this.unsubscribeDisconnect?.();
972
1024
  for (const pending of this.approvals.values()) pending.resolve({ decision: "cancel" });
973
1025
  this.approvals.clear();
1026
+ this.modeChanges.clear();
974
1027
  this.sessionSyncs.clear();
975
1028
  this.sessions.clear();
976
1029
  this.provider.close();
@@ -1064,13 +1117,50 @@ export class CodexRuntime implements AgentRuntime {
1064
1117
  return;
1065
1118
  }
1066
1119
  const sessionId = session.localSessionId;
1067
- if (mapped.kind === "token_usage") {
1120
+ if (mapped.kind === "mode_change_requested") {
1121
+ const requestId = `mode:${mapped.threadId}:${mapped.requestId}`;
1122
+ if (session.modeChangeRequestIds.has(requestId)) return;
1123
+ session.modeChangeRequestIds.add(requestId);
1124
+ this.modeChanges.set(requestId, {
1125
+ sessionId, turnId: mapped.turnId, requestId: mapped.requestId,
1126
+ targetMode: mapped.targetMode, responding: false,
1127
+ });
1128
+ this.emit({
1129
+ type: "approval_requested", sessionId, turnId: mapped.turnId,
1130
+ request: {
1131
+ id: requestId,
1132
+ kind: "mode_change",
1133
+ title: mapped.targetMode === "default" ? "确认方案并开始执行" : "确认进入规划模式",
1134
+ reason: [mapped.reason, ...(mapped.allowedPrompts ?? []).map((entry) => `- ${entry.tool}: ${entry.prompt}`)]
1135
+ .filter(Boolean).join("\n"),
1136
+ options: [
1137
+ { id: "accept", label: mapped.targetMode === "default" ? "Approve Plan" : "Enter Plan Mode" },
1138
+ { id: "decline", label: "Reject" },
1139
+ { id: "cancel", label: "Cancel Request" },
1140
+ ],
1141
+ },
1142
+ });
1143
+ } else if (mapped.kind === "mode_change_resolved") {
1144
+ const requestId = `mode:${mapped.threadId}:${mapped.requestId}`;
1145
+ const pending = this.modeChanges.get(requestId);
1146
+ if (!pending || pending.sessionId !== sessionId || pending.turnId !== mapped.turnId
1147
+ || pending.targetMode !== mapped.targetMode) return;
1148
+ this.modeChanges.delete(requestId);
1149
+ this.emit({
1150
+ type: "approval_resolved", sessionId, turnId: mapped.turnId, requestId,
1151
+ decision: mapped.decision === "approved" ? "accept" : mapped.decision === "rejected" ? "decline" : "cancel",
1152
+ });
1153
+ } else if (mapped.kind === "token_usage") {
1068
1154
  this.emit({
1069
1155
  type: "token_usage_updated",
1070
1156
  sessionId,
1071
1157
  turnId: mapped.turnId,
1072
1158
  lastTokens: mapped.lastTokens,
1073
1159
  cumulativeTokens: mapped.cumulativeTokens,
1160
+ lastTotalTokens: mapped.lastTotalTokens,
1161
+ cumulativeTotalTokens: mapped.cumulativeTotalTokens,
1162
+ lastCachedTokens: mapped.lastCachedTokens,
1163
+ cumulativeCachedTokens: mapped.cumulativeCachedTokens,
1074
1164
  contextTokens: mapped.contextTokens,
1075
1165
  });
1076
1166
  } else if (mapped.kind === "context_compaction") {
@@ -1086,8 +1176,16 @@ export class CodexRuntime implements AgentRuntime {
1086
1176
  });
1087
1177
  } else if (mapped.kind === "agent_message_phase") {
1088
1178
  session.messagePhases.set(mapped.itemId, mapped.phase);
1179
+ if (mapped.phase === "final_answer") this.promoteUnphasedMessage(session, mapped.turnId, mapped.itemId);
1180
+ else if (session.unphasedFinalMessageId === mapped.itemId) session.unphasedFinalMessageId = undefined;
1089
1181
  } else if (mapped.kind === "agent_delta") {
1090
- if (session.messagePhases.get(mapped.itemId) === "commentary") {
1182
+ const phase = session.messagePhases.get(mapped.itemId);
1183
+ if (phase !== "final_answer") {
1184
+ // Providers may omit phase. Keep their text in sequence until a tool or completion disambiguates it.
1185
+ if (phase === undefined) {
1186
+ session.unphasedMessages.set(mapped.itemId, (session.unphasedMessages.get(mapped.itemId) ?? "") + mapped.text);
1187
+ session.unphasedFinalMessageId = mapped.itemId;
1188
+ }
1091
1189
  this.emit({
1092
1190
  type: "progress",
1093
1191
  sessionId,
@@ -1100,6 +1198,19 @@ export class CodexRuntime implements AgentRuntime {
1100
1198
  session.finalText += mapped.text;
1101
1199
  this.emit({ type: "agent_text_delta", sessionId, turnId: mapped.turnId, text: mapped.text });
1102
1200
  }
1201
+ } else if (mapped.kind === "runtime_error") {
1202
+ this.logger.warn({ sessionId, turnId: mapped.turnId, willRetry: mapped.willRetry, error: mapped.message },
1203
+ "App Server reported a turn error.");
1204
+ // Only turn/completed terminates the turn; native retries keep the same progress card.
1205
+ this.emit({
1206
+ type: "progress",
1207
+ sessionId,
1208
+ turnId: mapped.turnId,
1209
+ activityId: `commentary:runtime-error:${mapped.turnId}`,
1210
+ text: `${mapped.willRetry ? "运行请求出错,Agent 正在重试" : "运行请求失败,等待 Agent 返回最终状态"}:${mapped.message}`,
1211
+ append: false,
1212
+ severity: "warning",
1213
+ });
1103
1214
  } else if (mapped.kind === "progress") {
1104
1215
  this.emit({
1105
1216
  type: "progress",
@@ -1112,6 +1223,7 @@ export class CodexRuntime implements AgentRuntime {
1112
1223
  } else if (mapped.kind === "plan") {
1113
1224
  this.emit({ type: "plan_updated", sessionId, turnId: mapped.turnId, steps: mapped.steps });
1114
1225
  } else if (mapped.kind === "tool") {
1226
+ if (mapped.phase === "started") session.unphasedFinalMessageId = undefined;
1115
1227
  if (
1116
1228
  mapped.phase === "updated"
1117
1229
  && mapped.tool.kind === "image_generation"
@@ -1135,6 +1247,11 @@ export class CodexRuntime implements AgentRuntime {
1135
1247
  delta: mapped.delta,
1136
1248
  });
1137
1249
  } else if (mapped.kind === "terminal") {
1250
+ if (mapped.status === "completed" && !session.finalText && session.unphasedFinalMessageId) {
1251
+ this.promoteUnphasedMessage(session, mapped.turnId, session.unphasedFinalMessageId);
1252
+ }
1253
+ session.unphasedMessages.clear();
1254
+ session.unphasedFinalMessageId = undefined;
1138
1255
  session.activeTurnId = undefined;
1139
1256
  session.activeTurnStartedAt = undefined;
1140
1257
  session.terminalTurnIds.add(mapped.turnId);
@@ -1191,6 +1308,18 @@ export class CodexRuntime implements AgentRuntime {
1191
1308
  return response;
1192
1309
  }
1193
1310
 
1311
+ private promoteUnphasedMessage(session: CodexSession, turnId: string, itemId: string): void {
1312
+ const text = session.unphasedMessages.get(itemId);
1313
+ if (text === undefined) return;
1314
+ session.unphasedMessages.delete(itemId);
1315
+ if (session.unphasedFinalMessageId === itemId) session.unphasedFinalMessageId = undefined;
1316
+ session.finalText += text;
1317
+ this.emit({
1318
+ type: "agent_text_delta", sessionId: session.localSessionId, turnId, text,
1319
+ replacesActivityId: `commentary:${itemId}`,
1320
+ });
1321
+ }
1322
+
1194
1323
  private async resolveReasoningEffort(
1195
1324
  input: CreateRuntimeSessionInput,
1196
1325
  response: ThreadResponse,
@@ -1290,6 +1419,9 @@ export class CodexRuntime implements AgentRuntime {
1290
1419
  }
1291
1420
  session.activeTurnId = turnId;
1292
1421
  session.activeTurnStartedAt = startedAt;
1422
+ session.modeChangeRequestIds.clear();
1423
+ session.unphasedMessages.clear();
1424
+ session.unphasedFinalMessageId = undefined;
1293
1425
  session.finalText = "";
1294
1426
  session.generatedImagePaths = [];
1295
1427
  session.messagePhases.clear();
@@ -1308,6 +1440,13 @@ export class CodexRuntime implements AgentRuntime {
1308
1440
 
1309
1441
  private finishSnapshotTurn(session: CodexSession, turn: CodexTurnSnapshot): void {
1310
1442
  if (session.activeTurnId !== turn.id) return;
1443
+ if (turn.status === "completed") {
1444
+ for (const item of finalResponseMessages(turn)) {
1445
+ if (item.id) this.promoteUnphasedMessage(session, turn.id, item.id);
1446
+ }
1447
+ }
1448
+ session.unphasedMessages.clear();
1449
+ session.unphasedFinalMessageId = undefined;
1311
1450
  session.activeTurnId = undefined;
1312
1451
  session.activeTurnStartedAt = undefined;
1313
1452
  session.terminalTurnIds.add(turn.id);
@@ -1321,7 +1460,7 @@ export class CodexRuntime implements AgentRuntime {
1321
1460
  type: "turn_failed",
1322
1461
  sessionId: session.localSessionId,
1323
1462
  turnId: turn.id,
1324
- message: turn.error?.message ?? "App Server turn failed.",
1463
+ message: formatCodexError(turn.error) ?? "App Server turn failed.",
1325
1464
  });
1326
1465
  return;
1327
1466
  }
@@ -1375,9 +1514,11 @@ export class CodexRuntime implements AgentRuntime {
1375
1514
  ? [input.lastTurnId]
1376
1515
  : [],
1377
1516
  ),
1517
+ modeChangeRequestIds: new Set(),
1378
1518
  finalText: "",
1379
1519
  generatedImagePaths: [],
1380
1520
  messagePhases: new Map(),
1521
+ unphasedMessages: new Map(),
1381
1522
  needsResume: false,
1382
1523
  canReplaceEmptyThread: !("remoteSessionId" in input),
1383
1524
  rolloutPath: stringValue(response.thread.path)?.trim() || undefined,
@@ -1410,6 +1551,11 @@ export class CodexRuntime implements AgentRuntime {
1410
1551
  }
1411
1552
 
1412
1553
  private emit(event: RuntimeEvent): void {
1554
+ if (event.type === "turn_completed" || event.type === "turn_cancelled" || event.type === "turn_failed") {
1555
+ for (const [requestId, pending] of this.modeChanges) {
1556
+ if (pending.sessionId === event.sessionId && pending.turnId === event.turnId) this.modeChanges.delete(requestId);
1557
+ }
1558
+ }
1413
1559
  for (const listener of this.listeners) listener(event);
1414
1560
  }
1415
1561
 
@@ -1417,6 +1563,7 @@ export class CodexRuntime implements AgentRuntime {
1417
1563
  this.unsubscribe?.();
1418
1564
  this.unsubscribe = undefined;
1419
1565
  this.attachedClient = undefined;
1566
+ this.modeChanges.clear();
1420
1567
  for (const session of this.sessions.values()) {
1421
1568
  session.needsResume = true;
1422
1569
  const turnId = session.activeTurnId;
@@ -1481,6 +1628,7 @@ interface CodexTurnSnapshot {
1481
1628
  id: string;
1482
1629
  status: "completed" | "interrupted" | "failed" | "inProgress";
1483
1630
  items?: Array<{
1631
+ id?: string;
1484
1632
  type?: string;
1485
1633
  text?: string;
1486
1634
  content?: Array<{
@@ -1491,7 +1639,7 @@ interface CodexTurnSnapshot {
1491
1639
  status?: string;
1492
1640
  savedPath?: string;
1493
1641
  }>;
1494
- error?: { message?: string } | null;
1642
+ error?: { message?: string; additionalDetails?: string | null } | null;
1495
1643
  startedAt?: number | null;
1496
1644
  durationMs?: number | null;
1497
1645
  }
@@ -1575,13 +1723,15 @@ function turnStartedAt(turn: CodexTurnSnapshot): number {
1575
1723
  }
1576
1724
 
1577
1725
  function extractFinalResponse(turn: CodexTurnSnapshot): string {
1726
+ return finalResponseMessages(turn).map((item) => item.text!.trim()).join("\n\n");
1727
+ }
1728
+
1729
+ function finalResponseMessages(turn: CodexTurnSnapshot) {
1578
1730
  const messages = (turn.items ?? []).filter(
1579
1731
  (item) => item.type === "agentMessage" && typeof item.text === "string" && item.text.trim(),
1580
1732
  );
1581
1733
  const finalMessages = messages.filter((item) => item.phase === "final_answer");
1582
- return (finalMessages.length ? finalMessages : messages.slice(-1))
1583
- .map((item) => item.text!.trim())
1584
- .join("\n\n");
1734
+ return finalMessages.length ? finalMessages : messages.filter((item) => item.phase !== "commentary").slice(-1);
1585
1735
  }
1586
1736
 
1587
1737
  function extractGeneratedImagePaths(turn: CodexTurnSnapshot): string[] {
@@ -1648,7 +1798,7 @@ function remoteSessionSummary(thread: CodexThreadSnapshot): RemoteSessionSummary
1648
1798
  finalResponse: lastTurn && lastTurn.status !== "inProgress"
1649
1799
  ? appendGeneratedImageMarkdown(extractFinalResponse(lastTurn), extractGeneratedImagePaths(lastTurn)) || undefined
1650
1800
  : undefined,
1651
- lastError: lastTurn?.error?.message,
1801
+ lastError: formatCodexError(lastTurn?.error),
1652
1802
  lastTurnToolCount: toolCounts?.total,
1653
1803
  lastTurnCompletedToolCount: toolCounts?.completed,
1654
1804
  lastTurnFailedToolCount: toolCounts?.failed,
@@ -196,6 +196,7 @@ function parseNewCommand(args: string[]): Extract<Command, { type: "new" }> {
196
196
  title: options.title,
197
197
  cwd: options.cwd,
198
198
  ...(options.projectless ? { projectless: true } : {}),
199
+ ...(options.agentName ? { agentName: options.agentName } : {}),
199
200
  };
200
201
  }
201
202
 
@@ -206,6 +207,7 @@ function parseNewGroupCommand(args: string[]): Extract<Command, { type: "newgrou
206
207
  title: options.title,
207
208
  ...(options.cwd === undefined ? {} : { cwd: options.cwd }),
208
209
  ...(options.projectless ? { projectless: true } : {}),
210
+ ...(options.agentName ? { agentName: options.agentName } : {}),
209
211
  };
210
212
  }
211
213
 
@@ -214,9 +216,10 @@ function parseNewTaskOptions(
214
216
  args: string[],
215
217
  directoryLabel: string,
216
218
  exampleDirectory: string,
217
- ): { title?: string; cwd?: string; projectless: boolean } {
219
+ ): { title?: string; cwd?: string; projectless: boolean; agentName?: string } {
218
220
  const titleParts: string[] = [];
219
221
  let cwd: string | undefined;
222
+ let agentName: string | undefined;
220
223
  let projectless = false;
221
224
  for (let index = 0; index < args.length; index += 1) {
222
225
  const argument = args[index]!;
@@ -226,6 +229,13 @@ function parseNewTaskOptions(
226
229
  projectless = true;
227
230
  continue;
228
231
  }
232
+ if (argument === "--agent") {
233
+ if (agentName !== undefined) throw new Error(`${commandName} 只能指定一次 --agent。`);
234
+ const name = args[++index];
235
+ if (!name?.trim() || name.startsWith("--")) throw new Error("请在 --agent 后指定 Agent 名称。");
236
+ agentName = name.trim();
237
+ continue;
238
+ }
229
239
  if (argument !== "--dir") {
230
240
  titleParts.push(argument);
231
241
  continue;
@@ -233,7 +243,7 @@ function parseNewTaskOptions(
233
243
  if (projectless) throw new Error(`${commandName} 的 --dir 和 --nodir 不能同时使用。`);
234
244
  if (cwd !== undefined) throw new Error(`${commandName} 只能指定一次 --dir。`);
235
245
  const directory = args[index + 1];
236
- if (!directory || directory === "--dir" || directory === "--nodir") {
246
+ if (!directory || directory.startsWith("--")) {
237
247
  throw new Error(
238
248
  `请在 --dir 后指定${directoryLabel},例如:${commandName} 修复会话列表 --dir ${exampleDirectory}。`,
239
249
  );
@@ -245,6 +255,7 @@ function parseNewTaskOptions(
245
255
  title: titleParts.join(" ").trim() || undefined,
246
256
  cwd,
247
257
  projectless,
258
+ agentName,
248
259
  };
249
260
  }
250
261
 
@@ -1,7 +1,7 @@
1
1
  export type Command =
2
2
  | { type: "shell"; command: string }
3
- | { type: "new"; title?: string; cwd?: string; projectless?: boolean }
4
- | { type: "newgroup"; title?: string; cwd?: string; projectless?: boolean }
3
+ | { type: "new"; title?: string; cwd?: string; projectless?: boolean; agentName?: string }
4
+ | { type: "newgroup"; title?: string; cwd?: string; projectless?: boolean; agentName?: string }
5
5
  | { type: "dir"; directory?: string }
6
6
  | { type: "file"; filePath: string }
7
7
  | { type: "clone" | "clonegroup"; title?: string; agentName?: string }
@@ -64,6 +64,8 @@ export interface SafeRestartStatusView {
64
64
  pendingFinalDeliveries: number;
65
65
  waitingTasks: Array<{
66
66
  id: string;
67
+ localSessionId: string;
68
+ isRunning: boolean;
67
69
  title?: string;
68
70
  }>;
69
71
  }
@@ -88,6 +90,7 @@ export interface ModelSelectorCardView {
88
90
  permissionMode?: PermissionMode;
89
91
  unifiedSettings?: boolean;
90
92
  notice?: string;
93
+ warning?: string;
91
94
  }
92
95
 
93
96
  export interface ReasoningSelectorCardView {
@@ -100,6 +103,7 @@ export interface ReasoningSelectorCardView {
100
103
  permissionMode?: PermissionMode;
101
104
  unifiedSettings?: boolean;
102
105
  notice?: string;
106
+ warning?: string;
103
107
  }
104
108
 
105
109
  export interface ProviderSelectorCardView {
@@ -154,6 +158,7 @@ export interface ExecutionSettingsCardView {
154
158
  models: ModelOption[];
155
159
  reasoningOptions: ReasoningEffortOption[];
156
160
  notice?: string;
161
+ warning?: string;
157
162
  }
158
163
 
159
164
  export interface CardSection {
@@ -545,6 +550,7 @@ export class CardRenderer {
545
550
  ]
546
551
  : []),
547
552
  ...(view.notice ? [view.notice] : []),
553
+ ...(view.warning ? [`**提示**:${escapeCardHtml(view.warning)}`] : []),
548
554
  ].join("\n")),
549
555
  ...(tabRow ? [tabRow] : []),
550
556
  { tag: "hr" },
@@ -639,7 +645,7 @@ export class CardRenderer {
639
645
  }
640
646
 
641
647
  elements.push(markdown(`> ${EXECUTION_SETTING_EFFECTS[view.activeTab]}`));
642
- return sectionCard("运行设置", elements, view.notice ? "green" : "blue");
648
+ return sectionCard("运行设置", elements, view.warning ? "orange" : view.notice ? "green" : "blue");
643
649
  }
644
650
 
645
651
  renderProviderSelector(view: ProviderSelectorCardView): Record<string, unknown> {
@@ -700,6 +706,7 @@ export class CardRenderer {
700
706
  `**模型**:${inlineCode(view.model)}`,
701
707
  `**当前思考模式**:${inlineCode(view.currentEffort ?? "默认")}`,
702
708
  ...(view.notice ? [view.notice] : []),
709
+ ...(view.warning ? [`**提示**:${escapeCardHtml(view.warning)}`] : []),
703
710
  ].join("\n")),
704
711
  { tag: "hr" },
705
712
  ];
@@ -757,7 +764,7 @@ export class CardRenderer {
757
764
  },
758
765
  }]),
759
766
  );
760
- return sectionCard("思考模式", elements, view.notice ? "green" : "blue");
767
+ return sectionCard("思考模式", elements, view.warning ? "orange" : view.notice ? "green" : "blue");
761
768
  }
762
769
 
763
770
  renderModelSelector(view: ModelSelectorCardView): Record<string, unknown> {
@@ -767,6 +774,7 @@ export class CardRenderer {
767
774
  `**当前模型**:${inlineCode(view.currentModel ?? "默认")}`,
768
775
  `**思考强度**:${inlineCode(view.reasoningEffort ?? "默认")}`,
769
776
  ...(view.notice ? [view.notice] : []),
777
+ ...(view.warning ? [`**提示**:${escapeCardHtml(view.warning)}`] : []),
770
778
  ].join("\n")),
771
779
  { tag: "hr" },
772
780
  ];
@@ -825,7 +833,8 @@ export class CardRenderer {
825
833
  }]),
826
834
  );
827
835
  }
828
- return sectionCard(view.unifiedSettings ? "Provider 设置 · 模型" : "模型", elements, view.notice ? "green" : "blue");
836
+ return sectionCard(view.unifiedSettings ? "Provider 设置 · 模型" : "模型", elements,
837
+ view.warning ? "orange" : view.notice ? "green" : "blue");
829
838
  }
830
839
 
831
840
  renderPermissionSelector(view: PermissionSelectorCardView): Record<string, unknown> {
@@ -982,6 +991,7 @@ export class CardRenderer {
982
991
  }
983
992
 
984
993
  renderSafeRestartStatus(view: SafeRestartStatusView): Record<string, unknown> {
994
+ const actionable = view.phase !== "restarting" && view.phase !== "cancelled" && view.phase !== "superseded";
985
995
  const countdown = view.phase === "countdown"
986
996
  ? `${Math.max(0, Math.ceil((view.remainingMs ?? 0) / 1_000))}s`
987
997
  : view.phase === "restarting"
@@ -1011,16 +1021,34 @@ export class CardRenderer {
1011
1021
  const elements: Record<string, unknown>[] = [markdown(lines.join("\n"))];
1012
1022
  if (view.waitingTasks.length > 0) {
1013
1023
  const visible = view.waitingTasks.slice(0, 10);
1014
- const taskLines = visible.map((task, index) =>
1015
- `${index + 1}. ${task.title ? `${inlineCode(truncateText(task.title, 80))} · ` : ""}${inlineCode(task.id)}`);
1024
+ elements.push({ tag: "hr" }, markdown(`**当前等待的任务(${view.waitingTasks.length})**`));
1025
+ for (const [index, task] of visible.entries()) {
1026
+ const label = `${index + 1}. ${task.title ? `${inlineCode(truncateText(task.title, 80))} · ` : ""}${inlineCode(task.id)}`;
1027
+ elements.push({
1028
+ tag: "column_set",
1029
+ flex_mode: "none",
1030
+ horizontal_spacing: "8px",
1031
+ columns: [
1032
+ { tag: "column", width: "weighted", weight: 1, elements: [markdown(label)] },
1033
+ ...(actionable && task.isRunning ? [{
1034
+ tag: "column",
1035
+ width: "auto",
1036
+ elements: [taskActionElement({
1037
+ text: "Stop",
1038
+ type: "danger",
1039
+ value: { action: "session_stop", sessionId: task.localSessionId, cardView: "safe_restart" },
1040
+ })],
1041
+ }] : []),
1042
+ ],
1043
+ });
1044
+ }
1016
1045
  if (view.waitingTasks.length > visible.length) {
1017
- taskLines.push(`… 还有 ${view.waitingTasks.length - visible.length} 个任务`);
1046
+ elements.push(markdown(`… 还有 ${view.waitingTasks.length - visible.length} 个任务`));
1018
1047
  }
1019
- elements.push({ tag: "hr" }, markdown(`**当前等待的任务(${view.waitingTasks.length})**\n${taskLines.join("\n")}`));
1020
1048
  } else {
1021
1049
  elements.push({ tag: "hr" }, markdown("**当前等待的任务**:无"));
1022
1050
  }
1023
- if (view.phase !== "restarting" && view.phase !== "cancelled" && view.phase !== "superseded") {
1051
+ if (actionable) {
1024
1052
  elements.push(
1025
1053
  { tag: "hr" },
1026
1054
  taskActionRow([{
@@ -1029,6 +1057,12 @@ export class CardRenderer {
1029
1057
  action: "safe_restart_cancel",
1030
1058
  scheduleId: String(view.scheduleId),
1031
1059
  },
1060
+ }, {
1061
+ text: "ForceRestart",
1062
+ value: {
1063
+ action: "safe_restart_force",
1064
+ scheduleId: String(view.scheduleId),
1065
+ },
1032
1066
  }]),
1033
1067
  );
1034
1068
  }
@@ -1665,13 +1699,12 @@ function renderTurnElements(
1665
1699
  tag: "column_set",
1666
1700
  flex_mode: "flow",
1667
1701
  horizontal_spacing: "8px",
1668
- vertical_spacing: "8px",
1669
1702
  columns: request.options.map((option) => ({
1670
1703
  tag: "column",
1671
1704
  width: "auto",
1672
1705
  elements: [{
1673
1706
  tag: "button",
1674
- text: { tag: "plain_text", content: approvalDecisionLabel(option.id) },
1707
+ text: { tag: "plain_text", content: request.kind === "mode_change" ? option.label : approvalDecisionLabel(option.id) },
1675
1708
  type: option.id === "accept" || option.id === "acceptForSession" ? "primary" : option.id === "cancel" ? "danger" : "default",
1676
1709
  behaviors: [{
1677
1710
  type: "callback",
@@ -1735,13 +1768,12 @@ function renderGroupedTurnElements(
1735
1768
  tag: "column_set",
1736
1769
  flex_mode: "flow",
1737
1770
  horizontal_spacing: "8px",
1738
- vertical_spacing: "8px",
1739
1771
  columns: request.options.map((option) => ({
1740
1772
  tag: "column",
1741
1773
  width: "auto",
1742
1774
  elements: [{
1743
1775
  tag: "button",
1744
- text: { tag: "plain_text", content: approvalDecisionLabel(option.id) },
1776
+ text: { tag: "plain_text", content: request.kind === "mode_change" ? option.label : approvalDecisionLabel(option.id) },
1745
1777
  type: option.id === "accept" || option.id === "acceptForSession" ? "primary" : option.id === "cancel" ? "danger" : "default",
1746
1778
  behaviors: [{
1747
1779
  type: "callback",
@@ -1815,6 +1847,10 @@ function renderActivity(
1815
1847
  if (activity.kind === "assistant" || (activity.kind === "reasoning" && activity.id.startsWith("commentary:"))) {
1816
1848
  const text = activity.text.trim();
1817
1849
  if (!text) return [];
1850
+ if (activity.id.startsWith("commentary:runtime-error:")) {
1851
+ const chunks = fullAssistantText ? splitText(text, ACTIVITY_TEXT_CHUNK) : [text];
1852
+ return chunks.map((chunk) => markdown(codeBlock(chunk, fullAssistantText ? ACTIVITY_TEXT_CHUNK : MAX_LIVE_ASSISTANT_TEXT)));
1853
+ }
1818
1854
  return fullAssistantText
1819
1855
  ? splitText(text, ACTIVITY_TEXT_CHUNK).map(markdown)
1820
1856
  : [markdown(truncateText(text, MAX_LIVE_ASSISTANT_TEXT))];