@keyou007/agent-bot 0.1.10 → 0.1.11

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 (68) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +16 -10
  3. package/README.zh.md +16 -10
  4. package/assets/agent-bot-logo.png +0 -0
  5. package/dist/acp/AcpSessionManager.js +78 -46
  6. package/dist/acp/AcpSessionManager.js.map +1 -1
  7. package/dist/codex/AppServerConnection.js +15 -1
  8. package/dist/codex/AppServerConnection.js.map +1 -1
  9. package/dist/codex/CodexLocalActivityDetector.js +4 -1
  10. package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
  11. package/dist/codex/CodexProcessManager.js +1 -0
  12. package/dist/codex/CodexProcessManager.js.map +1 -1
  13. package/dist/codex/CodexRuntime.js +77 -2
  14. package/dist/codex/CodexRuntime.js.map +1 -1
  15. package/dist/commands/CommandRouter.js +64 -49
  16. package/dist/commands/CommandRouter.js.map +1 -1
  17. package/dist/console/ConsoleTurnPresenter.js +1 -1
  18. package/dist/console/ConsoleTurnPresenter.js.map +1 -1
  19. package/dist/feishu/CardRenderer.js +345 -11
  20. package/dist/feishu/CardRenderer.js.map +1 -1
  21. package/dist/feishu/FeishuMessageClient.js +51 -5
  22. package/dist/feishu/FeishuMessageClient.js.map +1 -1
  23. package/dist/feishu/FeishuTurnPresenter.js +8 -3
  24. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  25. package/dist/index.js +18 -17
  26. package/dist/index.js.map +1 -1
  27. package/dist/proxy/ProxySessionController.js +589 -205
  28. package/dist/proxy/ProxySessionController.js.map +1 -1
  29. package/dist/runtime/AcpRuntimeAdapter.js +1 -0
  30. package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
  31. package/dist/runtime/AgentRuntimeRegistry.js +7 -4
  32. package/dist/runtime/AgentRuntimeRegistry.js.map +1 -1
  33. package/dist/runtime/createAgentRuntimeRegistry.js +22 -0
  34. package/dist/runtime/createAgentRuntimeRegistry.js.map +1 -0
  35. package/dist/startup/SessionMetadataHydrator.js +2 -2
  36. package/dist/startup/SessionMetadataHydrator.js.map +1 -1
  37. package/dist/startup/StartupNotifier.js +1 -0
  38. package/dist/startup/StartupNotifier.js.map +1 -1
  39. package/dist/state/StateStore.js +21 -15
  40. package/dist/state/StateStore.js.map +1 -1
  41. package/dist/supervision/SafeRestartScheduler.js +5 -0
  42. package/dist/supervision/SafeRestartScheduler.js.map +1 -1
  43. package/docs/technical-reference.md +15 -5
  44. package/docs/technical-reference.zh.md +15 -5
  45. package/npm-shrinkwrap.json +2 -2
  46. package/package.json +2 -1
  47. package/skills/agent-bot/SKILL.md +13 -3
  48. package/src/acp/AcpSessionManager.ts +73 -54
  49. package/src/codex/AppServerConnection.ts +21 -1
  50. package/src/codex/CodexLocalActivityDetector.ts +8 -1
  51. package/src/codex/CodexProcessManager.ts +1 -0
  52. package/src/codex/CodexRuntime.ts +91 -2
  53. package/src/commands/CommandRouter.ts +71 -49
  54. package/src/commands/commandTypes.ts +6 -11
  55. package/src/console/ConsoleTurnPresenter.ts +1 -1
  56. package/src/feishu/CardRenderer.ts +418 -11
  57. package/src/feishu/FeishuMessageClient.ts +66 -3
  58. package/src/feishu/FeishuTurnPresenter.ts +7 -2
  59. package/src/index.ts +21 -22
  60. package/src/proxy/ProxySessionController.ts +706 -208
  61. package/src/runtime/AcpRuntimeAdapter.ts +1 -0
  62. package/src/runtime/AgentRuntimeRegistry.ts +7 -6
  63. package/src/runtime/createAgentRuntimeRegistry.ts +25 -0
  64. package/src/runtime/types.ts +18 -0
  65. package/src/startup/SessionMetadataHydrator.ts +2 -2
  66. package/src/startup/StartupNotifier.ts +1 -0
  67. package/src/state/StateStore.ts +29 -15
  68. package/src/supervision/SafeRestartScheduler.ts +5 -0
@@ -2,6 +2,7 @@ import path from "node:path";
2
2
  import os from "node:os";
3
3
  import type { Logger } from "pino";
4
4
  import { createProjectlessWorkspace, detectProjectlessWorkspace } from "../codex/ProjectlessWorkspace.js";
5
+ import { resolveUserPath } from "../config/paths.js";
5
6
  import type { AppConfig } from "../config/schema.js";
6
7
  import { CommandRouter } from "../commands/CommandRouter.js";
7
8
  import type { Command } from "../commands/commandTypes.js";
@@ -12,7 +13,12 @@ import type {
12
13
  IncomingMessage,
13
14
  MessageReplyTarget,
14
15
  } from "../feishu/types.js";
15
- import { CardRenderer, type CardSection, type TaskListCardAction } from "../feishu/CardRenderer.js";
16
+ import {
17
+ CardRenderer,
18
+ type CardSection,
19
+ type ExecutionSettingsTab,
20
+ type TaskListCardAction,
21
+ } from "../feishu/CardRenderer.js";
16
22
  import { generateGroupAvatarPng, resolveGroupAvatarProjectName } from "../feishu/GroupAvatarGenerator.js";
17
23
  import type { OutboundRouter } from "../presentation/OutboundRouter.js";
18
24
  import type { TurnActivity, TurnViewState } from "../presentation/turnViewTypes.js";
@@ -55,6 +61,18 @@ const MESSAGE_COMPLETED_REACTION = "DONE";
55
61
  const MESSAGE_FAILED_REACTION = "ERROR";
56
62
  const MESSAGE_CANCELLED_REACTION = "CrossMark";
57
63
  const SESSION_PAGE_SIZE = 5;
64
+ const REMOTE_SESSION_REFERENCE_PREFIX = "agent-runtime:";
65
+
66
+ interface AgentRemoteSession {
67
+ agentName: string;
68
+ runtime: AgentRuntime;
69
+ remote: RemoteSessionSummary;
70
+ }
71
+
72
+ interface AgentRemoteSessionSummary {
73
+ agentName: string;
74
+ session: RemoteSessionSummary;
75
+ }
58
76
 
59
77
  interface SessionsCardOptions {
60
78
  updateMessageId?: string;
@@ -76,7 +94,12 @@ interface ThinkingCardOptions extends ModelCardOptions {
76
94
  expectedModel?: string;
77
95
  }
78
96
 
97
+ interface ExecutionSettingsCardOptions extends ModelCardOptions {
98
+ notice?: string;
99
+ }
100
+
79
101
  interface SessionExecutionSettings {
102
+ modelProvider?: string;
80
103
  model?: string;
81
104
  reasoningEffort?: string;
82
105
  permissionMode?: PermissionMode;
@@ -99,6 +122,7 @@ interface ForkSessionPlan {
99
122
  lastTurnId: string;
100
123
  cwd: string;
101
124
  forkTitle: string;
125
+ modelProvider?: string;
102
126
  model?: string;
103
127
  reasoningEffort?: string;
104
128
  permissionMode: PermissionMode;
@@ -130,7 +154,7 @@ interface CreatedFeishuGroupContext {
130
154
 
131
155
  export interface ProxyLifecycle {
132
156
  supervised?: boolean;
133
- restart(contextKey: string): Promise<void>;
157
+ restart(contextKey: string, force: boolean): Promise<void>;
134
158
  cancelSafeRestart?(scheduleId: number): Promise<boolean>;
135
159
  }
136
160
 
@@ -168,9 +192,9 @@ export class ProxySessionController {
168
192
  "Marked interrupted ACP sessions as failed after process restart.",
169
193
  );
170
194
  }
171
- for (const kind of ["acp", "codex"] as const) {
195
+ for (const [, runtime] of this.runtimes.entries()) {
172
196
  this.unsubscribe.push(
173
- this.runtimes.get(kind).onEvent((event) => {
197
+ runtime.onEvent((event) => {
174
198
  void this.handleRuntimeEvent(event).catch((error: unknown) => {
175
199
  this.logger.warn({ error, event }, "Failed to present runtime event.");
176
200
  });
@@ -236,7 +260,7 @@ export class ProxySessionController {
236
260
  let command: Command;
237
261
  try {
238
262
  command = imageCount > 0 && !message.text.trimStart().startsWith("/")
239
- ? { type: "prompt", text: message.text.trim() || "请分析这张图片。" }
263
+ ? { type: "prompt", text: message.text.trim() || "请查看这张图片" }
240
264
  : this.router.parse(message.text);
241
265
  } catch (error) {
242
266
  await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
@@ -327,7 +351,7 @@ export class ProxySessionController {
327
351
  contextKey,
328
352
  String(scopedAction.value.turnId ?? ""),
329
353
  requestedPage === "latest" ? "latest" : Number.isFinite(numericPage) ? numericPage : 0,
330
- scopedAction.messageId,
354
+ requiredCardMessageId(scopedAction.messageId),
331
355
  );
332
356
  } else if (kind === "turn_cancel") {
333
357
  const sessionId = String(scopedAction.value.sessionId ?? "");
@@ -378,6 +402,37 @@ export class ProxySessionController {
378
402
  await this.status(contextKey, String(scopedAction.value.sessionId ?? ""));
379
403
  } else if (kind === "session_status_refresh") {
380
404
  await this.refreshStatusCardFromAction(scopedAction);
405
+ } else if (kind === "settings_tab_open") {
406
+ await this.openExecutionSettings(contextKey, executionSettingsTabValue(scopedAction.value.tab), {
407
+ sessionId: String(scopedAction.value.sessionId ?? ""),
408
+ updateMessageId: scopedAction.messageId,
409
+ });
410
+ } else if (kind === "settings_agent_select") {
411
+ await this.setDefaultAgent(contextKey, String(scopedAction.value.agent ?? ""), {
412
+ sessionId: String(scopedAction.value.sessionId ?? ""),
413
+ updateMessageId: scopedAction.messageId,
414
+ });
415
+ } else if (kind === "settings_provider_select") {
416
+ await this.selectProvider(contextKey, String(scopedAction.value.provider ?? ""), {
417
+ sessionId: String(scopedAction.value.sessionId ?? ""),
418
+ updateMessageId: scopedAction.messageId,
419
+ });
420
+ } else if (kind === "settings_model_select") {
421
+ await this.model(contextKey, String(scopedAction.value.model ?? ""), {
422
+ sessionId: String(scopedAction.value.sessionId ?? ""),
423
+ updateMessageId: scopedAction.messageId,
424
+ });
425
+ } else if (kind === "settings_thinking_select") {
426
+ await this.thinking(contextKey, String(scopedAction.value.effort ?? ""), {
427
+ sessionId: String(scopedAction.value.sessionId ?? ""),
428
+ updateMessageId: scopedAction.messageId,
429
+ expectedModel: String(scopedAction.value.model ?? ""),
430
+ });
431
+ } else if (kind === "settings_permission_select") {
432
+ await this.permissions(contextKey, permissionModeValue(scopedAction.value.permissionMode), {
433
+ sessionId: String(scopedAction.value.sessionId ?? ""),
434
+ updateMessageId: scopedAction.messageId,
435
+ });
381
436
  } else if (kind === "model_select") {
382
437
  const sessionId = String(scopedAction.value.sessionId ?? "");
383
438
  const model = String(scopedAction.value.model ?? "");
@@ -400,6 +455,52 @@ export class ProxySessionController {
400
455
  updateMessageId: scopedAction.messageId,
401
456
  expectedModel: model,
402
457
  });
458
+ } else if (kind === "provider_open") {
459
+ await this.openExecutionSettings(contextKey, "provider", {
460
+ sessionId: String(scopedAction.value.sessionId ?? ""),
461
+ updateMessageId: scopedAction.messageId,
462
+ });
463
+ } else if (kind === "provider_select" || kind === "provider_model_open") {
464
+ await this.openProviderModelSelector(
465
+ contextKey,
466
+ String(scopedAction.value.provider ?? ""),
467
+ permissionModeValue(scopedAction.value.permissionMode),
468
+ String(scopedAction.value.sessionId ?? ""),
469
+ requiredCardMessageId(scopedAction.messageId),
470
+ );
471
+ } else if (kind === "provider_model_select" || kind === "provider_reasoning_open") {
472
+ await this.openProviderReasoningSelector(
473
+ contextKey,
474
+ String(scopedAction.value.provider ?? ""),
475
+ String(scopedAction.value.model ?? ""),
476
+ permissionModeValue(scopedAction.value.permissionMode),
477
+ String(scopedAction.value.sessionId ?? ""),
478
+ requiredCardMessageId(scopedAction.messageId),
479
+ );
480
+ } else if (kind === "provider_reasoning_select") {
481
+ await this.openProviderPermissionSelector(
482
+ contextKey,
483
+ String(scopedAction.value.provider ?? ""),
484
+ String(scopedAction.value.model ?? ""),
485
+ String(scopedAction.value.effort ?? ""),
486
+ permissionModeValue(scopedAction.value.permissionMode),
487
+ String(scopedAction.value.sessionId ?? ""),
488
+ requiredCardMessageId(scopedAction.messageId),
489
+ );
490
+ } else if (kind === "provider_permission_select") {
491
+ await this.applyProviderSettings(
492
+ contextKey,
493
+ {
494
+ provider: String(scopedAction.value.provider ?? ""),
495
+ model: String(scopedAction.value.model ?? ""),
496
+ effort: String(scopedAction.value.effort ?? ""),
497
+ mode: permissionModeValue(scopedAction.value.permissionMode),
498
+ },
499
+ {
500
+ sessionId: String(scopedAction.value.sessionId ?? ""),
501
+ updateMessageId: scopedAction.messageId,
502
+ },
503
+ );
403
504
  } else if (kind === "approval") {
404
505
  await this.resolveApproval(scopedAction);
405
506
  }
@@ -445,7 +546,7 @@ export class ProxySessionController {
445
546
  if (!record) throw new Error(`Task not found: ${localSessionId}`);
446
547
  let remote: RemoteSessionSummary | undefined;
447
548
  if (record.remoteSessionId) {
448
- const runtime = this.runtimes.forAgent(this.ensureAgent(record.agentName));
549
+ const runtime = this.runtimes.forAgent(record.agentName);
449
550
  if (runtime.readRemoteSession) {
450
551
  try {
451
552
  remote = await runtime.readRemoteSession(record.remoteSessionId);
@@ -484,7 +585,7 @@ export class ProxySessionController {
484
585
  if (!promptText) throw new Error("The Prompt cannot be empty.");
485
586
  const record = this.store.getSession(localSessionId);
486
587
  if (!record || record.status === "closed") throw new Error(`Task not found: ${localSessionId}`);
487
- const runtime = this.runtimes.forAgent(this.ensureAgent(record.agentName));
588
+ const runtime = this.runtimes.forAgent(record.agentName);
488
589
  const activeTurnId = runtime.getSession(localSessionId)?.activeTurnId;
489
590
  const routedContextKey = this.outbound.getSessionContextKey(localSessionId);
490
591
  const responseContextKey = (activeTurnId ? this.store.getTurnContextKey(activeTurnId) ?? routedContextKey : undefined)
@@ -546,7 +647,7 @@ export class ProxySessionController {
546
647
 
547
648
  const sessionReference = typeof action.value.sessionId === "string" ? action.value.sessionId : undefined;
548
649
  const session = sessionReference
549
- ? this.store.getSession(sessionReference) ?? this.store.findSessionByRemoteSessionId(sessionReference)
650
+ ? this.store.getSession(sessionReference) ?? this.findStoredSessionByReference(sessionReference)
550
651
  : undefined;
551
652
  if (session && baseChatContextKey(session.contextKey) === baseChatContextKey(action.contextKey)) {
552
653
  return session.contextKey;
@@ -581,20 +682,30 @@ export class ProxySessionController {
581
682
  await this.createSession(
582
683
  contextKey,
583
684
  context.defaultAgent,
584
- command.projectless ? undefined : command.cwd ?? this.inheritedNewTaskCwd(contextKey),
685
+ command.projectless
686
+ ? undefined
687
+ : command.cwd === undefined
688
+ ? this.inheritedNewTaskCwd(contextKey)
689
+ : resolveUserPath(command.cwd),
585
690
  true,
586
691
  false,
587
692
  undefined,
588
693
  undefined,
589
694
  command.title,
695
+ this.inheritedExecutionSettings(contextKey, context.defaultAgent),
590
696
  );
591
697
  return;
592
698
  case "newgroup":
699
+ if (command.projectless && this.ensureAgent(context.defaultAgent).kind !== "codex") {
700
+ throw new Error("/newgroup --nodir 仅支持 Codex Agent。");
701
+ }
593
702
  await this.createFeishuGroup(
594
703
  contextKey,
595
704
  context.defaultAgent,
596
705
  command.title,
597
706
  userId,
707
+ command.cwd === undefined ? undefined : resolveUserPath(command.cwd),
708
+ command.projectless === true,
598
709
  );
599
710
  return;
600
711
  case "forkgroup":
@@ -606,7 +717,6 @@ export class ProxySessionController {
606
717
  case "title":
607
718
  await this.setTitle(contextKey, command.title);
608
719
  return;
609
- case "ask":
610
720
  case "prompt":
611
721
  await this.prompt(contextKey, command.text, messageId, replyTarget, localImagePaths);
612
722
  return;
@@ -621,11 +731,7 @@ export class ProxySessionController {
621
731
  return;
622
732
  case "agent":
623
733
  if (command.agent) await this.setDefaultAgent(contextKey, command.agent);
624
- else await this.listAgents(contextKey);
625
- return;
626
- case "use":
627
- await this.setDefaultAgent(contextKey, command.agent);
628
- await this.createSession(contextKey, command.agent, command.cwd, true, false);
734
+ else await this.openAgentSettings(contextKey);
629
735
  return;
630
736
  case "status":
631
737
  await this.status(contextKey, command.sessionId);
@@ -635,20 +741,19 @@ export class ProxySessionController {
635
741
  return;
636
742
  case "restart":
637
743
  if (!this.lifecycle) throw new Error("当前运行方式不支持自动重启。");
638
- await this.lifecycle.restart(contextKey);
744
+ await this.lifecycle.restart(contextKey, command.force === true);
639
745
  return;
640
746
  case "model":
641
- await this.model(contextKey, command.model);
747
+ await this.openExecutionSettings(contextKey, "model");
748
+ return;
749
+ case "provider":
750
+ await this.openProviderSettings(contextKey);
642
751
  return;
643
752
  case "thinking":
644
- await this.thinking(contextKey, command.effort);
753
+ await this.openExecutionSettings(contextKey, "thinking");
645
754
  return;
646
755
  case "permissions":
647
- await this.permissions(contextKey, command.mode);
648
- return;
649
- case "modes":
650
- case "mode":
651
- await this.outbound.sendText(contextKey, "当前统一运行时不再暴露 ACP modes;Codex 请使用 /model 和 /permissions。");
756
+ await this.openExecutionSettings(contextKey, "permission");
652
757
  return;
653
758
  case "help":
654
759
  await this.help(contextKey);
@@ -731,7 +836,7 @@ export class ProxySessionController {
731
836
  replyTarget?: MessageReplyTarget,
732
837
  localImagePaths?: string[],
733
838
  ): Promise<void> {
734
- const configuredRuntime = this.runtimes.forAgent(this.ensureAgent(record.agentName));
839
+ const configuredRuntime = this.runtimes.forAgent(record.agentName);
735
840
  if (!configuredRuntime.getSession(record.localSessionId)) {
736
841
  this.outbound.registerSession(record.localSessionId, contextKey, record.title, record.cwd);
737
842
  await this.outbound.startPendingTurn(record.localSessionId, contextKey, record.title, replyTarget, text);
@@ -967,7 +1072,7 @@ export class ProxySessionController {
967
1072
  private async forkThreadSession(message: IncomingMessage): Promise<void> {
968
1073
  const { anchor, source, snapshot } = this.resolveThreadForkAnchor(message);
969
1074
  const agent = this.ensureAgent(source.agentName);
970
- const runtime = this.runtimes.forAgent(agent);
1075
+ const runtime = this.runtimes.forAgent(source.agentName);
971
1076
  if (runtime.kind !== "codex" || !runtime.forkSession) {
972
1077
  throw new Error("当前 Codex 运行时不支持 fork 任务。");
973
1078
  }
@@ -987,6 +1092,7 @@ export class ProxySessionController {
987
1092
  this.store.updateRuntimeSession(localSessionId, {
988
1093
  runtimeKind: "codex",
989
1094
  title: forkTitle,
1095
+ modelProvider: source.modelProvider,
990
1096
  model: source.model,
991
1097
  reasoningEffort: source.reasoningEffort,
992
1098
  permissionMode: source.permissionMode ?? "auto",
@@ -1002,6 +1108,7 @@ export class ProxySessionController {
1002
1108
  agentName: source.agentName,
1003
1109
  cwd: source.cwd,
1004
1110
  title: forkTitle,
1111
+ modelProvider: source.modelProvider,
1005
1112
  model: source.model,
1006
1113
  reasoningEffort: source.reasoningEffort,
1007
1114
  permissionMode: source.permissionMode ?? "auto",
@@ -1091,7 +1198,7 @@ export class ProxySessionController {
1091
1198
  if (direct?.status === "closed") {
1092
1199
  throw new Error(`找不到任务:${taskId}`);
1093
1200
  }
1094
- const global = direct ?? this.store.findSessionByRemoteSessionId(taskId);
1201
+ const global = direct ?? this.findStoredSessionByReference(taskId);
1095
1202
  source = global ? { ...global, contextKey } : undefined;
1096
1203
  }
1097
1204
 
@@ -1101,25 +1208,27 @@ export class ProxySessionController {
1101
1208
 
1102
1209
  let runtime: AgentRuntime;
1103
1210
  let agentName: string;
1211
+ let remote: RemoteSessionSummary | undefined;
1104
1212
  if (source) {
1105
1213
  agentName = source.agentName;
1106
- runtime = this.runtimes.forAgent(this.ensureAgent(agentName));
1214
+ runtime = this.runtimes.forAgent(agentName);
1107
1215
  } else {
1108
- const agentEntry = Object.entries(this.config.agents).find(([, agent]) => agent.kind === "codex");
1109
- if (!agentEntry) throw new Error("未配置 Codex Agent。");
1110
- [agentName] = agentEntry;
1111
- runtime = this.runtimes.get("codex");
1216
+ if (!taskId) throw new Error("缺少要 fork 的 Codex 任务 ID。");
1217
+ const resolved = await this.resolveRemoteCodexSession(taskId);
1218
+ agentName = resolved.agentName;
1219
+ runtime = resolved.runtime;
1220
+ remote = resolved.remote;
1112
1221
  }
1113
1222
  if (runtime.kind !== "codex" || !runtime.forkSession) {
1114
1223
  throw new Error("当前 Codex 运行时不支持 fork 任务。");
1115
1224
  }
1116
1225
 
1117
- const remoteSessionId = source?.remoteSessionId ?? taskId;
1226
+ const remoteSessionId = source?.remoteSessionId ?? remote?.id ?? taskId;
1118
1227
  if (!remoteSessionId) throw new Error("当前任务尚未创建 Codex 任务 ID,暂时不能 fork。");
1119
1228
  if (!runtime.readRemoteSession && !source) {
1120
1229
  throw new Error("当前 Codex 运行时不支持读取指定任务。");
1121
1230
  }
1122
- const remote = runtime.readRemoteSession
1231
+ remote ??= runtime.readRemoteSession
1123
1232
  ? await runtime.readRemoteSession(remoteSessionId)
1124
1233
  : undefined;
1125
1234
  const latestTurnId = remote?.lastTurnId ?? source?.lastTurnId;
@@ -1145,7 +1254,8 @@ export class ProxySessionController {
1145
1254
  if (!cwd) throw new Error("指定的 Codex 任务没有可用的工作目录,暂时不能 fork。");
1146
1255
  const sourceTitle = remote?.title ?? source?.title ?? remote?.preview;
1147
1256
  const forkTitle = normalizeTaskTitle(requestedTitle) ?? this.store.nextForkTitle(sourceTitle);
1148
- const model = source?.model;
1257
+ const modelProvider = remote?.modelProvider ?? source?.modelProvider;
1258
+ const model = remote?.model ?? source?.model;
1149
1259
  const reasoningEffort = source?.reasoningEffort;
1150
1260
  const permissionMode = source?.permissionMode ?? "auto";
1151
1261
 
@@ -1158,6 +1268,7 @@ export class ProxySessionController {
1158
1268
  lastTurnId,
1159
1269
  cwd,
1160
1270
  forkTitle,
1271
+ modelProvider,
1161
1272
  model,
1162
1273
  reasoningEffort,
1163
1274
  permissionMode,
@@ -1223,7 +1334,7 @@ export class ProxySessionController {
1223
1334
  if (!source.remoteSessionId || !this.isCodexSession(source)) {
1224
1335
  throw new Error(`${sourceLabel}不是可 fork 的 Codex 任务。`);
1225
1336
  }
1226
- const runtime = this.runtimes.forAgent(this.ensureAgent(source.agentName));
1337
+ const runtime = this.runtimes.forAgent(source.agentName);
1227
1338
  if (runtime.kind !== "codex" || !runtime.forkSession) {
1228
1339
  throw new Error("当前 Codex 运行时不支持 fork 任务。");
1229
1340
  }
@@ -1248,6 +1359,7 @@ export class ProxySessionController {
1248
1359
  lastTurnId,
1249
1360
  cwd: source.cwd,
1250
1361
  forkTitle,
1362
+ modelProvider: source.modelProvider,
1251
1363
  model: source.model,
1252
1364
  reasoningEffort: source.reasoningEffort,
1253
1365
  permissionMode: source.permissionMode ?? "auto",
@@ -1274,6 +1386,7 @@ export class ProxySessionController {
1274
1386
  this.store.updateRuntimeSession(localSessionId, {
1275
1387
  runtimeKind: "codex",
1276
1388
  title: plan.forkTitle,
1389
+ modelProvider: plan.modelProvider,
1277
1390
  model: plan.model,
1278
1391
  reasoningEffort: plan.reasoningEffort,
1279
1392
  permissionMode: plan.permissionMode,
@@ -1287,6 +1400,7 @@ export class ProxySessionController {
1287
1400
  agentName: plan.agentName,
1288
1401
  cwd: plan.cwd,
1289
1402
  title: plan.forkTitle,
1403
+ modelProvider: plan.modelProvider,
1290
1404
  model: plan.model,
1291
1405
  reasoningEffort: plan.reasoningEffort,
1292
1406
  permissionMode: plan.permissionMode,
@@ -1348,26 +1462,26 @@ export class ProxySessionController {
1348
1462
  if (direct?.status === "closed") {
1349
1463
  throw new Error(`找不到任务:${taskId}`);
1350
1464
  }
1351
- const source = direct ?? this.store.findSessionByRemoteSessionId(taskId);
1465
+ const source = direct ?? this.findStoredSessionByReference(taskId);
1352
1466
  if (source && !this.isCodexSession(source)) {
1353
1467
  throw new Error("指定任务不是 Codex 任务,暂时不能按项目创建新任务。");
1354
1468
  }
1355
1469
 
1356
1470
  let agentName: string;
1357
1471
  let runtime: AgentRuntime;
1472
+ let remote: RemoteSessionSummary | undefined;
1358
1473
  if (source) {
1359
1474
  agentName = source.agentName;
1360
- runtime = this.runtimes.forAgent(this.ensureAgent(agentName));
1475
+ runtime = this.runtimes.forAgent(agentName);
1361
1476
  } else {
1362
- const agentEntry = Object.entries(this.config.agents).find(([, agent]) => agent.kind === "codex");
1363
- if (!agentEntry) throw new Error("未配置 Codex Agent。");
1364
- [agentName] = agentEntry;
1365
- runtime = this.runtimes.get("codex");
1477
+ const resolved = await this.resolveRemoteCodexSession(taskId);
1478
+ agentName = resolved.agentName;
1479
+ runtime = resolved.runtime;
1480
+ remote = resolved.remote;
1366
1481
  }
1367
1482
 
1368
- const remoteSessionId = source?.remoteSessionId ?? taskId;
1369
- let remote: RemoteSessionSummary | undefined;
1370
- if (runtime.readRemoteSession && remoteSessionId) {
1483
+ const remoteSessionId = source?.remoteSessionId ?? remote?.id ?? taskId;
1484
+ if (!remote && runtime.readRemoteSession && remoteSessionId) {
1371
1485
  try {
1372
1486
  remote = await runtime.readRemoteSession(remoteSessionId);
1373
1487
  } catch (error) {
@@ -1384,6 +1498,7 @@ export class ProxySessionController {
1384
1498
  throw new Error("指定的 Codex 任务没有可用的工作目录,暂时不能按项目创建新任务。");
1385
1499
  }
1386
1500
  const executionSettings: SessionExecutionSettings = {
1501
+ modelProvider: remote?.modelProvider ?? source?.modelProvider,
1387
1502
  model: remote?.model ?? source?.model,
1388
1503
  reasoningEffort: remote?.reasoningEffort ?? source?.reasoningEffort,
1389
1504
  permissionMode: remote?.permissionMode ?? source?.permissionMode ?? "auto",
@@ -1455,9 +1570,10 @@ export class ProxySessionController {
1455
1570
  ? createProjectlessWorkspace({ prompt: initialTitle }).cwd
1456
1571
  : path.resolve(cwd ?? this.config.defaults.cwd);
1457
1572
  const record = this.store.createSession({ localSessionId, contextKey, agentName, cwd: sessionCwd, status: "starting" });
1458
- if (initialTitle || executionSettings.model || executionSettings.reasoningEffort || executionSettings.permissionMode) {
1573
+ if (initialTitle || executionSettings.modelProvider || executionSettings.model || executionSettings.reasoningEffort || executionSettings.permissionMode) {
1459
1574
  this.store.updateRuntimeSession(localSessionId, {
1460
1575
  title: initialTitle,
1576
+ modelProvider: executionSettings.modelProvider,
1461
1577
  model: executionSettings.model,
1462
1578
  reasoningEffort: executionSettings.reasoningEffort,
1463
1579
  permissionMode: executionSettings.permissionMode,
@@ -1465,7 +1581,7 @@ export class ProxySessionController {
1465
1581
  }
1466
1582
  this.store.setCurrentSession(contextKey, localSessionId);
1467
1583
  this.outbound.registerSession(localSessionId, contextKey, initialTitle, sessionCwd);
1468
- const runtime = this.runtimes.forAgent(agent);
1584
+ const runtime = this.runtimes.forAgent(agentName);
1469
1585
  try {
1470
1586
  if (prepareTurn) {
1471
1587
  await this.outbound.startPendingTurn(localSessionId, contextKey, initialTitle, replyTarget, prompt);
@@ -1475,6 +1591,7 @@ export class ProxySessionController {
1475
1591
  agentName,
1476
1592
  cwd: sessionCwd,
1477
1593
  title: initialTitle,
1594
+ modelProvider: executionSettings.modelProvider,
1478
1595
  model: executionSettings.model,
1479
1596
  reasoningEffort: executionSettings.reasoningEffort,
1480
1597
  permissionMode: executionSettings.permissionMode ?? "auto",
@@ -1500,11 +1617,16 @@ export class ProxySessionController {
1500
1617
  agentName: string,
1501
1618
  requestedTitle: string | undefined,
1502
1619
  userId: string | undefined,
1620
+ requestedProjectCwd?: string,
1621
+ forceProjectless = false,
1503
1622
  ): Promise<void> {
1504
1623
  const source = this.currentSession(sourceContextKey);
1505
- const boundProjectCwd = this.currentProjectCwd(sourceContextKey);
1624
+ const boundProjectCwd = forceProjectless
1625
+ ? undefined
1626
+ : requestedProjectCwd ?? this.currentProjectCwd(sourceContextKey);
1506
1627
  const executionSettings: SessionExecutionSettings = source?.agentName === agentName
1507
1628
  ? {
1629
+ modelProvider: source.modelProvider,
1508
1630
  model: source.model,
1509
1631
  reasoningEffort: source.reasoningEffort,
1510
1632
  permissionMode: source.permissionMode,
@@ -1587,8 +1709,10 @@ export class ProxySessionController {
1587
1709
  "群和新任务已创建。",
1588
1710
  `当前任务:${taskDescription}`,
1589
1711
  `当前 Project 目录:${boundProjectCwd ?? "未绑定(Projectless)"}`,
1712
+ `当前 Provider:${task.modelProvider ?? "Codex 默认"}`,
1590
1713
  `当前模型:${task.model ?? "默认"}`,
1591
1714
  `思考强度:${task.reasoningEffort ?? "自动"}`,
1715
+ `权限类型:${task.permissionMode === "confirm" ? "执行前确认" : "自动执行"}`,
1592
1716
  ].join("\n"),
1593
1717
  );
1594
1718
  await this.outbound.sendText(
@@ -1675,6 +1799,7 @@ export class ProxySessionController {
1675
1799
  `已从${prepared.sourceDescription}创建分支。`,
1676
1800
  `当前任务:${taskDescription}`,
1677
1801
  `当前 Project 目录:${boundProjectCwd ?? "未绑定(Projectless)"}`,
1802
+ `当前 Provider:${forked.session.modelProvider ?? "Codex 默认"}`,
1678
1803
  `当前模型:${forked.session.model ?? "默认"}`,
1679
1804
  `思考强度:${forked.session.reasoningEffort ?? "自动"}`,
1680
1805
  `权限类型:${forked.session.permissionMode === "confirm" ? "执行前确认" : "自动执行"}`,
@@ -1716,7 +1841,7 @@ export class ProxySessionController {
1716
1841
 
1717
1842
  private async loadSession(record: SessionRecord): Promise<LoadedSession> {
1718
1843
  const agent = this.ensureAgent(record.agentName);
1719
- const runtime = this.runtimes.forAgent(agent);
1844
+ const runtime = this.runtimes.forAgent(record.agentName);
1720
1845
  const existing = runtime.getSession(record.localSessionId);
1721
1846
  if (existing) return { record, runtime, session: existing };
1722
1847
  const pending = this.sessionLoads.get(record.localSessionId);
@@ -1745,6 +1870,7 @@ export class ProxySessionController {
1745
1870
  agentName: record.agentName,
1746
1871
  cwd: record.cwd,
1747
1872
  title: record.title,
1873
+ modelProvider: record.modelProvider,
1748
1874
  model: record.model,
1749
1875
  reasoningEffort: record.reasoningEffort,
1750
1876
  permissionMode,
@@ -1762,6 +1888,7 @@ export class ProxySessionController {
1762
1888
  agentName: record.agentName,
1763
1889
  cwd: record.cwd,
1764
1890
  title: record.title,
1891
+ modelProvider: record.modelProvider,
1765
1892
  model: record.model,
1766
1893
  reasoningEffort: record.reasoningEffort,
1767
1894
  permissionMode,
@@ -1773,6 +1900,7 @@ export class ProxySessionController {
1773
1900
  agentName: record.agentName,
1774
1901
  cwd: record.cwd,
1775
1902
  title: record.title,
1903
+ modelProvider: record.modelProvider,
1776
1904
  model: record.model,
1777
1905
  reasoningEffort: record.reasoningEffort,
1778
1906
  permissionMode,
@@ -1795,6 +1923,7 @@ export class ProxySessionController {
1795
1923
  runtimeKind: session.runtimeKind,
1796
1924
  remoteSessionId: session.remoteSessionId,
1797
1925
  title: session.title,
1926
+ modelProvider: session.modelProvider,
1798
1927
  model: session.model,
1799
1928
  reasoningEffort: session.reasoningEffort,
1800
1929
  permissionMode: session.permissionMode,
@@ -1821,36 +1950,39 @@ export class ProxySessionController {
1821
1950
  });
1822
1951
  }
1823
1952
 
1953
+ let presentationError: unknown;
1824
1954
  try {
1825
1955
  await this.outbound.onEvent(event);
1826
- if (event.type === "turn_completed" || event.type === "turn_cancelled" || event.type === "turn_failed") {
1827
- const terminalStatus = event.type === "turn_completed"
1828
- ? "completed"
1829
- : event.type === "turn_cancelled"
1830
- ? "cancelled"
1831
- : "failed";
1832
- const latest = this.store.getSession(event.sessionId);
1833
- if (latest?.lastTurnId === event.turnId && latest.lastTurnStatus === terminalStatus) {
1834
- await this.finalizeTurnMessageReactions(event.turnId, terminalStatus);
1835
- }
1836
- }
1837
- } finally {
1838
- if (event.type === "turn_completed" || event.type === "turn_cancelled" || event.type === "turn_failed") {
1839
- const latest = this.store.getSession(event.sessionId);
1840
- const agent = latest ? this.ensureAgent(latest.agentName) : undefined;
1841
- const activeTurnId = agent ? this.runtimes.forAgent(agent).getSession(event.sessionId)?.activeTurnId : undefined;
1842
- if (latest?.lastTurnId === event.turnId && !activeTurnId) {
1843
- this.store.updateSession(event.sessionId, { status: event.type === "turn_failed" ? "failed" : "ready" });
1844
- }
1845
- }
1956
+ } catch (error) {
1957
+ presentationError = error;
1846
1958
  }
1959
+
1847
1960
  if (event.type === "turn_completed" || event.type === "turn_cancelled" || event.type === "turn_failed") {
1961
+ const terminalStatus = event.type === "turn_completed"
1962
+ ? "completed"
1963
+ : event.type === "turn_cancelled"
1964
+ ? "cancelled"
1965
+ : "failed";
1966
+ const reactionSession = this.store.getSession(event.sessionId);
1967
+ if (
1968
+ !presentationError
1969
+ && reactionSession?.lastTurnId === event.turnId
1970
+ && reactionSession.lastTurnStatus === terminalStatus
1971
+ ) {
1972
+ await this.finalizeTurnMessageReactions(event.turnId, terminalStatus);
1973
+ }
1974
+
1848
1975
  const latest = this.store.getSession(event.sessionId);
1849
- const agent = latest ? this.ensureAgent(latest.agentName) : undefined;
1850
- const activeTurnId = agent ? this.runtimes.forAgent(agent).getSession(event.sessionId)?.activeTurnId : undefined;
1851
- if (latest?.lastTurnId !== event.turnId || activeTurnId) return;
1852
- await this.scheduleNextQueuedPrompt(event.sessionId);
1976
+ const activeTurnId = latest
1977
+ ? this.runtimes.forAgent(latest.agentName).getSession(event.sessionId)?.activeTurnId
1978
+ : undefined;
1979
+ if (latest?.lastTurnId === event.turnId && !activeTurnId) {
1980
+ this.store.updateSession(event.sessionId, { status: event.type === "turn_failed" ? "failed" : "ready" });
1981
+ await this.scheduleNextQueuedPrompt(event.sessionId);
1982
+ }
1853
1983
  }
1984
+
1985
+ if (presentationError) throw presentationError;
1854
1986
  }
1855
1987
 
1856
1988
  private scheduleNextQueuedPrompt(sessionId: string): Promise<void> {
@@ -1960,7 +2092,7 @@ export class ProxySessionController {
1960
2092
  }
1961
2093
 
1962
2094
  private async cancelSession(record: SessionRecord): Promise<void> {
1963
- const runtime = this.runtimes.forAgent(this.ensureAgent(record.agentName));
2095
+ const runtime = this.runtimes.forAgent(record.agentName);
1964
2096
  if (runtime.kind === "codex" && record.remoteSessionId && runtime.interruptRemoteTurn) {
1965
2097
  if (runtime.getGoal && runtime.setGoal) {
1966
2098
  try {
@@ -2022,29 +2154,295 @@ export class ProxySessionController {
2022
2154
  await loaded.runtime.respondToApproval(sessionId, requestId, decision);
2023
2155
  }
2024
2156
 
2025
- private async model(contextKey: string, model?: string, options: ModelCardOptions = {}): Promise<void> {
2157
+ private async openExecutionSettings(
2158
+ contextKey: string,
2159
+ activeTab: ExecutionSettingsTab,
2160
+ options: ExecutionSettingsCardOptions = {},
2161
+ ): Promise<void> {
2162
+ const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent!);
2163
+ const record = options.sessionId
2164
+ ? this.requireSession(contextKey, options.sessionId)
2165
+ : this.currentSession(contextKey);
2166
+ if (!record && activeTab !== "agent") this.requireCurrentSession(contextKey);
2167
+ const agents = Object.entries(this.config.agents).map(([name, agent]) => ({
2168
+ name,
2169
+ title: agent.title,
2170
+ }));
2171
+
2172
+ let runtimeSettings: Omit<Parameters<CardRenderer["renderExecutionSettings"]>[0],
2173
+ "sessionId" | "contextKey" | "activeTab" | "currentAgent" | "taskAgent" | "agents" | "runtimeSettingsAvailable" | "notice"> = {
2174
+ currentPermissionMode: "auto",
2175
+ providers: [],
2176
+ providerSupported: false,
2177
+ models: [],
2178
+ reasoningOptions: [],
2179
+ };
2180
+ if (record) {
2181
+ const loaded = await this.loadSession(record);
2182
+ const models = await loaded.runtime.listModels();
2183
+ const currentModel = models.find((model) => model.id === loaded.session.model)
2184
+ ?? models.find((model) => model.isDefault);
2185
+ const providerSupported = loaded.runtime.kind === "codex" && Boolean(loaded.runtime.listModelProviders);
2186
+ const providers = providerSupported ? await this.modelProviderOptions(loaded) : [];
2187
+ const currentProvider = loaded.session.modelProvider ?? providers.find((provider) => provider.isDefault)?.id;
2188
+ const currentEffort = currentModel?.supportedReasoningEfforts.some(
2189
+ (option) => option.value === loaded.session.reasoningEffort,
2190
+ )
2191
+ ? loaded.session.reasoningEffort
2192
+ : currentModel?.defaultReasoningEffort ?? loaded.session.reasoningEffort;
2193
+ runtimeSettings = {
2194
+ currentProvider,
2195
+ currentModel: currentModel?.id ?? loaded.session.model,
2196
+ currentEffort,
2197
+ currentPermissionMode: loaded.session.permissionMode,
2198
+ providers,
2199
+ providerSupported,
2200
+ models,
2201
+ reasoningOptions: currentModel?.supportedReasoningEfforts ?? [],
2202
+ };
2203
+ }
2204
+ const card = this.cardRenderer.renderExecutionSettings({
2205
+ ...(record ? { sessionId: record.localSessionId, taskAgent: record.agentName } : {}),
2206
+ contextKey,
2207
+ activeTab,
2208
+ currentAgent: context.defaultAgent,
2209
+ agents,
2210
+ runtimeSettingsAvailable: Boolean(record),
2211
+ ...runtimeSettings,
2212
+ notice: options.notice,
2213
+ });
2214
+ if (options.updateMessageId) {
2215
+ await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
2216
+ } else {
2217
+ await this.outbound.sendInteractiveCard(contextKey, card);
2218
+ }
2219
+ }
2220
+
2221
+ private async openAgentSettings(contextKey: string): Promise<void> {
2222
+ const current = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent!).defaultAgent;
2223
+ if (Object.keys(this.config.agents).length <= 1) {
2224
+ await this.outbound.sendText(contextKey, `当前 Agent:${current}\n当前没有其他 Agent 可以切换。`);
2225
+ return;
2226
+ }
2227
+ await this.openExecutionSettings(contextKey, "agent");
2228
+ }
2229
+
2230
+ private async openProviderSettings(contextKey: string): Promise<void> {
2231
+ const loaded = await this.loadSession(this.requireCurrentSession(contextKey));
2232
+ const providers = loaded.runtime.kind === "codex" && loaded.runtime.listModelProviders
2233
+ ? await this.modelProviderOptions(loaded)
2234
+ : [];
2235
+ const current = loaded.session.modelProvider?.trim()
2236
+ || providers.find((provider) => provider.isDefault)?.id
2237
+ || "运行时默认";
2238
+ if (providers.length <= 1) {
2239
+ await this.outbound.sendText(contextKey, `当前 Provider:${current}\n当前没有其他 Provider 可以切换。`);
2240
+ return;
2241
+ }
2242
+ await this.openExecutionSettings(contextKey, "provider", { sessionId: loaded.record.localSessionId });
2243
+ }
2244
+
2245
+ private async selectProvider(
2246
+ contextKey: string,
2247
+ modelProvider: string,
2248
+ options: ModelCardOptions = {},
2249
+ ): Promise<void> {
2026
2250
  const record = options.sessionId
2027
2251
  ? this.requireSession(contextKey, options.sessionId)
2028
2252
  : this.requireCurrentSession(contextKey);
2029
2253
  const loaded = await this.loadSession(record);
2254
+ await this.assertModelProvider(loaded, modelProvider);
2030
2255
  const models = await loaded.runtime.listModels();
2031
- if (!model) {
2032
- const currentModel = loaded.session.model ?? models.find((item) => item.isDefault)?.id;
2033
- const card = this.cardRenderer.renderModelSelector({
2034
- sessionId: loaded.record.localSessionId,
2256
+ const model = models.find((candidate) => candidate.id === loaded.session.model)
2257
+ ?? models.find((candidate) => candidate.isDefault);
2258
+ if (!model) throw new Error("当前运行时没有可用于 Provider 切换的模型。");
2259
+ const effort = model.supportedReasoningEfforts.some(
2260
+ (candidate) => candidate.value === loaded.session.reasoningEffort,
2261
+ )
2262
+ ? loaded.session.reasoningEffort
2263
+ : model.defaultReasoningEffort ?? model.supportedReasoningEfforts[0]?.value;
2264
+ if (!effort) throw new Error(`模型 ${model.id} 没有可用于 Provider 切换的思考强度。`);
2265
+ await this.applyProviderSettings(contextKey, {
2266
+ provider: modelProvider,
2267
+ model: model.id,
2268
+ effort,
2269
+ mode: loaded.session.permissionMode,
2270
+ }, options);
2271
+ }
2272
+
2273
+ private async openProviderSelector(
2274
+ contextKey: string,
2275
+ options: ExecutionSettingsCardOptions = {},
2276
+ ): Promise<void> {
2277
+ await this.openExecutionSettings(contextKey, "provider", options);
2278
+ }
2279
+
2280
+ private async openProviderModelSelector(
2281
+ contextKey: string,
2282
+ modelProvider: string,
2283
+ permissionMode: PermissionMode,
2284
+ sessionId: string,
2285
+ updateMessageId: string,
2286
+ ): Promise<void> {
2287
+ const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
2288
+ await this.assertModelProvider(loaded, modelProvider);
2289
+ const models = await loaded.runtime.listModels();
2290
+ const card = this.cardRenderer.renderModelSelector({
2291
+ sessionId,
2292
+ contextKey,
2293
+ currentModel: loaded.session.model ?? models.find((model) => model.isDefault)?.id,
2294
+ reasoningEffort: loaded.session.reasoningEffort,
2295
+ models,
2296
+ modelProvider,
2297
+ permissionMode,
2298
+ unifiedSettings: true,
2299
+ });
2300
+ await this.outbound.updateInteractiveCard(contextKey, updateMessageId, card);
2301
+ }
2302
+
2303
+ private async openProviderReasoningSelector(
2304
+ contextKey: string,
2305
+ modelProvider: string,
2306
+ model: string,
2307
+ permissionMode: PermissionMode,
2308
+ sessionId: string,
2309
+ updateMessageId: string,
2310
+ ): Promise<void> {
2311
+ const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
2312
+ await this.assertModelProvider(loaded, modelProvider);
2313
+ const models = await loaded.runtime.listModels();
2314
+ const selected = models.find((candidate) => candidate.id === model);
2315
+ if (!selected) throw new Error(`未知模型:${model}`);
2316
+ const currentEffort = selected.supportedReasoningEfforts.some(
2317
+ (option) => option.value === loaded.session.reasoningEffort,
2318
+ )
2319
+ ? loaded.session.reasoningEffort
2320
+ : selected.defaultReasoningEffort;
2321
+ const card = this.cardRenderer.renderReasoningSelector({
2322
+ sessionId,
2323
+ contextKey,
2324
+ modelProvider,
2325
+ model,
2326
+ currentEffort,
2327
+ options: selected.supportedReasoningEfforts,
2328
+ permissionMode,
2329
+ unifiedSettings: true,
2330
+ });
2331
+ await this.outbound.updateInteractiveCard(contextKey, updateMessageId, card);
2332
+ }
2333
+
2334
+ private async openProviderPermissionSelector(
2335
+ contextKey: string,
2336
+ modelProvider: string,
2337
+ model: string,
2338
+ effort: string,
2339
+ permissionMode: PermissionMode,
2340
+ sessionId: string,
2341
+ updateMessageId: string,
2342
+ ): Promise<void> {
2343
+ const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
2344
+ await this.assertProviderModelSettings(loaded, modelProvider, model, effort);
2345
+ await this.outbound.updateInteractiveCard(
2346
+ contextKey,
2347
+ updateMessageId,
2348
+ this.cardRenderer.renderPermissionSelector({
2349
+ sessionId,
2035
2350
  contextKey,
2036
- currentModel,
2037
- reasoningEffort: loaded.session.reasoningEffort,
2038
- models,
2351
+ modelProvider,
2352
+ model,
2353
+ reasoningEffort: effort,
2354
+ currentMode: permissionMode,
2355
+ }),
2356
+ );
2357
+ }
2358
+
2359
+ private async applyProviderSettings(
2360
+ contextKey: string,
2361
+ settings: { provider: string; model: string; effort: string; mode: PermissionMode },
2362
+ options: ModelCardOptions = {},
2363
+ ): Promise<void> {
2364
+ const record = options.sessionId
2365
+ ? this.requireSession(contextKey, options.sessionId)
2366
+ : this.requireCurrentSession(contextKey);
2367
+ const loaded = await this.loadSession(record);
2368
+ await this.assertProviderModelSettings(loaded, settings.provider, settings.model, settings.effort);
2369
+ if (!loaded.runtime.setExecutionSettings) {
2370
+ throw new Error("当前运行时不支持 Provider 设置。");
2371
+ }
2372
+ const session = await loaded.runtime.setExecutionSettings(loaded.record.localSessionId, {
2373
+ modelProvider: settings.provider,
2374
+ model: settings.model,
2375
+ reasoningEffort: settings.effort,
2376
+ permissionMode: settings.mode,
2377
+ });
2378
+ this.store.updateRuntimeSession(loaded.record.localSessionId, {
2379
+ modelProvider: session.modelProvider ?? settings.provider,
2380
+ model: session.model ?? settings.model,
2381
+ reasoningEffort: session.reasoningEffort ?? settings.effort,
2382
+ permissionMode: session.permissionMode,
2383
+ });
2384
+ const notice = [
2385
+ `Provider 已切换为 ${cardCode(session.modelProvider ?? settings.provider)}`,
2386
+ `模型 ${cardCode(session.model ?? settings.model)}`,
2387
+ `思考强度 ${cardCode(session.reasoningEffort ?? settings.effort)}`,
2388
+ `权限 ${cardCode(session.permissionMode)}`,
2389
+ "从下一次请求生效。",
2390
+ ].join(",");
2391
+ if (options.updateMessageId) {
2392
+ await this.openProviderSelector(contextKey, {
2393
+ sessionId: loaded.record.localSessionId,
2394
+ updateMessageId: options.updateMessageId,
2395
+ notice,
2039
2396
  });
2040
- if (options.updateMessageId) {
2041
- await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
2042
- } else {
2043
- await this.outbound.sendInteractiveCard(contextKey, card);
2044
- }
2045
- return;
2397
+ } else {
2398
+ await this.outbound.sendText(contextKey, notice.replaceAll("`", ""));
2399
+ }
2400
+ }
2401
+
2402
+ private async assertProviderModelSettings(
2403
+ loaded: LoadedSession,
2404
+ modelProvider: string,
2405
+ model: string,
2406
+ effort: string,
2407
+ ): Promise<void> {
2408
+ await this.assertModelProvider(loaded, modelProvider);
2409
+ const selected = (await loaded.runtime.listModels()).find((candidate) => candidate.id === model);
2410
+ if (!selected) throw new Error(`未知模型:${model}`);
2411
+ if (!selected.supportedReasoningEfforts.some((option) => option.value === effort)) {
2412
+ const supported = selected.supportedReasoningEfforts.map((option) => option.value).join("、") || "无";
2413
+ throw new Error(`模型 ${model} 不支持思考强度 ${effort}。支持的强度:${supported}`);
2046
2414
  }
2415
+ }
2047
2416
 
2417
+ private async assertModelProvider(loaded: LoadedSession, modelProvider: string): Promise<void> {
2418
+ const providers = await this.modelProviderOptions(loaded);
2419
+ if (!providers.some((provider) => provider.id === modelProvider)) {
2420
+ throw new Error(`未知 Provider:${modelProvider}`);
2421
+ }
2422
+ }
2423
+
2424
+ private async modelProviderOptions(loaded: LoadedSession) {
2425
+ if (loaded.runtime.kind !== "codex" || !loaded.runtime.listModelProviders) {
2426
+ throw new Error("当前任务不支持 Provider 设置。");
2427
+ }
2428
+ const providers = await loaded.runtime.listModelProviders();
2429
+ const current = loaded.session.modelProvider?.trim();
2430
+ if (current && !providers.some((provider) => provider.id === current)) {
2431
+ providers.unshift({ id: current });
2432
+ }
2433
+ return providers;
2434
+ }
2435
+
2436
+ private async model(contextKey: string, model?: string, options: ModelCardOptions = {}): Promise<void> {
2437
+ if (!model) {
2438
+ await this.openExecutionSettings(contextKey, "model", options);
2439
+ return;
2440
+ }
2441
+ const record = options.sessionId
2442
+ ? this.requireSession(contextKey, options.sessionId)
2443
+ : this.requireCurrentSession(contextKey);
2444
+ const loaded = await this.loadSession(record);
2445
+ const models = await loaded.runtime.listModels();
2048
2446
  const selected = models.find((item) => item.id === model);
2049
2447
  if (!selected) throw new Error(`未知模型:${model}`);
2050
2448
  const currentEffort = loaded.session.reasoningEffort;
@@ -2062,18 +2460,11 @@ export class ProxySessionController {
2062
2460
  ? `,思考强度已自动调整为 ${nextEffort}`
2063
2461
  : "";
2064
2462
  if (options.updateMessageId) {
2065
- await this.outbound.updateInteractiveCard(
2066
- contextKey,
2067
- options.updateMessageId,
2068
- this.cardRenderer.renderReasoningSelector({
2069
- sessionId: loaded.record.localSessionId,
2070
- contextKey,
2071
- model,
2072
- currentEffort: nextEffort,
2073
- options: selected.supportedReasoningEfforts,
2074
- notice: `模型已切换为 ${cardCode(model)},请选择思考模式。`,
2075
- }),
2076
- );
2463
+ await this.openExecutionSettings(contextKey, "model", {
2464
+ sessionId: loaded.record.localSessionId,
2465
+ updateMessageId: options.updateMessageId,
2466
+ notice: `模型已切换为 ${cardCode(model)}${effortMessage},从下一次请求生效。`,
2467
+ });
2077
2468
  return;
2078
2469
  }
2079
2470
  await this.outbound.sendText(contextKey, `模型已切换为 ${model}${effortMessage},从下一次请求生效。`);
@@ -2242,6 +2633,10 @@ export class ProxySessionController {
2242
2633
  }
2243
2634
 
2244
2635
  private async thinking(contextKey: string, effort?: string, options: ThinkingCardOptions = {}): Promise<void> {
2636
+ if (!effort) {
2637
+ await this.openExecutionSettings(contextKey, "thinking", options);
2638
+ return;
2639
+ }
2245
2640
  const record = options.sessionId
2246
2641
  ? this.requireSession(contextKey, options.sessionId)
2247
2642
  : this.requireCurrentSession(contextKey);
@@ -2255,22 +2650,6 @@ export class ProxySessionController {
2255
2650
  }
2256
2651
  const supported = currentModel.supportedReasoningEfforts;
2257
2652
 
2258
- if (!effort) {
2259
- const card = this.cardRenderer.renderReasoningSelector({
2260
- sessionId: loaded.record.localSessionId,
2261
- contextKey,
2262
- model: currentModel.id,
2263
- currentEffort: loaded.session.reasoningEffort ?? currentModel.defaultReasoningEffort,
2264
- options: supported,
2265
- });
2266
- if (options.updateMessageId) {
2267
- await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
2268
- } else {
2269
- await this.outbound.sendInteractiveCard(contextKey, card);
2270
- }
2271
- return;
2272
- }
2273
-
2274
2653
  if (!supported.some((option) => option.value === effort)) {
2275
2654
  const options = supported.map((option) => option.value).join("、") || "无";
2276
2655
  throw new Error(`不支持的思考强度:${effort}。支持的强度:${options}`);
@@ -2278,48 +2657,45 @@ export class ProxySessionController {
2278
2657
  await loaded.runtime.setReasoningEffort(loaded.record.localSessionId, effort);
2279
2658
  this.store.updateRuntimeSession(loaded.record.localSessionId, { reasoningEffort: effort });
2280
2659
  if (options.updateMessageId) {
2281
- await this.outbound.updateInteractiveCard(
2282
- contextKey,
2283
- options.updateMessageId,
2284
- this.cardRenderer.renderReasoningSelector({
2285
- sessionId: loaded.record.localSessionId,
2286
- contextKey,
2287
- model: currentModel.id,
2288
- currentEffort: effort,
2289
- options: supported,
2290
- notice: `思考模式已切换为 ${cardCode(effort)},从下一次请求生效。`,
2291
- }),
2292
- );
2660
+ await this.openExecutionSettings(contextKey, "thinking", {
2661
+ sessionId: loaded.record.localSessionId,
2662
+ updateMessageId: options.updateMessageId,
2663
+ notice: `思考强度已切换为 ${cardCode(effort)},从下一次请求生效。`,
2664
+ });
2293
2665
  return;
2294
2666
  }
2295
2667
  await this.outbound.sendText(contextKey, `思考强度已切换为 ${effort},从下一次请求生效。`);
2296
2668
  }
2297
2669
 
2298
- private async permissions(contextKey: string, mode?: PermissionMode): Promise<void> {
2299
- const record = this.requireCurrentSession(contextKey);
2670
+ private async permissions(
2671
+ contextKey: string,
2672
+ mode?: PermissionMode,
2673
+ options: ModelCardOptions = {},
2674
+ ): Promise<void> {
2300
2675
  if (!mode) {
2301
- await this.outbound.sendText(contextKey, `当前权限模式:${record.permissionMode ?? "auto"}`);
2676
+ await this.openExecutionSettings(contextKey, "permission", options);
2302
2677
  return;
2303
2678
  }
2679
+ const record = options.sessionId
2680
+ ? this.requireSession(contextKey, options.sessionId)
2681
+ : this.requireCurrentSession(contextKey);
2304
2682
  const loaded = await this.loadSession(record);
2305
2683
  await loaded.runtime.setPermissionMode(record.localSessionId, mode);
2684
+ loaded.session.permissionMode = mode;
2306
2685
  this.store.updateRuntimeSession(record.localSessionId, { permissionMode: mode });
2686
+ if (options.updateMessageId) {
2687
+ await this.openExecutionSettings(contextKey, "permission", {
2688
+ sessionId: loaded.record.localSessionId,
2689
+ updateMessageId: options.updateMessageId,
2690
+ notice: mode === "auto"
2691
+ ? "已切换为自动执行模式,从下一次请求生效。"
2692
+ : "已切换为执行前确认模式,从下一次请求生效。",
2693
+ });
2694
+ return;
2695
+ }
2307
2696
  await this.outbound.sendText(contextKey, mode === "auto" ? "已切换为自动执行模式。" : "已切换为执行前确认模式。");
2308
2697
  }
2309
2698
 
2310
- private async listAgents(contextKey: string): Promise<void> {
2311
- const current = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent!).defaultAgent;
2312
- const lines = Object.entries(this.config.agents).map(([name, agent]) => {
2313
- const selected = name === current;
2314
- return `- ${selected ? "✅ " : ""}${asInlineCode(name)}:${agent.title}${selected ? "(当前)" : ""}`;
2315
- });
2316
- await this.outbound.sendMarkdown(contextKey, [
2317
- `当前 Agent:${asInlineCode(current)}`,
2318
- "可用 Agent:",
2319
- lines.join("\n") || "无",
2320
- ].join("\n"));
2321
- }
2322
-
2323
2699
  private async listSessions(
2324
2700
  contextKey: string,
2325
2701
  searchTerm?: string,
@@ -2331,39 +2707,46 @@ export class ProxySessionController {
2331
2707
  const localSessions = this.store.listSessions(contextKey).filter((session) =>
2332
2708
  session.status !== "closed" && this.isCodexSession(session),
2333
2709
  );
2334
- let remoteSessions: RemoteSessionSummary[] = [];
2335
- let remoteHint: string | undefined;
2710
+ const remoteSessions: AgentRemoteSessionSummary[] = [];
2711
+ const remoteErrors: string[] = [];
2336
2712
  let remoteHasMore = false;
2337
- const runtime = this.runtimes.get("codex");
2338
- if (runtime.listRemoteSessions) {
2713
+ await Promise.all(this.runtimes.entries("codex").map(async ([agentName, runtime]) => {
2714
+ if (!runtime.listRemoteSessions) return;
2339
2715
  try {
2340
2716
  const page = await runtime.listRemoteSessions({ searchTerm, limit: visibleCount });
2341
- remoteSessions = page.sessions;
2342
- remoteHasMore = Boolean(page.nextCursor);
2717
+ remoteSessions.push(...page.sessions.map((session) => ({ agentName, session })));
2718
+ remoteHasMore ||= Boolean(page.nextCursor);
2343
2719
  } catch (error) {
2344
- this.logger.warn({ error, contextKey }, "Failed to list Codex sessions.");
2345
- remoteHint = `读取 Codex 任务失败:${error instanceof Error ? error.message : String(error)}`;
2720
+ this.logger.warn({ error, contextKey, agentName }, "Failed to list Codex sessions for an Agent.");
2721
+ remoteErrors.push(`${agentName}: ${error instanceof Error ? error.message : String(error)}`);
2346
2722
  }
2347
- }
2723
+ }));
2724
+ const remoteHint = remoteErrors.length > 0
2725
+ ? `部分 Agent 的任务读取失败:${remoteErrors.join(";")}`
2726
+ : undefined;
2348
2727
 
2349
2728
  const entries = mergeTaskList(localSessions, remoteSessions, context.currentSessionId)
2350
- .filter((entry) => !normalizedSearch || [entry.id, entry.title, entry.cwd].some((value) => value.toLowerCase().includes(normalizedSearch)))
2729
+ .filter((entry) => !normalizedSearch
2730
+ || [entry.id, entry.title, entry.cwd, entry.agentName]
2731
+ .some((value) => value.toLowerCase().includes(normalizedSearch)))
2351
2732
  .sort((left, right) => Number(right.current) - Number(left.current)
2352
2733
  || Number(right.active) - Number(left.active)
2353
2734
  || right.updatedAt - left.updatedAt);
2354
2735
  const activeCount = entries.filter((entry) => entry.active).length;
2355
2736
  const visibleEntries = entries.slice(0, visibleCount);
2356
2737
  const hasMore = remoteHasMore || entries.length > visibleCount;
2357
- this.lastSessionListings.set(contextKey, visibleEntries.map((entry) => entry.id));
2738
+ this.lastSessionListings.set(contextKey, visibleEntries.map((entry) => entry.reference));
2358
2739
  const cardEntries = visibleEntries.map((entry, index) => {
2359
2740
  const marker = entry.current ? "✅" : entry.active ? "🟢 **活跃**" : "•";
2360
- const showStop = entry.status === "外部执行中" && entry.id !== options.forceSwitchTaskId;
2741
+ const showStop = entry.status === "外部执行中"
2742
+ && entry.reference !== options.forceSwitchTaskId
2743
+ && entry.id !== options.forceSwitchTaskId;
2361
2744
  const actions: TaskListCardAction[] = entry.current ? [] : [{
2362
2745
  text: showStop ? "Stop" : "Switch",
2363
2746
  type: showStop ? "danger" as const : "default" as const,
2364
2747
  value: {
2365
2748
  action: showStop ? "session_stop" : "session_switch",
2366
- sessionId: entry.id,
2749
+ sessionId: entry.reference,
2367
2750
  ...(searchTerm ? { searchTerm } : {}),
2368
2751
  visibleCount: String(visibleCount),
2369
2752
  contextKey,
@@ -2373,7 +2756,7 @@ export class ProxySessionController {
2373
2756
  text: "New",
2374
2757
  value: {
2375
2758
  action: "session_new",
2376
- sessionId: entry.id,
2759
+ sessionId: entry.reference,
2377
2760
  ...(searchTerm ? { searchTerm } : {}),
2378
2761
  visibleCount: String(visibleCount),
2379
2762
  contextKey,
@@ -2383,7 +2766,7 @@ export class ProxySessionController {
2383
2766
  text: "NewGroup",
2384
2767
  value: {
2385
2768
  action: "session_new_group",
2386
- sessionId: entry.id,
2769
+ sessionId: entry.reference,
2387
2770
  ...(searchTerm ? { searchTerm } : {}),
2388
2771
  visibleCount: String(visibleCount),
2389
2772
  contextKey,
@@ -2393,7 +2776,7 @@ export class ProxySessionController {
2393
2776
  text: "Fork",
2394
2777
  value: {
2395
2778
  action: "session_fork",
2396
- sessionId: entry.id,
2779
+ sessionId: entry.reference,
2397
2780
  ...(searchTerm ? { searchTerm } : {}),
2398
2781
  visibleCount: String(visibleCount),
2399
2782
  contextKey,
@@ -2403,7 +2786,7 @@ export class ProxySessionController {
2403
2786
  text: "ForkGroup",
2404
2787
  value: {
2405
2788
  action: "session_fork_group",
2406
- sessionId: entry.id,
2789
+ sessionId: entry.reference,
2407
2790
  ...(searchTerm ? { searchTerm } : {}),
2408
2791
  visibleCount: String(visibleCount),
2409
2792
  contextKey,
@@ -2413,14 +2796,14 @@ export class ProxySessionController {
2413
2796
  text: "Status",
2414
2797
  value: {
2415
2798
  action: "session_status",
2416
- sessionId: entry.id,
2799
+ sessionId: entry.reference,
2417
2800
  contextKey,
2418
2801
  },
2419
2802
  });
2420
2803
  return {
2421
2804
  lines: [
2422
2805
  `**${index + 1}.** ${marker} **${cardText(entry.title)}** ${cardText(entry.id)}`,
2423
- `${entry.status} · ${entry.updatedLabel} · ${cardText(entry.cwd || "目录未知")}`,
2806
+ `${entry.status} · ${entry.updatedLabel} · ${cardText(entry.agentName)} · ${cardText(entry.cwd || "目录未知")}`,
2424
2807
  ],
2425
2808
  actions,
2426
2809
  };
@@ -2483,6 +2866,46 @@ export class ProxySessionController {
2483
2866
  });
2484
2867
  }
2485
2868
 
2869
+ private findStoredSessionByReference(reference: string, contextKey?: string): SessionRecord | undefined {
2870
+ const scoped = parseRemoteSessionReference(reference);
2871
+ return scoped
2872
+ ? this.store.findSessionByRemoteSessionId(scoped.remoteSessionId, contextKey, scoped.agentName)
2873
+ : this.store.findSessionByRemoteSessionId(reference, contextKey);
2874
+ }
2875
+
2876
+ private async resolveRemoteCodexSession(reference: string): Promise<AgentRemoteSession> {
2877
+ const scoped = parseRemoteSessionReference(reference);
2878
+ const candidates = scoped
2879
+ ? [[scoped.agentName, this.runtimes.forAgent(scoped.agentName)] as const]
2880
+ : this.runtimes.entries("codex");
2881
+ if (candidates.length === 0) throw new Error("未配置 Codex Agent。");
2882
+
2883
+ const reads = await Promise.allSettled(candidates.map(async ([agentName, runtime]) => {
2884
+ if (runtime.kind !== "codex" || !runtime.readRemoteSession) {
2885
+ throw new Error(`Agent ${agentName} 不支持读取远端任务。`);
2886
+ }
2887
+ return {
2888
+ agentName,
2889
+ runtime,
2890
+ remote: await runtime.readRemoteSession(scoped?.remoteSessionId ?? reference),
2891
+ } satisfies AgentRemoteSession;
2892
+ }));
2893
+ const matches = reads
2894
+ .filter((result): result is PromiseFulfilledResult<AgentRemoteSession> => result.status === "fulfilled")
2895
+ .map((result) => result.value);
2896
+ if (matches.length === 1) return matches[0]!;
2897
+ if (matches.length > 1) {
2898
+ throw new Error(`多个 Agent 中存在相同任务 ID:${reference}。请先使用 /sessions,再通过序号选择任务。`);
2899
+ }
2900
+ const details = reads
2901
+ .map((result, index) => result.status === "rejected"
2902
+ ? `${candidates[index]?.[0] ?? "unknown"}: ${runtimeErrorMessage(result.reason)}`
2903
+ : undefined)
2904
+ .filter((detail): detail is string => Boolean(detail))
2905
+ .join(";");
2906
+ throw new Error(`找不到 Codex 任务:${scoped?.remoteSessionId ?? reference}${details ? `(${details})` : ""}`);
2907
+ }
2908
+
2486
2909
  private async switchSession(contextKey: string, reference?: string): Promise<void> {
2487
2910
  this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent!);
2488
2911
  const taskId = this.resolveSessionReference(contextKey, reference);
@@ -2490,9 +2913,9 @@ export class ProxySessionController {
2490
2913
  if (direct?.status === "closed") {
2491
2914
  throw new Error(`找不到任务:${taskId}`);
2492
2915
  }
2493
- const existing = direct ?? this.store.findSessionByRemoteSessionId(taskId);
2916
+ const existing = direct ?? this.findStoredSessionByReference(taskId);
2494
2917
  if (existing) {
2495
- const runtime = this.runtimes.forAgent(this.ensureAgent(existing.agentName));
2918
+ const runtime = this.runtimes.forAgent(existing.agentName);
2496
2919
  await this.assertSessionTurnOwnership(existing, runtime);
2497
2920
  this.store.attachSessionToContext(contextKey, existing.localSessionId);
2498
2921
  this.store.setCurrentSession(contextKey, existing.localSessionId);
@@ -2501,16 +2924,11 @@ export class ProxySessionController {
2501
2924
  return;
2502
2925
  }
2503
2926
 
2504
- const runtime = this.runtimes.get("codex");
2505
- if (!runtime.readRemoteSession) throw new Error("当前 Codex 运行时不支持读取任务。");
2506
- const remote = await runtime.readRemoteSession(taskId);
2927
+ const { agentName, remote } = await this.resolveRemoteCodexSession(taskId);
2507
2928
  if (remote.status === "active" || remote.lastTurnStatus === "inProgress") {
2508
2929
  throw new Error(`这个任务正在外部 Codex 中执行,当前不会切换。可使用 /status ${taskId} 查看进度。`);
2509
2930
  }
2510
2931
  if (!remote.cwd) throw new Error("这个 Codex 任务没有可用的工作目录,暂时无法切换。");
2511
- const agentEntry = Object.entries(this.config.agents).find(([, agent]) => agent.kind === "codex");
2512
- if (!agentEntry) throw new Error("未配置 Codex Agent。");
2513
- const [agentName] = agentEntry;
2514
2932
  const localSessionId = createId("sess");
2515
2933
  this.store.createSession({
2516
2934
  localSessionId,
@@ -2543,17 +2961,16 @@ export class ProxySessionController {
2543
2961
  return;
2544
2962
  }
2545
2963
 
2546
- const existing = this.store.findSessionByRemoteSessionId(taskId);
2964
+ const existing = this.findStoredSessionByReference(taskId);
2547
2965
  if (existing) {
2548
2966
  await this.cancelSession({ ...existing, contextKey });
2549
2967
  return;
2550
2968
  }
2551
2969
 
2552
- const runtime = this.runtimes.get("codex");
2970
+ const { runtime, remote } = await this.resolveRemoteCodexSession(taskId);
2553
2971
  if (!runtime.readRemoteSession || !runtime.interruptRemoteTurn) {
2554
2972
  throw new Error("当前 Codex 运行时不支持停止外部任务。");
2555
2973
  }
2556
- const remote = await runtime.readRemoteSession(taskId);
2557
2974
  const turnId = remote.status === "active" || remote.lastTurnStatus === "inProgress"
2558
2975
  ? remote.lastTurnId
2559
2976
  : undefined;
@@ -2628,11 +3045,22 @@ export class ProxySessionController {
2628
3045
  return undefined;
2629
3046
  }
2630
3047
 
2631
- private async setDefaultAgent(contextKey: string, agentName: string): Promise<void> {
3048
+ private async setDefaultAgent(
3049
+ contextKey: string,
3050
+ agentName: string,
3051
+ options: ExecutionSettingsCardOptions = {},
3052
+ ): Promise<void> {
2632
3053
  this.ensureAgent(agentName);
2633
3054
  this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent!);
2634
3055
  this.store.setDefaultAgent(contextKey, agentName);
2635
- await this.outbound.sendText(contextKey, `默认 agent 已切换为:${agentName}`);
3056
+ if (Object.keys(this.config.agents).length <= 1) {
3057
+ await this.outbound.sendText(contextKey, `当前 Agent:${agentName}\n当前没有其他 Agent 可以切换。`);
3058
+ return;
3059
+ }
3060
+ await this.openExecutionSettings(contextKey, "agent", {
3061
+ ...options,
3062
+ notice: `默认 Agent 已切换为 ${cardCode(agentName)},从下一次新建任务生效。`,
3063
+ });
2636
3064
  }
2637
3065
 
2638
3066
  private async status(
@@ -2647,7 +3075,7 @@ export class ProxySessionController {
2647
3075
  const direct = this.store.getSession(targetSessionId);
2648
3076
  current = direct
2649
3077
  ? { ...direct, contextKey }
2650
- : this.store.findSessionByRemoteSessionId(targetSessionId, contextKey);
3078
+ : this.findStoredSessionByReference(targetSessionId, contextKey);
2651
3079
  if (!current) {
2652
3080
  await this.statusForCodexTask(contextKey, targetSessionId, options);
2653
3081
  return;
@@ -2658,7 +3086,7 @@ export class ProxySessionController {
2658
3086
 
2659
3087
  if (current && current.status === "running") {
2660
3088
  try {
2661
- const runtime = this.runtimes.forAgent(this.ensureAgent(current.agentName));
3089
+ const runtime = this.runtimes.forAgent(current.agentName);
2662
3090
  if (runtime.getSession(current.localSessionId)) {
2663
3091
  await runtime.synchronizeSession(current.localSessionId);
2664
3092
  current = this.store.getSession(current.localSessionId) ?? current;
@@ -2672,7 +3100,7 @@ export class ProxySessionController {
2672
3100
  let remote: RemoteSessionSummary | undefined;
2673
3101
  let goal: RuntimeGoal | undefined;
2674
3102
  if (current?.remoteSessionId) {
2675
- const runtime = this.runtimes.forAgent(this.ensureAgent(current.agentName));
3103
+ const runtime = this.runtimes.forAgent(current.agentName);
2676
3104
  if (runtime.readRemoteSession) {
2677
3105
  try {
2678
3106
  remote = await runtime.readRemoteSession(current.remoteSessionId);
@@ -2700,7 +3128,7 @@ export class ProxySessionController {
2700
3128
  taskLines.push("无。直接发送消息即可创建一个未指定项目的 Codex 任务。");
2701
3129
  } else {
2702
3130
  const agent = this.ensureAgent(current.agentName);
2703
- const runtimeSession = this.runtimes.forAgent(agent).getSession(current.localSessionId);
3131
+ const runtimeSession = this.runtimes.forAgent(current.agentName).getSession(current.localSessionId);
2704
3132
  activeTurnId = runtimeSession?.activeTurnId;
2705
3133
  const remoteActive = isRemoteSessionActive(remote);
2706
3134
  activeTurnId = remoteActive ? remote?.lastTurnId ?? activeTurnId : activeTurnId;
@@ -2715,7 +3143,7 @@ export class ProxySessionController {
2715
3143
  taskLines.push(
2716
3144
  `**标题**:${cardCode(current.title ?? "未命名任务")}`,
2717
3145
  `**工作目录**:${cardCode(current.cwd)}`,
2718
- `**模型 / 思考强度**:${cardCode(current.model ?? "默认")} / ${cardCode(current.reasoningEffort ?? "自动")}`,
3146
+ `**Provider / 模型 / 思考强度**:${cardCode(current.modelProvider ?? "Codex 默认")} / ${cardCode(current.model ?? "默认")} / ${cardCode(current.reasoningEffort ?? "自动")}`,
2719
3147
  `**状态 / 最近结果**:${statusLabel} / ${resultLabel}`,
2720
3148
  `**Agent**:${cardCode(agent.title)}`,
2721
3149
  `**权限 / 任务范围**:${current.permissionMode === "confirm" ? "执行前确认" : "自动执行"} / ${detectProjectlessWorkspace(current.cwd) ? "未指定项目" : "指定项目"}`,
@@ -2752,12 +3180,17 @@ export class ProxySessionController {
2752
3180
  const title = targetSessionId && current
2753
3181
  ? `Codex 状态:${truncateText((current.title ?? current.remoteSessionId ?? current.localSessionId).replace(/\s+/g, " "), 40)}`
2754
3182
  : "Codex 状态";
2755
- const taskId = current?.remoteSessionId ?? current?.localSessionId;
3183
+ const taskId = current?.remoteSessionId
3184
+ ? remoteSessionReference(current.agentName, current.remoteSessionId)
3185
+ : current?.localSessionId;
2756
3186
  const isCurrent = Boolean(current && current.localSessionId === context.currentSessionId);
2757
3187
  const remoteActive = isRemoteSessionActive(remote);
2758
3188
  const botOwnsActiveTurn = Boolean(localCurrent && remote && isBotOwnedActiveTurn(localCurrent, remote));
2759
3189
  const active = Boolean(activeTurnId || current?.status === "running" || remoteActive);
2760
- const forceSwitch = Boolean(taskId && options.forceSwitchTaskId === taskId);
3190
+ const forceSwitch = Boolean(taskId && (
3191
+ options.forceSwitchTaskId === taskId
3192
+ || options.forceSwitchTaskId === current?.remoteSessionId
3193
+ ));
2761
3194
  const taskActions: TaskListCardAction[] = !taskId ? []
2762
3195
  : remoteActive && !botOwnsActiveTurn && !forceSwitch
2763
3196
  ? [statusCardAction("Stop", "danger", "session_stop", taskId, contextKey)]
@@ -2777,12 +3210,11 @@ export class ProxySessionController {
2777
3210
 
2778
3211
  private async statusForCodexTask(
2779
3212
  contextKey: string,
2780
- remoteSessionId: string,
3213
+ reference: string,
2781
3214
  options: StatusCardOptions = {},
2782
3215
  ): Promise<void> {
2783
- const runtime = this.runtimes.get("codex");
2784
- if (!runtime.readRemoteSession) throw new Error("当前 Codex 运行时不支持读取外部任务状态。");
2785
- const remote = await runtime.readRemoteSession(remoteSessionId);
3216
+ const { agentName, remote } = await this.resolveRemoteCodexSession(reference);
3217
+ const actionReference = remoteSessionReference(agentName, remote.id);
2786
3218
  const sections: CardSection[] = [
2787
3219
  {
2788
3220
  title: "指定任务",
@@ -2790,6 +3222,7 @@ export class ProxySessionController {
2790
3222
  `**标题**:${cardCode(remote.title ?? remote.preview ?? "未命名任务")}`,
2791
3223
  `**工作目录**:${cardCode(remote.cwd || "目录未知")}`,
2792
3224
  `**状态 / 当前任务**:${remoteSessionDetailStatus(remote)} / 未切换`,
3225
+ `**Agent**:${cardCode(agentName)}`,
2793
3226
  `**Codex 任务 ID**:${cardCode(remote.id)}`,
2794
3227
  `**最近回合**:${cardCode(remote.lastTurnId ?? "无")} ${remoteTurnStatusLabel(remote.lastTurnStatus)}`,
2795
3228
  `**创建时间 / 最近活动**:${formatRemoteTime(remote.createdAt)} / ${formatRemoteTime(latestRemoteTimestamp(remote.recencyAt, remote.updatedAt))}`,
@@ -2809,14 +3242,16 @@ export class ProxySessionController {
2809
3242
  },
2810
3243
  ];
2811
3244
  const title = `Codex 状态:${truncateText((remote.title ?? remote.preview ?? remote.id).replace(/\s+/g, " "), 40)}`;
2812
- const showStop = isRemoteSessionActive(remote) && options.forceSwitchTaskId !== remote.id;
3245
+ const showStop = isRemoteSessionActive(remote)
3246
+ && options.forceSwitchTaskId !== actionReference
3247
+ && options.forceSwitchTaskId !== remote.id;
2813
3248
  const actions = [
2814
- statusRefreshAction(remote.id, contextKey),
3249
+ statusRefreshAction(actionReference, contextKey),
2815
3250
  statusCardAction(
2816
3251
  showStop ? "Stop" : "Switch",
2817
3252
  showStop ? "danger" : "default",
2818
3253
  showStop ? "session_stop" : "session_switch",
2819
- remote.id,
3254
+ actionReference,
2820
3255
  contextKey,
2821
3256
  ),
2822
3257
  ];
@@ -2840,7 +3275,7 @@ export class ProxySessionController {
2840
3275
  title: "任务管理",
2841
3276
  lines: [
2842
3277
  "**/new [title] [--dir &#60;cwd&#62; | --nodir]** 使用默认 Agent 创建任务;--nodir 强制创建 Projectless 任务",
2843
- "**/newgroup [title]** 创建飞书群和新任务;继承当前项目与执行设置",
3278
+ "**/newgroup [title] [--dir &#60;cwd&#62; | --nodir]** 创建飞书群和新任务;参数与 /new 相同",
2844
3279
  "**/forkgroup [title]** 从当前任务最新已完成轮次创建分支并绑定到新群",
2845
3280
  "**/fork [序号或任务 ID]** 从当前或指定任务创建分支;运行中使用最近已完成轮次",
2846
3281
  "**/title &#60;新标题&#62;** 修改当前任务的标题",
@@ -2855,22 +3290,23 @@ export class ProxySessionController {
2855
3290
  "**/stop** 停止当前执行",
2856
3291
  "**/queue &#60;prompt&#62;**(兼容 **/nosteer**) 不追加到当前轮次,按顺序排队为后续轮次",
2857
3292
  "**/goal [目标]** 查看或创建长任务 Goal;支持 pause、resume、edit、clear",
2858
- "**/model [name]** 查看或切换模型",
2859
- "**/thinking [level]** 查看或设置思考强度",
2860
- "**/permissions [auto|confirm]** 查看或切换确认模式",
3293
+ "**/provider** 选择 AI 服务提供商;没有其他候选时仅显示当前 Provider",
3294
+ "**/model** 选择当前任务使用的模型",
3295
+ "**/thinking** 设置模型的思考强度",
3296
+ "**/permissions** 设置执行工具前是否需要确认",
2861
3297
  ],
2862
3298
  },
2863
3299
  {
2864
3300
  title: "Agent",
2865
3301
  lines: [
2866
- "**/agent [name]** 查看或设置新任务的默认 Agent",
3302
+ "**/agent [name]** 选择新任务使用的默认 Agent;仅配置一个时直接显示当前 Agent",
2867
3303
  ],
2868
3304
  },
2869
3305
  {
2870
3306
  title: "系统",
2871
3307
  lines: [
2872
3308
  "**/status [序号或任务 ID]** 查看当前或指定任务的详细状态",
2873
- "**/restart** 重启 Agent Bot 并恢复会话",
3309
+ "**/restart [--force]** 默认安全重启;--force 立即重启",
2874
3310
  "**/help** 显示本帮助",
2875
3311
  ],
2876
3312
  },
@@ -2904,6 +3340,17 @@ export class ProxySessionController {
2904
3340
  : current.cwd;
2905
3341
  }
2906
3342
 
3343
+ private inheritedExecutionSettings(contextKey: string, agentName: string): SessionExecutionSettings {
3344
+ const current = this.currentSession(contextKey);
3345
+ if (!current || current.agentName !== agentName) return {};
3346
+ return {
3347
+ modelProvider: current.modelProvider,
3348
+ model: current.model,
3349
+ reasoningEffort: current.reasoningEffort,
3350
+ permissionMode: current.permissionMode,
3351
+ };
3352
+ }
3353
+
2907
3354
  private currentProjectCwd(contextKey: string): string | undefined {
2908
3355
  const current = this.currentSession(contextKey);
2909
3356
  return current && !detectProjectlessWorkspace(current.cwd) ? current.cwd : undefined;
@@ -2969,7 +3416,9 @@ function statusRefreshAction(
2969
3416
  }
2970
3417
 
2971
3418
  interface UnifiedTaskListEntry {
3419
+ reference: string;
2972
3420
  id: string;
3421
+ agentName: string;
2973
3422
  title: string;
2974
3423
  cwd: string;
2975
3424
  status: string;
@@ -2981,17 +3430,17 @@ interface UnifiedTaskListEntry {
2981
3430
 
2982
3431
  function mergeTaskList(
2983
3432
  localSessions: SessionRecord[],
2984
- remoteSessions: RemoteSessionSummary[],
3433
+ remoteSessions: AgentRemoteSessionSummary[],
2985
3434
  currentLocalSessionId?: string,
2986
3435
  ): UnifiedTaskListEntry[] {
2987
3436
  const localByRemoteId = new Map(
2988
3437
  localSessions
2989
3438
  .filter((session) => session.runtimeKind === "codex" && session.remoteSessionId)
2990
- .map((session) => [session.remoteSessionId!, session]),
3439
+ .map((session) => [agentRemoteKey(session.agentName, session.remoteSessionId!), session]),
2991
3440
  );
2992
3441
  const representedLocalIds = new Set<string>();
2993
- const entries = remoteSessions.map((remote): UnifiedTaskListEntry => {
2994
- const local = localByRemoteId.get(remote.id);
3442
+ const entries = remoteSessions.map(({ agentName, session: remote }): UnifiedTaskListEntry => {
3443
+ const local = localByRemoteId.get(agentRemoteKey(agentName, remote.id));
2995
3444
  if (local) representedLocalIds.add(local.localSessionId);
2996
3445
  const active = remote.status === "active" || remote.lastTurnStatus === "inProgress";
2997
3446
  const status = remote.status === "active" || remote.lastTurnStatus === "inProgress"
@@ -2999,7 +3448,9 @@ function mergeTaskList(
2999
3448
  : remoteSessionStatusLabel(remote.status);
3000
3449
  const recencyAt = remote.recencyAt ?? remote.updatedAt;
3001
3450
  return {
3451
+ reference: remoteSessionReference(agentName, remote.id),
3002
3452
  id: remote.id,
3453
+ agentName,
3003
3454
  title: remote.title ?? remote.preview ?? local?.title ?? "未命名任务",
3004
3455
  cwd: remote.cwd || local?.cwd || "",
3005
3456
  status,
@@ -3014,7 +3465,11 @@ function mergeTaskList(
3014
3465
  if (representedLocalIds.has(local.localSessionId)) continue;
3015
3466
  const updatedAt = Date.parse(local.updatedAt);
3016
3467
  entries.push({
3468
+ reference: local.remoteSessionId
3469
+ ? remoteSessionReference(local.agentName, local.remoteSessionId)
3470
+ : local.localSessionId,
3017
3471
  id: local.remoteSessionId ?? local.localSessionId,
3472
+ agentName: local.agentName,
3018
3473
  title: local.title ?? "未命名任务",
3019
3474
  cwd: local.cwd,
3020
3475
  status: sessionStatusLabel(local.status),
@@ -3027,6 +3482,32 @@ function mergeTaskList(
3027
3482
  return entries;
3028
3483
  }
3029
3484
 
3485
+ function agentRemoteKey(agentName: string, remoteSessionId: string): string {
3486
+ return `${agentName}\u0000${remoteSessionId}`;
3487
+ }
3488
+
3489
+ function remoteSessionReference(agentName: string, remoteSessionId: string): string {
3490
+ return `${REMOTE_SESSION_REFERENCE_PREFIX}${encodeURIComponent(agentName)}:${encodeURIComponent(remoteSessionId)}`;
3491
+ }
3492
+
3493
+ function parseRemoteSessionReference(reference: string): { agentName: string; remoteSessionId: string } | undefined {
3494
+ if (!reference.startsWith(REMOTE_SESSION_REFERENCE_PREFIX)) return undefined;
3495
+ const value = reference.slice(REMOTE_SESSION_REFERENCE_PREFIX.length);
3496
+ const separator = value.indexOf(":");
3497
+ if (separator <= 0 || separator === value.length - 1) return undefined;
3498
+ try {
3499
+ const agentName = decodeURIComponent(value.slice(0, separator));
3500
+ const remoteSessionId = decodeURIComponent(value.slice(separator + 1));
3501
+ return agentName && remoteSessionId ? { agentName, remoteSessionId } : undefined;
3502
+ } catch {
3503
+ return undefined;
3504
+ }
3505
+ }
3506
+
3507
+ function runtimeErrorMessage(error: unknown): string {
3508
+ return error instanceof Error ? error.message : String(error);
3509
+ }
3510
+
3030
3511
  function isMissingRolloutError(error: unknown): boolean {
3031
3512
  const message = error instanceof Error ? error.message : String(error);
3032
3513
  return /no rollout found|rollout[^\n]*(?:not found|missing)|thread\/resume failed/i.test(message);
@@ -3208,6 +3689,21 @@ function cardCode(value: string): string {
3208
3689
  return `\`${value.replaceAll("`", "'")}\``;
3209
3690
  }
3210
3691
 
3692
+ function permissionModeValue(value: unknown): PermissionMode {
3693
+ if (value === "auto" || value === "confirm") return value;
3694
+ throw new Error("权限模式只能是 auto 或 confirm。");
3695
+ }
3696
+
3697
+ function executionSettingsTabValue(value: unknown): ExecutionSettingsTab {
3698
+ if (value === "agent" || value === "provider" || value === "model" || value === "thinking" || value === "permission") return value;
3699
+ throw new Error("设置卡片的 tab 无效,请重新发送设置命令。");
3700
+ }
3701
+
3702
+ function requiredCardMessageId(value: string | undefined): string {
3703
+ if (value) return value;
3704
+ throw new Error("无法更新设置卡片,请重新发送设置命令。");
3705
+ }
3706
+
3211
3707
  function remoteSessionStatusLabel(status: RemoteSessionSummary["status"]): string {
3212
3708
  const labels: Record<RemoteSessionSummary["status"], string> = {
3213
3709
  active: "执行中",
@@ -3243,6 +3739,10 @@ function mergeRemoteTaskStatus(record: SessionRecord, remote?: RemoteSessionSumm
3243
3739
  ...record,
3244
3740
  title: remote.title ?? record.title,
3245
3741
  cwd: remote.cwd || record.cwd,
3742
+ modelProvider: remote.modelProvider ?? record.modelProvider,
3743
+ model: remote.model ?? record.model,
3744
+ reasoningEffort: remote.reasoningEffort ?? record.reasoningEffort,
3745
+ permissionMode: remote.permissionMode ?? record.permissionMode,
3246
3746
  status: isRemoteSessionActive(remote)
3247
3747
  ? "running"
3248
3748
  : remote.lastTurnStatus
@@ -3299,9 +3799,7 @@ function isBotOwnedActiveTurn(record: SessionRecord, remote: RemoteSessionSummar
3299
3799
  function isQueueIndependentCommand(command: Command): boolean {
3300
3800
  if (["stop", "status", "restart", "help", "sessions", "goal", "nosteer"].includes(command.type)) return true;
3301
3801
  if (command.type === "agent") return command.agent === undefined;
3302
- if (command.type === "model") return command.model === undefined;
3303
- if (command.type === "thinking") return command.effort === undefined;
3304
- if (command.type === "permissions") return command.mode === undefined;
3802
+ if (["model", "provider", "thinking", "permissions"].includes(command.type)) return true;
3305
3803
  return false;
3306
3804
  }
3307
3805
 
@@ -3311,7 +3809,7 @@ function parseSessionVisibleCount(value: unknown): number {
3311
3809
  }
3312
3810
 
3313
3811
  function isPromptCommand(command: Command): boolean {
3314
- return command.type === "ask" || command.type === "prompt" || command.type === "nosteer";
3812
+ return command.type === "prompt" || command.type === "nosteer";
3315
3813
  }
3316
3814
 
3317
3815
  function runtimePrompt(text: string, localImagePaths?: string[]): RuntimePrompt {
@@ -3332,8 +3830,8 @@ function formatNewGroupName(
3332
3830
  includeTimestamp: boolean,
3333
3831
  ): string {
3334
3832
  const prefix = `[${agentName}] `;
3335
- const project = formatGroupProjectDirectory(projectCwd);
3336
- const titlePrefix = `${prefix}${project} `;
3833
+ const projectPrefix = projectCwd ? `${formatGroupProjectDirectory(projectCwd)} ` : "";
3834
+ const titlePrefix = `${prefix}${projectPrefix}`;
3337
3835
  const timestampSuffix = includeTimestamp ? ` (${formatGroupDateSuffix(date)})` : "";
3338
3836
  const availableTitleLength = FEISHU_GROUP_NAME_MAX_LENGTH
3339
3837
  - Array.from(titlePrefix).length
@@ -3345,8 +3843,8 @@ function formatNewGroupName(
3345
3843
  const FEISHU_GROUP_NAME_MAX_LENGTH = 60;
3346
3844
  const GROUP_PROJECT_DIRECTORY_MAX_LENGTH = 15;
3347
3845
 
3348
- function formatGroupProjectDirectory(projectCwd: string | undefined): string {
3349
- const value = projectCwd ? abbreviateHomeDirectory(projectCwd) : "Projectless";
3846
+ function formatGroupProjectDirectory(projectCwd: string): string {
3847
+ const value = abbreviateHomeDirectory(projectCwd);
3350
3848
  if (Array.from(value).length <= GROUP_PROJECT_DIRECTORY_MAX_LENGTH) return `[${value}]`;
3351
3849
 
3352
3850
  const levels = value