@keyou007/agent-bot 0.1.10 → 0.1.12

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 (121) hide show
  1. package/.env.example +1 -1
  2. package/CHANGELOG.md +25 -0
  3. package/README.md +55 -43
  4. package/README.zh.md +55 -43
  5. package/assets/agent-bot-logo.png +0 -0
  6. package/config.example.yaml +11 -1
  7. package/dist/acp/AcpSessionManager.js +78 -46
  8. package/dist/acp/AcpSessionManager.js.map +1 -1
  9. package/dist/cli/FeishuAppConfiguration.js +1 -1
  10. package/dist/cli/FeishuAppConfiguration.js.map +1 -1
  11. package/dist/cli/FeishuAppRegistration.js +2 -2
  12. package/dist/cli/FeishuAppRegistration.js.map +1 -1
  13. package/dist/cli/Initializer.js +1 -1
  14. package/dist/cli/Initializer.js.map +1 -1
  15. package/dist/cli/ServerStarter.js +7 -2
  16. package/dist/cli/ServerStarter.js.map +1 -1
  17. package/dist/cli/controlProtocol.js.map +1 -1
  18. package/dist/cli/help.js +20 -16
  19. package/dist/cli/help.js.map +1 -1
  20. package/dist/cli/taskGroupOptions.js +95 -0
  21. package/dist/cli/taskGroupOptions.js.map +1 -0
  22. package/dist/cli.js +74 -2
  23. package/dist/cli.js.map +1 -1
  24. package/dist/codex/AppServerConnection.js +15 -1
  25. package/dist/codex/AppServerConnection.js.map +1 -1
  26. package/dist/codex/CodexLocalActivityDetector.js +4 -1
  27. package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
  28. package/dist/codex/CodexProcessManager.js +1 -0
  29. package/dist/codex/CodexProcessManager.js.map +1 -1
  30. package/dist/codex/CodexRuntime.js +112 -11
  31. package/dist/codex/CodexRuntime.js.map +1 -1
  32. package/dist/commands/CommandRouter.js +64 -49
  33. package/dist/commands/CommandRouter.js.map +1 -1
  34. package/dist/config/loadConfig.js +11 -1
  35. package/dist/config/loadConfig.js.map +1 -1
  36. package/dist/config/schema.js +6 -1
  37. package/dist/config/schema.js.map +1 -1
  38. package/dist/console/ConsoleConnector.js +1 -1
  39. package/dist/console/ConsoleConnector.js.map +1 -1
  40. package/dist/console/ConsoleTurnPresenter.js +15 -8
  41. package/dist/console/ConsoleTurnPresenter.js.map +1 -1
  42. package/dist/deprecated-cli.js +5 -0
  43. package/dist/deprecated-cli.js.map +1 -0
  44. package/dist/feishu/CardRenderer.js +350 -15
  45. package/dist/feishu/CardRenderer.js.map +1 -1
  46. package/dist/feishu/FeishuMessageClient.js +51 -5
  47. package/dist/feishu/FeishuMessageClient.js.map +1 -1
  48. package/dist/feishu/FeishuTurnPresenter.js +17 -8
  49. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  50. package/dist/feishu/transport.js +1 -1
  51. package/dist/feishu/transport.js.map +1 -1
  52. package/dist/index.js +38 -19
  53. package/dist/index.js.map +1 -1
  54. package/dist/presentation/OutboundRouter.js +2 -2
  55. package/dist/presentation/OutboundRouter.js.map +1 -1
  56. package/dist/presentation/TurnStateReducer.js +2 -1
  57. package/dist/presentation/TurnStateReducer.js.map +1 -1
  58. package/dist/proxy/ProxySessionController.js +665 -225
  59. package/dist/proxy/ProxySessionController.js.map +1 -1
  60. package/dist/runtime/AcpRuntimeAdapter.js +1 -0
  61. package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
  62. package/dist/runtime/AgentRuntimeRegistry.js +7 -4
  63. package/dist/runtime/AgentRuntimeRegistry.js.map +1 -1
  64. package/dist/runtime/createAgentRuntimeRegistry.js +22 -0
  65. package/dist/runtime/createAgentRuntimeRegistry.js.map +1 -0
  66. package/dist/startup/SessionMetadataHydrator.js +2 -2
  67. package/dist/startup/SessionMetadataHydrator.js.map +1 -1
  68. package/dist/startup/StartupNotifier.js +1 -0
  69. package/dist/startup/StartupNotifier.js.map +1 -1
  70. package/dist/state/StateStore.js +21 -15
  71. package/dist/state/StateStore.js.map +1 -1
  72. package/dist/supervision/SafeRestartScheduler.js +5 -0
  73. package/dist/supervision/SafeRestartScheduler.js.map +1 -1
  74. package/dist/supervision/systemEnvironment.js +129 -0
  75. package/dist/supervision/systemEnvironment.js.map +1 -0
  76. package/dist/supervisor.js +13 -1
  77. package/dist/supervisor.js.map +1 -1
  78. package/docs/technical-reference.md +35 -21
  79. package/docs/technical-reference.zh.md +35 -21
  80. package/npm-shrinkwrap.json +4 -3
  81. package/package.json +4 -2
  82. package/skills/agent-bot/SKILL.md +73 -30
  83. package/src/acp/AcpSessionManager.ts +73 -54
  84. package/src/cli/FeishuAppConfiguration.ts +2 -2
  85. package/src/cli/FeishuAppRegistration.ts +4 -4
  86. package/src/cli/Initializer.ts +2 -2
  87. package/src/cli/ServerStarter.ts +8 -3
  88. package/src/cli/controlProtocol.ts +21 -1
  89. package/src/cli/help.ts +20 -16
  90. package/src/cli/taskGroupOptions.ts +149 -0
  91. package/src/cli.ts +98 -4
  92. package/src/codex/AppServerConnection.ts +21 -1
  93. package/src/codex/CodexLocalActivityDetector.ts +8 -1
  94. package/src/codex/CodexProcessManager.ts +1 -0
  95. package/src/codex/CodexRuntime.ts +137 -15
  96. package/src/commands/CommandRouter.ts +71 -49
  97. package/src/commands/commandTypes.ts +6 -11
  98. package/src/config/loadConfig.ts +11 -1
  99. package/src/config/schema.ts +7 -1
  100. package/src/console/ConsoleConnector.ts +1 -1
  101. package/src/console/ConsoleTurnPresenter.ts +21 -8
  102. package/src/deprecated-cli.ts +9 -0
  103. package/src/feishu/CardRenderer.ts +423 -15
  104. package/src/feishu/FeishuMessageClient.ts +66 -3
  105. package/src/feishu/FeishuTurnPresenter.ts +22 -3
  106. package/src/feishu/transport.ts +1 -1
  107. package/src/index.ts +54 -24
  108. package/src/presentation/OutboundRouter.ts +15 -3
  109. package/src/presentation/TurnStateReducer.ts +2 -0
  110. package/src/presentation/turnViewTypes.ts +1 -0
  111. package/src/proxy/ProxySessionController.ts +870 -228
  112. package/src/runtime/AcpRuntimeAdapter.ts +1 -0
  113. package/src/runtime/AgentRuntimeRegistry.ts +7 -6
  114. package/src/runtime/createAgentRuntimeRegistry.ts +25 -0
  115. package/src/runtime/types.ts +18 -0
  116. package/src/startup/SessionMetadataHydrator.ts +2 -2
  117. package/src/startup/StartupNotifier.ts +1 -0
  118. package/src/state/StateStore.ts +29 -15
  119. package/src/supervision/SafeRestartScheduler.ts +5 -0
  120. package/src/supervision/systemEnvironment.ts +174 -0
  121. package/src/supervisor.ts +12 -1
@@ -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;
@@ -124,13 +148,26 @@ interface ResolvedThreadForkAnchor {
124
148
  }
125
149
 
126
150
  interface CreatedFeishuGroupContext {
151
+ chatId: string;
127
152
  contextKey: string;
128
153
  name: string;
129
154
  }
130
155
 
156
+ interface CreatedFeishuTaskGroup {
157
+ group: CreatedFeishuGroupContext;
158
+ task: SessionRecord;
159
+ }
160
+
161
+ export interface ControlTaskGroupResult {
162
+ sourceLocalSessionId: string;
163
+ sourceTurnId?: string;
164
+ group: CreatedFeishuGroupContext;
165
+ task: SessionRecord;
166
+ }
167
+
131
168
  export interface ProxyLifecycle {
132
169
  supervised?: boolean;
133
- restart(contextKey: string): Promise<void>;
170
+ restart(contextKey: string, force: boolean): Promise<void>;
134
171
  cancelSafeRestart?(scheduleId: number): Promise<boolean>;
135
172
  }
136
173
 
@@ -168,9 +205,9 @@ export class ProxySessionController {
168
205
  "Marked interrupted ACP sessions as failed after process restart.",
169
206
  );
170
207
  }
171
- for (const kind of ["acp", "codex"] as const) {
208
+ for (const [, runtime] of this.runtimes.entries()) {
172
209
  this.unsubscribe.push(
173
- this.runtimes.get(kind).onEvent((event) => {
210
+ runtime.onEvent((event) => {
174
211
  void this.handleRuntimeEvent(event).catch((error: unknown) => {
175
212
  this.logger.warn({ error, event }, "Failed to present runtime event.");
176
213
  });
@@ -236,7 +273,7 @@ export class ProxySessionController {
236
273
  let command: Command;
237
274
  try {
238
275
  command = imageCount > 0 && !message.text.trimStart().startsWith("/")
239
- ? { type: "prompt", text: message.text.trim() || "请分析这张图片。" }
276
+ ? { type: "prompt", text: message.text.trim() || "请查看这张图片" }
240
277
  : this.router.parse(message.text);
241
278
  } catch (error) {
242
279
  await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
@@ -327,7 +364,7 @@ export class ProxySessionController {
327
364
  contextKey,
328
365
  String(scopedAction.value.turnId ?? ""),
329
366
  requestedPage === "latest" ? "latest" : Number.isFinite(numericPage) ? numericPage : 0,
330
- scopedAction.messageId,
367
+ requiredCardMessageId(scopedAction.messageId),
331
368
  );
332
369
  } else if (kind === "turn_cancel") {
333
370
  const sessionId = String(scopedAction.value.sessionId ?? "");
@@ -378,6 +415,37 @@ export class ProxySessionController {
378
415
  await this.status(contextKey, String(scopedAction.value.sessionId ?? ""));
379
416
  } else if (kind === "session_status_refresh") {
380
417
  await this.refreshStatusCardFromAction(scopedAction);
418
+ } else if (kind === "settings_tab_open") {
419
+ await this.openExecutionSettings(contextKey, executionSettingsTabValue(scopedAction.value.tab), {
420
+ sessionId: String(scopedAction.value.sessionId ?? ""),
421
+ updateMessageId: scopedAction.messageId,
422
+ });
423
+ } else if (kind === "settings_agent_select") {
424
+ await this.setDefaultAgent(contextKey, String(scopedAction.value.agent ?? ""), {
425
+ sessionId: String(scopedAction.value.sessionId ?? ""),
426
+ updateMessageId: scopedAction.messageId,
427
+ });
428
+ } else if (kind === "settings_provider_select") {
429
+ await this.selectProvider(contextKey, String(scopedAction.value.provider ?? ""), {
430
+ sessionId: String(scopedAction.value.sessionId ?? ""),
431
+ updateMessageId: scopedAction.messageId,
432
+ });
433
+ } else if (kind === "settings_model_select") {
434
+ await this.model(contextKey, String(scopedAction.value.model ?? ""), {
435
+ sessionId: String(scopedAction.value.sessionId ?? ""),
436
+ updateMessageId: scopedAction.messageId,
437
+ });
438
+ } else if (kind === "settings_thinking_select") {
439
+ await this.thinking(contextKey, String(scopedAction.value.effort ?? ""), {
440
+ sessionId: String(scopedAction.value.sessionId ?? ""),
441
+ updateMessageId: scopedAction.messageId,
442
+ expectedModel: String(scopedAction.value.model ?? ""),
443
+ });
444
+ } else if (kind === "settings_permission_select") {
445
+ await this.permissions(contextKey, permissionModeValue(scopedAction.value.permissionMode), {
446
+ sessionId: String(scopedAction.value.sessionId ?? ""),
447
+ updateMessageId: scopedAction.messageId,
448
+ });
381
449
  } else if (kind === "model_select") {
382
450
  const sessionId = String(scopedAction.value.sessionId ?? "");
383
451
  const model = String(scopedAction.value.model ?? "");
@@ -400,6 +468,52 @@ export class ProxySessionController {
400
468
  updateMessageId: scopedAction.messageId,
401
469
  expectedModel: model,
402
470
  });
471
+ } else if (kind === "provider_open") {
472
+ await this.openExecutionSettings(contextKey, "provider", {
473
+ sessionId: String(scopedAction.value.sessionId ?? ""),
474
+ updateMessageId: scopedAction.messageId,
475
+ });
476
+ } else if (kind === "provider_select" || kind === "provider_model_open") {
477
+ await this.openProviderModelSelector(
478
+ contextKey,
479
+ String(scopedAction.value.provider ?? ""),
480
+ permissionModeValue(scopedAction.value.permissionMode),
481
+ String(scopedAction.value.sessionId ?? ""),
482
+ requiredCardMessageId(scopedAction.messageId),
483
+ );
484
+ } else if (kind === "provider_model_select" || kind === "provider_reasoning_open") {
485
+ await this.openProviderReasoningSelector(
486
+ contextKey,
487
+ String(scopedAction.value.provider ?? ""),
488
+ String(scopedAction.value.model ?? ""),
489
+ permissionModeValue(scopedAction.value.permissionMode),
490
+ String(scopedAction.value.sessionId ?? ""),
491
+ requiredCardMessageId(scopedAction.messageId),
492
+ );
493
+ } else if (kind === "provider_reasoning_select") {
494
+ await this.openProviderPermissionSelector(
495
+ contextKey,
496
+ String(scopedAction.value.provider ?? ""),
497
+ String(scopedAction.value.model ?? ""),
498
+ String(scopedAction.value.effort ?? ""),
499
+ permissionModeValue(scopedAction.value.permissionMode),
500
+ String(scopedAction.value.sessionId ?? ""),
501
+ requiredCardMessageId(scopedAction.messageId),
502
+ );
503
+ } else if (kind === "provider_permission_select") {
504
+ await this.applyProviderSettings(
505
+ contextKey,
506
+ {
507
+ provider: String(scopedAction.value.provider ?? ""),
508
+ model: String(scopedAction.value.model ?? ""),
509
+ effort: String(scopedAction.value.effort ?? ""),
510
+ mode: permissionModeValue(scopedAction.value.permissionMode),
511
+ },
512
+ {
513
+ sessionId: String(scopedAction.value.sessionId ?? ""),
514
+ updateMessageId: scopedAction.messageId,
515
+ },
516
+ );
403
517
  } else if (kind === "approval") {
404
518
  await this.resolveApproval(scopedAction);
405
519
  }
@@ -445,7 +559,7 @@ export class ProxySessionController {
445
559
  if (!record) throw new Error(`Task not found: ${localSessionId}`);
446
560
  let remote: RemoteSessionSummary | undefined;
447
561
  if (record.remoteSessionId) {
448
- const runtime = this.runtimes.forAgent(this.ensureAgent(record.agentName));
562
+ const runtime = this.runtimes.forAgent(record.agentName);
449
563
  if (runtime.readRemoteSession) {
450
564
  try {
451
565
  remote = await runtime.readRemoteSession(record.remoteSessionId);
@@ -484,7 +598,7 @@ export class ProxySessionController {
484
598
  if (!promptText) throw new Error("The Prompt cannot be empty.");
485
599
  const record = this.store.getSession(localSessionId);
486
600
  if (!record || record.status === "closed") throw new Error(`Task not found: ${localSessionId}`);
487
- const runtime = this.runtimes.forAgent(this.ensureAgent(record.agentName));
601
+ const runtime = this.runtimes.forAgent(record.agentName);
488
602
  const activeTurnId = runtime.getSession(localSessionId)?.activeTurnId;
489
603
  const routedContextKey = this.outbound.getSessionContextKey(localSessionId);
490
604
  const responseContextKey = (activeTurnId ? this.store.getTurnContextKey(activeTurnId) ?? routedContextKey : undefined)
@@ -514,6 +628,7 @@ export class ProxySessionController {
514
628
  responseContextKey,
515
629
  scopedRecord.title,
516
630
  scopedRecord.cwd,
631
+ this.agentLabel(scopedRecord.agentName),
517
632
  );
518
633
  const promptMessageId = await this.outbound.withReplyTarget(
519
634
  responseContextKey,
@@ -540,13 +655,89 @@ export class ProxySessionController {
540
655
  return `The Prompt was posted to the original chat and submitted to the task: ${record.title ?? record.remoteSessionId ?? record.localSessionId}`;
541
656
  }
542
657
 
658
+ async controlCreateTaskGroup(
659
+ localSessionId: string,
660
+ requestedTitle: string | undefined,
661
+ userOpenId: string | undefined,
662
+ requestedProjectCwd?: string,
663
+ forceProjectless = false,
664
+ requestedAgentName?: string,
665
+ ): Promise<ControlTaskGroupResult> {
666
+ const source = this.store.getSession(localSessionId);
667
+ if (!source || source.status === "closed") throw new Error(`Task not found: ${localSessionId}`);
668
+ const agentName = requestedAgentName?.trim() || source.agentName;
669
+ const agent = this.ensureAgent(agentName);
670
+ if (forceProjectless && agent.kind !== "app-server") {
671
+ throw new Error("task newgroup --nodir is only available for Codex agents.");
672
+ }
673
+ const sourceContextKey = this.outbound.getSessionContextKey(localSessionId) ?? source.contextKey;
674
+ const replyTarget = this.outbound.getSessionReplyTarget(localSessionId);
675
+ const boundProjectCwd = forceProjectless
676
+ ? undefined
677
+ : requestedProjectCwd === undefined
678
+ ? detectProjectlessWorkspace(source.cwd) ? undefined : source.cwd
679
+ : resolveUserPath(requestedProjectCwd);
680
+ const created = await this.outbound.withReplyTarget(sourceContextKey, replyTarget, () =>
681
+ this.createFeishuGroupWithTask(
682
+ sourceContextKey,
683
+ agentName,
684
+ requestedTitle,
685
+ userOpenId,
686
+ boundProjectCwd,
687
+ source.agentName === agentName
688
+ ? {
689
+ modelProvider: source.modelProvider,
690
+ model: source.model,
691
+ reasoningEffort: source.reasoningEffort,
692
+ permissionMode: source.permissionMode,
693
+ }
694
+ : {},
695
+ ));
696
+ return {
697
+ sourceLocalSessionId: source.localSessionId,
698
+ group: created.group,
699
+ task: created.task,
700
+ };
701
+ }
702
+
703
+ async controlForkTaskGroup(
704
+ localSessionId: string,
705
+ requestedTitle: string | undefined,
706
+ userOpenId: string | undefined,
707
+ ): Promise<ControlTaskGroupResult> {
708
+ const source = this.store.getSession(localSessionId);
709
+ if (!source || source.status === "closed") throw new Error(`Task not found: ${localSessionId}`);
710
+ const sourceContextKey = this.outbound.getSessionContextKey(localSessionId) ?? source.contextKey;
711
+ const replyTarget = this.outbound.getSessionReplyTarget(localSessionId);
712
+ const plan = await this.prepareForkSession(sourceContextKey, localSessionId, requestedTitle);
713
+ const prepared: ForkGroupSessionPlan = {
714
+ plan,
715
+ sourceDescription: plan.forkedFromHistoricalTurn
716
+ ? "当前任务最近已完成轮次"
717
+ : "当前任务最新轮次",
718
+ };
719
+ const forked = await this.outbound.withReplyTarget(sourceContextKey, replyTarget, () =>
720
+ this.forkPreparedSessionToFeishuGroup(
721
+ sourceContextKey,
722
+ prepared,
723
+ userOpenId ?? "",
724
+ "当前任务",
725
+ ));
726
+ return {
727
+ sourceLocalSessionId: source.localSessionId,
728
+ sourceTurnId: plan.lastTurnId,
729
+ group: forked.group,
730
+ task: forked.task,
731
+ };
732
+ }
733
+
543
734
  private cardActionContextKey(action: CardAction): string {
544
735
  const explicit = typeof action.value.contextKey === "string" ? action.value.contextKey : undefined;
545
736
  if (explicit && baseChatContextKey(explicit) === baseChatContextKey(action.contextKey)) return explicit;
546
737
 
547
738
  const sessionReference = typeof action.value.sessionId === "string" ? action.value.sessionId : undefined;
548
739
  const session = sessionReference
549
- ? this.store.getSession(sessionReference) ?? this.store.findSessionByRemoteSessionId(sessionReference)
740
+ ? this.store.getSession(sessionReference) ?? this.findStoredSessionByReference(sessionReference)
550
741
  : undefined;
551
742
  if (session && baseChatContextKey(session.contextKey) === baseChatContextKey(action.contextKey)) {
552
743
  return session.contextKey;
@@ -575,26 +766,36 @@ export class ProxySessionController {
575
766
  await this.runShellCommand(contextKey, command.command);
576
767
  return;
577
768
  case "new":
578
- if (command.projectless && this.ensureAgent(context.defaultAgent).kind !== "codex") {
579
- throw new Error("/new --nodir 仅支持 Codex Agent。");
769
+ if (command.projectless && this.ensureAgent(context.defaultAgent).kind !== "app-server") {
770
+ throw new Error("/new --nodir 仅支持 App Server Agent。");
580
771
  }
581
772
  await this.createSession(
582
773
  contextKey,
583
774
  context.defaultAgent,
584
- command.projectless ? undefined : command.cwd ?? this.inheritedNewTaskCwd(contextKey),
775
+ command.projectless
776
+ ? undefined
777
+ : command.cwd === undefined
778
+ ? this.inheritedNewTaskCwd(contextKey)
779
+ : resolveUserPath(command.cwd),
585
780
  true,
586
781
  false,
587
782
  undefined,
588
783
  undefined,
589
784
  command.title,
785
+ this.inheritedExecutionSettings(contextKey, context.defaultAgent),
590
786
  );
591
787
  return;
592
788
  case "newgroup":
789
+ if (command.projectless && this.ensureAgent(context.defaultAgent).kind !== "app-server") {
790
+ throw new Error("/newgroup --nodir 仅支持 App Server Agent。");
791
+ }
593
792
  await this.createFeishuGroup(
594
793
  contextKey,
595
794
  context.defaultAgent,
596
795
  command.title,
597
796
  userId,
797
+ command.cwd === undefined ? undefined : resolveUserPath(command.cwd),
798
+ command.projectless === true,
598
799
  );
599
800
  return;
600
801
  case "forkgroup":
@@ -606,7 +807,6 @@ export class ProxySessionController {
606
807
  case "title":
607
808
  await this.setTitle(contextKey, command.title);
608
809
  return;
609
- case "ask":
610
810
  case "prompt":
611
811
  await this.prompt(contextKey, command.text, messageId, replyTarget, localImagePaths);
612
812
  return;
@@ -621,11 +821,7 @@ export class ProxySessionController {
621
821
  return;
622
822
  case "agent":
623
823
  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);
824
+ else await this.openAgentSettings(contextKey);
629
825
  return;
630
826
  case "status":
631
827
  await this.status(contextKey, command.sessionId);
@@ -635,20 +831,19 @@ export class ProxySessionController {
635
831
  return;
636
832
  case "restart":
637
833
  if (!this.lifecycle) throw new Error("当前运行方式不支持自动重启。");
638
- await this.lifecycle.restart(contextKey);
834
+ await this.lifecycle.restart(contextKey, command.force === true);
639
835
  return;
640
836
  case "model":
641
- await this.model(contextKey, command.model);
837
+ await this.openExecutionSettings(contextKey, "model");
838
+ return;
839
+ case "provider":
840
+ await this.openProviderSettings(contextKey);
642
841
  return;
643
842
  case "thinking":
644
- await this.thinking(contextKey, command.effort);
843
+ await this.openExecutionSettings(contextKey, "thinking");
645
844
  return;
646
845
  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。");
846
+ await this.openExecutionSettings(contextKey, "permission");
652
847
  return;
653
848
  case "help":
654
849
  await this.help(contextKey);
@@ -667,7 +862,13 @@ export class ProxySessionController {
667
862
  const turnContextKey = session.lastTurnId
668
863
  ? this.store.getTurnContextKey(session.lastTurnId) ?? context.contextKey
669
864
  : context.contextKey;
670
- this.outbound.registerSession(session.localSessionId, turnContextKey, session.title, session.cwd);
865
+ this.outbound.registerSession(
866
+ session.localSessionId,
867
+ turnContextKey,
868
+ session.title,
869
+ session.cwd,
870
+ this.agentLabel(session.agentName),
871
+ );
671
872
  if (!session.lastTurnId) continue;
672
873
  void this.outbound.resumeDelivery(session.localSessionId, turnContextKey, session.lastTurnId).catch((error: unknown) => {
673
874
  this.logger.warn({ error, sessionId: session.localSessionId }, "Failed to restore persisted turn delivery.");
@@ -731,9 +932,15 @@ export class ProxySessionController {
731
932
  replyTarget?: MessageReplyTarget,
732
933
  localImagePaths?: string[],
733
934
  ): Promise<void> {
734
- const configuredRuntime = this.runtimes.forAgent(this.ensureAgent(record.agentName));
935
+ const configuredRuntime = this.runtimes.forAgent(record.agentName);
735
936
  if (!configuredRuntime.getSession(record.localSessionId)) {
736
- this.outbound.registerSession(record.localSessionId, contextKey, record.title, record.cwd);
937
+ this.outbound.registerSession(
938
+ record.localSessionId,
939
+ contextKey,
940
+ record.title,
941
+ record.cwd,
942
+ this.agentLabel(record.agentName),
943
+ );
737
944
  await this.outbound.startPendingTurn(record.localSessionId, contextKey, record.title, replyTarget, text);
738
945
  }
739
946
  let loaded: LoadedSession;
@@ -967,7 +1174,7 @@ export class ProxySessionController {
967
1174
  private async forkThreadSession(message: IncomingMessage): Promise<void> {
968
1175
  const { anchor, source, snapshot } = this.resolveThreadForkAnchor(message);
969
1176
  const agent = this.ensureAgent(source.agentName);
970
- const runtime = this.runtimes.forAgent(agent);
1177
+ const runtime = this.runtimes.forAgent(source.agentName);
971
1178
  if (runtime.kind !== "codex" || !runtime.forkSession) {
972
1179
  throw new Error("当前 Codex 运行时不支持 fork 任务。");
973
1180
  }
@@ -987,12 +1194,19 @@ export class ProxySessionController {
987
1194
  this.store.updateRuntimeSession(localSessionId, {
988
1195
  runtimeKind: "codex",
989
1196
  title: forkTitle,
1197
+ modelProvider: source.modelProvider,
990
1198
  model: source.model,
991
1199
  reasoningEffort: source.reasoningEffort,
992
1200
  permissionMode: source.permissionMode ?? "auto",
993
1201
  });
994
1202
  this.store.setCurrentSession(message.contextKey, localSessionId);
995
- this.outbound.registerSession(localSessionId, message.contextKey, forkTitle, source.cwd);
1203
+ this.outbound.registerSession(
1204
+ localSessionId,
1205
+ message.contextKey,
1206
+ forkTitle,
1207
+ source.cwd,
1208
+ this.agentLabel(source.agentName),
1209
+ );
996
1210
 
997
1211
  try {
998
1212
  const forked = await runtime.forkSession({
@@ -1002,6 +1216,7 @@ export class ProxySessionController {
1002
1216
  agentName: source.agentName,
1003
1217
  cwd: source.cwd,
1004
1218
  title: forkTitle,
1219
+ modelProvider: source.modelProvider,
1005
1220
  model: source.model,
1006
1221
  reasoningEffort: source.reasoningEffort,
1007
1222
  permissionMode: source.permissionMode ?? "auto",
@@ -1091,7 +1306,7 @@ export class ProxySessionController {
1091
1306
  if (direct?.status === "closed") {
1092
1307
  throw new Error(`找不到任务:${taskId}`);
1093
1308
  }
1094
- const global = direct ?? this.store.findSessionByRemoteSessionId(taskId);
1309
+ const global = direct ?? this.findStoredSessionByReference(taskId);
1095
1310
  source = global ? { ...global, contextKey } : undefined;
1096
1311
  }
1097
1312
 
@@ -1101,25 +1316,27 @@ export class ProxySessionController {
1101
1316
 
1102
1317
  let runtime: AgentRuntime;
1103
1318
  let agentName: string;
1319
+ let remote: RemoteSessionSummary | undefined;
1104
1320
  if (source) {
1105
1321
  agentName = source.agentName;
1106
- runtime = this.runtimes.forAgent(this.ensureAgent(agentName));
1322
+ runtime = this.runtimes.forAgent(agentName);
1107
1323
  } 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");
1324
+ if (!taskId) throw new Error("缺少要 fork 的 Codex 任务 ID。");
1325
+ const resolved = await this.resolveRemoteCodexSession(taskId);
1326
+ agentName = resolved.agentName;
1327
+ runtime = resolved.runtime;
1328
+ remote = resolved.remote;
1112
1329
  }
1113
1330
  if (runtime.kind !== "codex" || !runtime.forkSession) {
1114
1331
  throw new Error("当前 Codex 运行时不支持 fork 任务。");
1115
1332
  }
1116
1333
 
1117
- const remoteSessionId = source?.remoteSessionId ?? taskId;
1334
+ const remoteSessionId = source?.remoteSessionId ?? remote?.id ?? taskId;
1118
1335
  if (!remoteSessionId) throw new Error("当前任务尚未创建 Codex 任务 ID,暂时不能 fork。");
1119
1336
  if (!runtime.readRemoteSession && !source) {
1120
1337
  throw new Error("当前 Codex 运行时不支持读取指定任务。");
1121
1338
  }
1122
- const remote = runtime.readRemoteSession
1339
+ remote ??= runtime.readRemoteSession
1123
1340
  ? await runtime.readRemoteSession(remoteSessionId)
1124
1341
  : undefined;
1125
1342
  const latestTurnId = remote?.lastTurnId ?? source?.lastTurnId;
@@ -1145,7 +1362,8 @@ export class ProxySessionController {
1145
1362
  if (!cwd) throw new Error("指定的 Codex 任务没有可用的工作目录,暂时不能 fork。");
1146
1363
  const sourceTitle = remote?.title ?? source?.title ?? remote?.preview;
1147
1364
  const forkTitle = normalizeTaskTitle(requestedTitle) ?? this.store.nextForkTitle(sourceTitle);
1148
- const model = source?.model;
1365
+ const modelProvider = remote?.modelProvider ?? source?.modelProvider;
1366
+ const model = remote?.model ?? source?.model;
1149
1367
  const reasoningEffort = source?.reasoningEffort;
1150
1368
  const permissionMode = source?.permissionMode ?? "auto";
1151
1369
 
@@ -1158,6 +1376,7 @@ export class ProxySessionController {
1158
1376
  lastTurnId,
1159
1377
  cwd,
1160
1378
  forkTitle,
1379
+ modelProvider,
1161
1380
  model,
1162
1381
  reasoningEffort,
1163
1382
  permissionMode,
@@ -1223,7 +1442,7 @@ export class ProxySessionController {
1223
1442
  if (!source.remoteSessionId || !this.isCodexSession(source)) {
1224
1443
  throw new Error(`${sourceLabel}不是可 fork 的 Codex 任务。`);
1225
1444
  }
1226
- const runtime = this.runtimes.forAgent(this.ensureAgent(source.agentName));
1445
+ const runtime = this.runtimes.forAgent(source.agentName);
1227
1446
  if (runtime.kind !== "codex" || !runtime.forkSession) {
1228
1447
  throw new Error("当前 Codex 运行时不支持 fork 任务。");
1229
1448
  }
@@ -1248,6 +1467,7 @@ export class ProxySessionController {
1248
1467
  lastTurnId,
1249
1468
  cwd: source.cwd,
1250
1469
  forkTitle,
1470
+ modelProvider: source.modelProvider,
1251
1471
  model: source.model,
1252
1472
  reasoningEffort: source.reasoningEffort,
1253
1473
  permissionMode: source.permissionMode ?? "auto",
@@ -1274,6 +1494,7 @@ export class ProxySessionController {
1274
1494
  this.store.updateRuntimeSession(localSessionId, {
1275
1495
  runtimeKind: "codex",
1276
1496
  title: plan.forkTitle,
1497
+ modelProvider: plan.modelProvider,
1277
1498
  model: plan.model,
1278
1499
  reasoningEffort: plan.reasoningEffort,
1279
1500
  permissionMode: plan.permissionMode,
@@ -1287,6 +1508,7 @@ export class ProxySessionController {
1287
1508
  agentName: plan.agentName,
1288
1509
  cwd: plan.cwd,
1289
1510
  title: plan.forkTitle,
1511
+ modelProvider: plan.modelProvider,
1290
1512
  model: plan.model,
1291
1513
  reasoningEffort: plan.reasoningEffort,
1292
1514
  permissionMode: plan.permissionMode,
@@ -1297,7 +1519,13 @@ export class ProxySessionController {
1297
1519
  lastTurnStatus: plan.lastTurnStatus,
1298
1520
  });
1299
1521
  this.store.setCurrentSession(contextKey, localSessionId);
1300
- this.outbound.registerSession(localSessionId, contextKey, forked.title ?? plan.forkTitle, plan.cwd);
1522
+ this.outbound.registerSession(
1523
+ localSessionId,
1524
+ contextKey,
1525
+ forked.title ?? plan.forkTitle,
1526
+ plan.cwd,
1527
+ this.agentLabel(plan.agentName),
1528
+ );
1301
1529
  this.store.audit(contextKey, "session_forked", {
1302
1530
  sourceLocalSessionId: plan.source?.localSessionId,
1303
1531
  sourceRemoteSessionId: plan.remoteSessionId,
@@ -1348,26 +1576,26 @@ export class ProxySessionController {
1348
1576
  if (direct?.status === "closed") {
1349
1577
  throw new Error(`找不到任务:${taskId}`);
1350
1578
  }
1351
- const source = direct ?? this.store.findSessionByRemoteSessionId(taskId);
1579
+ const source = direct ?? this.findStoredSessionByReference(taskId);
1352
1580
  if (source && !this.isCodexSession(source)) {
1353
1581
  throw new Error("指定任务不是 Codex 任务,暂时不能按项目创建新任务。");
1354
1582
  }
1355
1583
 
1356
1584
  let agentName: string;
1357
1585
  let runtime: AgentRuntime;
1586
+ let remote: RemoteSessionSummary | undefined;
1358
1587
  if (source) {
1359
1588
  agentName = source.agentName;
1360
- runtime = this.runtimes.forAgent(this.ensureAgent(agentName));
1589
+ runtime = this.runtimes.forAgent(agentName);
1361
1590
  } 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");
1591
+ const resolved = await this.resolveRemoteCodexSession(taskId);
1592
+ agentName = resolved.agentName;
1593
+ runtime = resolved.runtime;
1594
+ remote = resolved.remote;
1366
1595
  }
1367
1596
 
1368
- const remoteSessionId = source?.remoteSessionId ?? taskId;
1369
- let remote: RemoteSessionSummary | undefined;
1370
- if (runtime.readRemoteSession && remoteSessionId) {
1597
+ const remoteSessionId = source?.remoteSessionId ?? remote?.id ?? taskId;
1598
+ if (!remote && runtime.readRemoteSession && remoteSessionId) {
1371
1599
  try {
1372
1600
  remote = await runtime.readRemoteSession(remoteSessionId);
1373
1601
  } catch (error) {
@@ -1384,6 +1612,7 @@ export class ProxySessionController {
1384
1612
  throw new Error("指定的 Codex 任务没有可用的工作目录,暂时不能按项目创建新任务。");
1385
1613
  }
1386
1614
  const executionSettings: SessionExecutionSettings = {
1615
+ modelProvider: remote?.modelProvider ?? source?.modelProvider,
1387
1616
  model: remote?.model ?? source?.model,
1388
1617
  reasoningEffort: remote?.reasoningEffort ?? source?.reasoningEffort,
1389
1618
  permissionMode: remote?.permissionMode ?? source?.permissionMode ?? "auto",
@@ -1451,21 +1680,28 @@ export class ProxySessionController {
1451
1680
  const agent = this.ensureAgent(agentName);
1452
1681
  const localSessionId = createId("sess");
1453
1682
  const initialTitle = normalizeTaskTitle(requestedTitle ?? prompt ?? "");
1454
- const sessionCwd = cwd === undefined && agent.kind === "codex"
1683
+ const sessionCwd = cwd === undefined && agent.kind === "app-server"
1455
1684
  ? createProjectlessWorkspace({ prompt: initialTitle }).cwd
1456
1685
  : path.resolve(cwd ?? this.config.defaults.cwd);
1457
1686
  const record = this.store.createSession({ localSessionId, contextKey, agentName, cwd: sessionCwd, status: "starting" });
1458
- if (initialTitle || executionSettings.model || executionSettings.reasoningEffort || executionSettings.permissionMode) {
1687
+ if (initialTitle || executionSettings.modelProvider || executionSettings.model || executionSettings.reasoningEffort || executionSettings.permissionMode) {
1459
1688
  this.store.updateRuntimeSession(localSessionId, {
1460
1689
  title: initialTitle,
1690
+ modelProvider: executionSettings.modelProvider,
1461
1691
  model: executionSettings.model,
1462
1692
  reasoningEffort: executionSettings.reasoningEffort,
1463
1693
  permissionMode: executionSettings.permissionMode,
1464
1694
  });
1465
1695
  }
1466
1696
  this.store.setCurrentSession(contextKey, localSessionId);
1467
- this.outbound.registerSession(localSessionId, contextKey, initialTitle, sessionCwd);
1468
- const runtime = this.runtimes.forAgent(agent);
1697
+ this.outbound.registerSession(
1698
+ localSessionId,
1699
+ contextKey,
1700
+ initialTitle,
1701
+ sessionCwd,
1702
+ this.agentLabel(agentName),
1703
+ );
1704
+ const runtime = this.runtimes.forAgent(agentName);
1469
1705
  try {
1470
1706
  if (prepareTurn) {
1471
1707
  await this.outbound.startPendingTurn(localSessionId, contextKey, initialTitle, replyTarget, prompt);
@@ -1475,6 +1711,7 @@ export class ProxySessionController {
1475
1711
  agentName,
1476
1712
  cwd: sessionCwd,
1477
1713
  title: initialTitle,
1714
+ modelProvider: executionSettings.modelProvider,
1478
1715
  model: executionSettings.model,
1479
1716
  reasoningEffort: executionSettings.reasoningEffort,
1480
1717
  permissionMode: executionSettings.permissionMode ?? "auto",
@@ -1500,11 +1737,16 @@ export class ProxySessionController {
1500
1737
  agentName: string,
1501
1738
  requestedTitle: string | undefined,
1502
1739
  userId: string | undefined,
1740
+ requestedProjectCwd?: string,
1741
+ forceProjectless = false,
1503
1742
  ): Promise<void> {
1504
1743
  const source = this.currentSession(sourceContextKey);
1505
- const boundProjectCwd = this.currentProjectCwd(sourceContextKey);
1744
+ const boundProjectCwd = forceProjectless
1745
+ ? undefined
1746
+ : requestedProjectCwd ?? this.currentProjectCwd(sourceContextKey);
1506
1747
  const executionSettings: SessionExecutionSettings = source?.agentName === agentName
1507
1748
  ? {
1749
+ modelProvider: source.modelProvider,
1508
1750
  model: source.model,
1509
1751
  reasoningEffort: source.reasoningEffort,
1510
1752
  permissionMode: source.permissionMode,
@@ -1544,9 +1786,9 @@ export class ProxySessionController {
1544
1786
  userId: string | undefined,
1545
1787
  boundProjectCwd: string | undefined,
1546
1788
  executionSettings: SessionExecutionSettings,
1547
- ): Promise<void> {
1789
+ ): Promise<CreatedFeishuTaskGroup> {
1548
1790
  const explicitTitle = normalizeTaskTitle(requestedTitle);
1549
- const taskTitle = explicitTitle ?? "新任务";
1791
+ const taskTitle = explicitTitle ?? `新任务 (${formatGroupDateSuffix(new Date())})`;
1550
1792
  const group = await this.createFeishuGroupContext(
1551
1793
  sourceContextKey,
1552
1794
  agentName,
@@ -1554,7 +1796,7 @@ export class ProxySessionController {
1554
1796
  userId,
1555
1797
  boundProjectCwd,
1556
1798
  "/newgroup",
1557
- !explicitTitle,
1799
+ false,
1558
1800
  );
1559
1801
 
1560
1802
  let task: SessionRecord;
@@ -1587,14 +1829,17 @@ export class ProxySessionController {
1587
1829
  "群和新任务已创建。",
1588
1830
  `当前任务:${taskDescription}`,
1589
1831
  `当前 Project 目录:${boundProjectCwd ?? "未绑定(Projectless)"}`,
1832
+ `当前 Provider:${task.modelProvider ?? "Codex 默认"}`,
1590
1833
  `当前模型:${task.model ?? "默认"}`,
1591
1834
  `思考强度:${task.reasoningEffort ?? "自动"}`,
1835
+ `权限类型:${task.permissionMode === "confirm" ? "执行前确认" : "自动执行"}`,
1592
1836
  ].join("\n"),
1593
1837
  );
1594
1838
  await this.outbound.sendText(
1595
1839
  sourceContextKey,
1596
1840
  `已创建飞书群:${group.name},并创建新任务 ${taskDescription}。`,
1597
1841
  );
1842
+ return { group, task };
1598
1843
  }
1599
1844
 
1600
1845
  private async forkCurrentSessionToFeishuGroup(
@@ -1642,7 +1887,7 @@ export class ProxySessionController {
1642
1887
  prepared: ForkGroupSessionPlan,
1643
1888
  userId: string,
1644
1889
  sourceSummary: string,
1645
- ): Promise<void> {
1890
+ ): Promise<CreatedFeishuTaskGroup> {
1646
1891
  const { plan } = prepared;
1647
1892
  const boundProjectCwd = detectProjectlessWorkspace(plan.cwd) ? undefined : plan.cwd;
1648
1893
  const group = await this.createFeishuGroupContext(
@@ -1675,6 +1920,7 @@ export class ProxySessionController {
1675
1920
  `已从${prepared.sourceDescription}创建分支。`,
1676
1921
  `当前任务:${taskDescription}`,
1677
1922
  `当前 Project 目录:${boundProjectCwd ?? "未绑定(Projectless)"}`,
1923
+ `当前 Provider:${forked.session.modelProvider ?? "Codex 默认"}`,
1678
1924
  `当前模型:${forked.session.model ?? "默认"}`,
1679
1925
  `思考强度:${forked.session.reasoningEffort ?? "自动"}`,
1680
1926
  `权限类型:${forked.session.permissionMode === "confirm" ? "执行前确认" : "自动执行"}`,
@@ -1684,6 +1930,7 @@ export class ProxySessionController {
1684
1930
  sourceContextKey,
1685
1931
  `已将${sourceSummary} Fork 到飞书群:${group.name};新群当前任务为 ${taskDescription}。`,
1686
1932
  );
1933
+ return { group, task: forked.record };
1687
1934
  }
1688
1935
 
1689
1936
  private async createFeishuGroupContext(
@@ -1711,18 +1958,24 @@ export class ProxySessionController {
1711
1958
  this.store.recordChatContext(groupContextKey, "group");
1712
1959
  this.store.getOrCreateUserContext(groupContextKey, agentName);
1713
1960
  if (boundProjectCwd) this.store.setBoundProjectCwd(groupContextKey, boundProjectCwd);
1714
- return { contextKey: groupContextKey, name: group.name };
1961
+ return { chatId: group.chatId, contextKey: groupContextKey, name: group.name };
1715
1962
  }
1716
1963
 
1717
1964
  private async loadSession(record: SessionRecord): Promise<LoadedSession> {
1718
1965
  const agent = this.ensureAgent(record.agentName);
1719
- const runtime = this.runtimes.forAgent(agent);
1966
+ const runtime = this.runtimes.forAgent(record.agentName);
1720
1967
  const existing = runtime.getSession(record.localSessionId);
1721
1968
  if (existing) return { record, runtime, session: existing };
1722
1969
  const pending = this.sessionLoads.get(record.localSessionId);
1723
1970
  if (pending) return pending;
1724
1971
 
1725
- this.outbound.registerSession(record.localSessionId, record.contextKey, record.title, record.cwd);
1972
+ this.outbound.registerSession(
1973
+ record.localSessionId,
1974
+ record.contextKey,
1975
+ record.title,
1976
+ record.cwd,
1977
+ this.agentLabel(record.agentName),
1978
+ );
1726
1979
  const loading = (async (): Promise<LoadedSession> => {
1727
1980
  if (record.lastTurnId) {
1728
1981
  try {
@@ -1745,23 +1998,25 @@ export class ProxySessionController {
1745
1998
  agentName: record.agentName,
1746
1999
  cwd: record.cwd,
1747
2000
  title: record.title,
2001
+ modelProvider: record.modelProvider,
1748
2002
  model: record.model,
1749
2003
  reasoningEffort: record.reasoningEffort,
1750
2004
  permissionMode,
1751
- activeTurnId: agent.kind === "codex" && record.status === "running" && record.lastTurnStatus === "running"
2005
+ activeTurnId: agent.kind === "app-server" && record.status === "running" && record.lastTurnStatus === "running"
1752
2006
  ? record.lastTurnId
1753
2007
  : undefined,
1754
2008
  lastTurnId: record.lastTurnId,
1755
2009
  lastTurnStatus: record.lastTurnStatus,
1756
2010
  });
1757
2011
  } catch (error) {
1758
- if (!(agent.kind === "codex" && !record.lastTurnId && isMissingRolloutError(error))) throw error;
2012
+ if (!(agent.kind === "app-server" && !record.lastTurnId && isMissingRolloutError(error))) throw error;
1759
2013
  this.logger.warn({ error, sessionId: record.localSessionId }, "Codex task has no rollout; creating a replacement task.");
1760
2014
  session = await runtime.createSession({
1761
2015
  localSessionId: record.localSessionId,
1762
2016
  agentName: record.agentName,
1763
2017
  cwd: record.cwd,
1764
2018
  title: record.title,
2019
+ modelProvider: record.modelProvider,
1765
2020
  model: record.model,
1766
2021
  reasoningEffort: record.reasoningEffort,
1767
2022
  permissionMode,
@@ -1773,6 +2028,7 @@ export class ProxySessionController {
1773
2028
  agentName: record.agentName,
1774
2029
  cwd: record.cwd,
1775
2030
  title: record.title,
2031
+ modelProvider: record.modelProvider,
1776
2032
  model: record.model,
1777
2033
  reasoningEffort: record.reasoningEffort,
1778
2034
  permissionMode,
@@ -1795,6 +2051,7 @@ export class ProxySessionController {
1795
2051
  runtimeKind: session.runtimeKind,
1796
2052
  remoteSessionId: session.remoteSessionId,
1797
2053
  title: session.title,
2054
+ modelProvider: session.modelProvider,
1798
2055
  model: session.model,
1799
2056
  reasoningEffort: session.reasoningEffort,
1800
2057
  permissionMode: session.permissionMode,
@@ -1821,36 +2078,39 @@ export class ProxySessionController {
1821
2078
  });
1822
2079
  }
1823
2080
 
2081
+ let presentationError: unknown;
1824
2082
  try {
1825
2083
  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
- }
2084
+ } catch (error) {
2085
+ presentationError = error;
1846
2086
  }
2087
+
1847
2088
  if (event.type === "turn_completed" || event.type === "turn_cancelled" || event.type === "turn_failed") {
2089
+ const terminalStatus = event.type === "turn_completed"
2090
+ ? "completed"
2091
+ : event.type === "turn_cancelled"
2092
+ ? "cancelled"
2093
+ : "failed";
2094
+ const reactionSession = this.store.getSession(event.sessionId);
2095
+ if (
2096
+ !presentationError
2097
+ && reactionSession?.lastTurnId === event.turnId
2098
+ && reactionSession.lastTurnStatus === terminalStatus
2099
+ ) {
2100
+ await this.finalizeTurnMessageReactions(event.turnId, terminalStatus);
2101
+ }
2102
+
1848
2103
  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);
2104
+ const activeTurnId = latest
2105
+ ? this.runtimes.forAgent(latest.agentName).getSession(event.sessionId)?.activeTurnId
2106
+ : undefined;
2107
+ if (latest?.lastTurnId === event.turnId && !activeTurnId) {
2108
+ this.store.updateSession(event.sessionId, { status: event.type === "turn_failed" ? "failed" : "ready" });
2109
+ await this.scheduleNextQueuedPrompt(event.sessionId);
2110
+ }
1853
2111
  }
2112
+
2113
+ if (presentationError) throw presentationError;
1854
2114
  }
1855
2115
 
1856
2116
  private scheduleNextQueuedPrompt(sessionId: string): Promise<void> {
@@ -1960,7 +2220,7 @@ export class ProxySessionController {
1960
2220
  }
1961
2221
 
1962
2222
  private async cancelSession(record: SessionRecord): Promise<void> {
1963
- const runtime = this.runtimes.forAgent(this.ensureAgent(record.agentName));
2223
+ const runtime = this.runtimes.forAgent(record.agentName);
1964
2224
  if (runtime.kind === "codex" && record.remoteSessionId && runtime.interruptRemoteTurn) {
1965
2225
  if (runtime.getGoal && runtime.setGoal) {
1966
2226
  try {
@@ -2022,29 +2282,295 @@ export class ProxySessionController {
2022
2282
  await loaded.runtime.respondToApproval(sessionId, requestId, decision);
2023
2283
  }
2024
2284
 
2025
- private async model(contextKey: string, model?: string, options: ModelCardOptions = {}): Promise<void> {
2285
+ private async openExecutionSettings(
2286
+ contextKey: string,
2287
+ activeTab: ExecutionSettingsTab,
2288
+ options: ExecutionSettingsCardOptions = {},
2289
+ ): Promise<void> {
2290
+ const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent!);
2291
+ const record = options.sessionId
2292
+ ? this.requireSession(contextKey, options.sessionId)
2293
+ : this.currentSession(contextKey);
2294
+ if (!record && activeTab !== "agent") this.requireCurrentSession(contextKey);
2295
+ const agents = Object.entries(this.config.agents).map(([name, agent]) => ({
2296
+ name,
2297
+ title: agent.title,
2298
+ }));
2299
+
2300
+ let runtimeSettings: Omit<Parameters<CardRenderer["renderExecutionSettings"]>[0],
2301
+ "sessionId" | "contextKey" | "activeTab" | "currentAgent" | "taskAgent" | "agents" | "runtimeSettingsAvailable" | "notice"> = {
2302
+ currentPermissionMode: "auto",
2303
+ providers: [],
2304
+ providerSupported: false,
2305
+ models: [],
2306
+ reasoningOptions: [],
2307
+ };
2308
+ if (record) {
2309
+ const loaded = await this.loadSession(record);
2310
+ const models = await loaded.runtime.listModels();
2311
+ const currentModel = models.find((model) => model.id === loaded.session.model)
2312
+ ?? models.find((model) => model.isDefault);
2313
+ const providerSupported = loaded.runtime.kind === "codex" && Boolean(loaded.runtime.listModelProviders);
2314
+ const providers = providerSupported ? await this.modelProviderOptions(loaded) : [];
2315
+ const currentProvider = loaded.session.modelProvider ?? providers.find((provider) => provider.isDefault)?.id;
2316
+ const currentEffort = currentModel?.supportedReasoningEfforts.some(
2317
+ (option) => option.value === loaded.session.reasoningEffort,
2318
+ )
2319
+ ? loaded.session.reasoningEffort
2320
+ : currentModel?.defaultReasoningEffort ?? loaded.session.reasoningEffort;
2321
+ runtimeSettings = {
2322
+ currentProvider,
2323
+ currentModel: currentModel?.id ?? loaded.session.model,
2324
+ currentEffort,
2325
+ currentPermissionMode: loaded.session.permissionMode,
2326
+ providers,
2327
+ providerSupported,
2328
+ models,
2329
+ reasoningOptions: currentModel?.supportedReasoningEfforts ?? [],
2330
+ };
2331
+ }
2332
+ const card = this.cardRenderer.renderExecutionSettings({
2333
+ ...(record ? { sessionId: record.localSessionId, taskAgent: record.agentName } : {}),
2334
+ contextKey,
2335
+ activeTab,
2336
+ currentAgent: context.defaultAgent,
2337
+ agents,
2338
+ runtimeSettingsAvailable: Boolean(record),
2339
+ ...runtimeSettings,
2340
+ notice: options.notice,
2341
+ });
2342
+ if (options.updateMessageId) {
2343
+ await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
2344
+ } else {
2345
+ await this.outbound.sendInteractiveCard(contextKey, card);
2346
+ }
2347
+ }
2348
+
2349
+ private async openAgentSettings(contextKey: string): Promise<void> {
2350
+ const current = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent!).defaultAgent;
2351
+ if (Object.keys(this.config.agents).length <= 1) {
2352
+ await this.outbound.sendText(contextKey, `当前 Agent:${current}\n当前没有其他 Agent 可以切换。`);
2353
+ return;
2354
+ }
2355
+ await this.openExecutionSettings(contextKey, "agent");
2356
+ }
2357
+
2358
+ private async openProviderSettings(contextKey: string): Promise<void> {
2359
+ const loaded = await this.loadSession(this.requireCurrentSession(contextKey));
2360
+ const providers = loaded.runtime.kind === "codex" && loaded.runtime.listModelProviders
2361
+ ? await this.modelProviderOptions(loaded)
2362
+ : [];
2363
+ const current = loaded.session.modelProvider?.trim()
2364
+ || providers.find((provider) => provider.isDefault)?.id
2365
+ || "运行时默认";
2366
+ if (providers.length <= 1) {
2367
+ await this.outbound.sendText(contextKey, `当前 Provider:${current}\n当前没有其他 Provider 可以切换。`);
2368
+ return;
2369
+ }
2370
+ await this.openExecutionSettings(contextKey, "provider", { sessionId: loaded.record.localSessionId });
2371
+ }
2372
+
2373
+ private async selectProvider(
2374
+ contextKey: string,
2375
+ modelProvider: string,
2376
+ options: ModelCardOptions = {},
2377
+ ): Promise<void> {
2026
2378
  const record = options.sessionId
2027
2379
  ? this.requireSession(contextKey, options.sessionId)
2028
2380
  : this.requireCurrentSession(contextKey);
2029
2381
  const loaded = await this.loadSession(record);
2382
+ await this.assertModelProvider(loaded, modelProvider);
2030
2383
  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,
2384
+ const model = models.find((candidate) => candidate.id === loaded.session.model)
2385
+ ?? models.find((candidate) => candidate.isDefault);
2386
+ if (!model) throw new Error("当前运行时没有可用于 Provider 切换的模型。");
2387
+ const effort = model.supportedReasoningEfforts.some(
2388
+ (candidate) => candidate.value === loaded.session.reasoningEffort,
2389
+ )
2390
+ ? loaded.session.reasoningEffort
2391
+ : model.defaultReasoningEffort ?? model.supportedReasoningEfforts[0]?.value;
2392
+ if (!effort) throw new Error(`模型 ${model.id} 没有可用于 Provider 切换的思考强度。`);
2393
+ await this.applyProviderSettings(contextKey, {
2394
+ provider: modelProvider,
2395
+ model: model.id,
2396
+ effort,
2397
+ mode: loaded.session.permissionMode,
2398
+ }, options);
2399
+ }
2400
+
2401
+ private async openProviderSelector(
2402
+ contextKey: string,
2403
+ options: ExecutionSettingsCardOptions = {},
2404
+ ): Promise<void> {
2405
+ await this.openExecutionSettings(contextKey, "provider", options);
2406
+ }
2407
+
2408
+ private async openProviderModelSelector(
2409
+ contextKey: string,
2410
+ modelProvider: string,
2411
+ permissionMode: PermissionMode,
2412
+ sessionId: string,
2413
+ updateMessageId: string,
2414
+ ): Promise<void> {
2415
+ const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
2416
+ await this.assertModelProvider(loaded, modelProvider);
2417
+ const models = await loaded.runtime.listModels();
2418
+ const card = this.cardRenderer.renderModelSelector({
2419
+ sessionId,
2420
+ contextKey,
2421
+ currentModel: loaded.session.model ?? models.find((model) => model.isDefault)?.id,
2422
+ reasoningEffort: loaded.session.reasoningEffort,
2423
+ models,
2424
+ modelProvider,
2425
+ permissionMode,
2426
+ unifiedSettings: true,
2427
+ });
2428
+ await this.outbound.updateInteractiveCard(contextKey, updateMessageId, card);
2429
+ }
2430
+
2431
+ private async openProviderReasoningSelector(
2432
+ contextKey: string,
2433
+ modelProvider: string,
2434
+ model: string,
2435
+ permissionMode: PermissionMode,
2436
+ sessionId: string,
2437
+ updateMessageId: string,
2438
+ ): Promise<void> {
2439
+ const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
2440
+ await this.assertModelProvider(loaded, modelProvider);
2441
+ const models = await loaded.runtime.listModels();
2442
+ const selected = models.find((candidate) => candidate.id === model);
2443
+ if (!selected) throw new Error(`未知模型:${model}`);
2444
+ const currentEffort = selected.supportedReasoningEfforts.some(
2445
+ (option) => option.value === loaded.session.reasoningEffort,
2446
+ )
2447
+ ? loaded.session.reasoningEffort
2448
+ : selected.defaultReasoningEffort;
2449
+ const card = this.cardRenderer.renderReasoningSelector({
2450
+ sessionId,
2451
+ contextKey,
2452
+ modelProvider,
2453
+ model,
2454
+ currentEffort,
2455
+ options: selected.supportedReasoningEfforts,
2456
+ permissionMode,
2457
+ unifiedSettings: true,
2458
+ });
2459
+ await this.outbound.updateInteractiveCard(contextKey, updateMessageId, card);
2460
+ }
2461
+
2462
+ private async openProviderPermissionSelector(
2463
+ contextKey: string,
2464
+ modelProvider: string,
2465
+ model: string,
2466
+ effort: string,
2467
+ permissionMode: PermissionMode,
2468
+ sessionId: string,
2469
+ updateMessageId: string,
2470
+ ): Promise<void> {
2471
+ const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
2472
+ await this.assertProviderModelSettings(loaded, modelProvider, model, effort);
2473
+ await this.outbound.updateInteractiveCard(
2474
+ contextKey,
2475
+ updateMessageId,
2476
+ this.cardRenderer.renderPermissionSelector({
2477
+ sessionId,
2035
2478
  contextKey,
2036
- currentModel,
2037
- reasoningEffort: loaded.session.reasoningEffort,
2038
- models,
2479
+ modelProvider,
2480
+ model,
2481
+ reasoningEffort: effort,
2482
+ currentMode: permissionMode,
2483
+ }),
2484
+ );
2485
+ }
2486
+
2487
+ private async applyProviderSettings(
2488
+ contextKey: string,
2489
+ settings: { provider: string; model: string; effort: string; mode: PermissionMode },
2490
+ options: ModelCardOptions = {},
2491
+ ): Promise<void> {
2492
+ const record = options.sessionId
2493
+ ? this.requireSession(contextKey, options.sessionId)
2494
+ : this.requireCurrentSession(contextKey);
2495
+ const loaded = await this.loadSession(record);
2496
+ await this.assertProviderModelSettings(loaded, settings.provider, settings.model, settings.effort);
2497
+ if (!loaded.runtime.setExecutionSettings) {
2498
+ throw new Error("当前运行时不支持 Provider 设置。");
2499
+ }
2500
+ const session = await loaded.runtime.setExecutionSettings(loaded.record.localSessionId, {
2501
+ modelProvider: settings.provider,
2502
+ model: settings.model,
2503
+ reasoningEffort: settings.effort,
2504
+ permissionMode: settings.mode,
2505
+ });
2506
+ this.store.updateRuntimeSession(loaded.record.localSessionId, {
2507
+ modelProvider: session.modelProvider ?? settings.provider,
2508
+ model: session.model ?? settings.model,
2509
+ reasoningEffort: session.reasoningEffort ?? settings.effort,
2510
+ permissionMode: session.permissionMode,
2511
+ });
2512
+ const notice = [
2513
+ `Provider 已切换为 ${cardCode(session.modelProvider ?? settings.provider)}`,
2514
+ `模型 ${cardCode(session.model ?? settings.model)}`,
2515
+ `思考强度 ${cardCode(session.reasoningEffort ?? settings.effort)}`,
2516
+ `权限 ${cardCode(session.permissionMode)}`,
2517
+ "从下一次请求生效。",
2518
+ ].join(",");
2519
+ if (options.updateMessageId) {
2520
+ await this.openProviderSelector(contextKey, {
2521
+ sessionId: loaded.record.localSessionId,
2522
+ updateMessageId: options.updateMessageId,
2523
+ notice,
2039
2524
  });
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;
2525
+ } else {
2526
+ await this.outbound.sendText(contextKey, notice.replaceAll("`", ""));
2527
+ }
2528
+ }
2529
+
2530
+ private async assertProviderModelSettings(
2531
+ loaded: LoadedSession,
2532
+ modelProvider: string,
2533
+ model: string,
2534
+ effort: string,
2535
+ ): Promise<void> {
2536
+ await this.assertModelProvider(loaded, modelProvider);
2537
+ const selected = (await loaded.runtime.listModels()).find((candidate) => candidate.id === model);
2538
+ if (!selected) throw new Error(`未知模型:${model}`);
2539
+ if (!selected.supportedReasoningEfforts.some((option) => option.value === effort)) {
2540
+ const supported = selected.supportedReasoningEfforts.map((option) => option.value).join("、") || "无";
2541
+ throw new Error(`模型 ${model} 不支持思考强度 ${effort}。支持的强度:${supported}`);
2046
2542
  }
2543
+ }
2544
+
2545
+ private async assertModelProvider(loaded: LoadedSession, modelProvider: string): Promise<void> {
2546
+ const providers = await this.modelProviderOptions(loaded);
2547
+ if (!providers.some((provider) => provider.id === modelProvider)) {
2548
+ throw new Error(`未知 Provider:${modelProvider}`);
2549
+ }
2550
+ }
2551
+
2552
+ private async modelProviderOptions(loaded: LoadedSession) {
2553
+ if (loaded.runtime.kind !== "codex" || !loaded.runtime.listModelProviders) {
2554
+ throw new Error("当前任务不支持 Provider 设置。");
2555
+ }
2556
+ const providers = await loaded.runtime.listModelProviders();
2557
+ const current = loaded.session.modelProvider?.trim();
2558
+ if (current && !providers.some((provider) => provider.id === current)) {
2559
+ providers.unshift({ id: current });
2560
+ }
2561
+ return providers;
2562
+ }
2047
2563
 
2564
+ private async model(contextKey: string, model?: string, options: ModelCardOptions = {}): Promise<void> {
2565
+ if (!model) {
2566
+ await this.openExecutionSettings(contextKey, "model", options);
2567
+ return;
2568
+ }
2569
+ const record = options.sessionId
2570
+ ? this.requireSession(contextKey, options.sessionId)
2571
+ : this.requireCurrentSession(contextKey);
2572
+ const loaded = await this.loadSession(record);
2573
+ const models = await loaded.runtime.listModels();
2048
2574
  const selected = models.find((item) => item.id === model);
2049
2575
  if (!selected) throw new Error(`未知模型:${model}`);
2050
2576
  const currentEffort = loaded.session.reasoningEffort;
@@ -2062,18 +2588,11 @@ export class ProxySessionController {
2062
2588
  ? `,思考强度已自动调整为 ${nextEffort}`
2063
2589
  : "";
2064
2590
  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
- );
2591
+ await this.openExecutionSettings(contextKey, "model", {
2592
+ sessionId: loaded.record.localSessionId,
2593
+ updateMessageId: options.updateMessageId,
2594
+ notice: `模型已切换为 ${cardCode(model)}${effortMessage},从下一次请求生效。`,
2595
+ });
2077
2596
  return;
2078
2597
  }
2079
2598
  await this.outbound.sendText(contextKey, `模型已切换为 ${model}${effortMessage},从下一次请求生效。`);
@@ -2089,7 +2608,7 @@ export class ProxySessionController {
2089
2608
  }
2090
2609
  const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent!);
2091
2610
  const agent = this.ensureAgent(context.defaultAgent);
2092
- if (agent.kind !== "codex") throw new Error("Goal 模式仅支持 Codex App Server 任务。");
2611
+ if (agent.kind !== "app-server") throw new Error("Goal 模式仅支持 App Server 任务。");
2093
2612
  record = await this.createSession(
2094
2613
  contextKey,
2095
2614
  context.defaultAgent,
@@ -2242,6 +2761,10 @@ export class ProxySessionController {
2242
2761
  }
2243
2762
 
2244
2763
  private async thinking(contextKey: string, effort?: string, options: ThinkingCardOptions = {}): Promise<void> {
2764
+ if (!effort) {
2765
+ await this.openExecutionSettings(contextKey, "thinking", options);
2766
+ return;
2767
+ }
2245
2768
  const record = options.sessionId
2246
2769
  ? this.requireSession(contextKey, options.sessionId)
2247
2770
  : this.requireCurrentSession(contextKey);
@@ -2255,22 +2778,6 @@ export class ProxySessionController {
2255
2778
  }
2256
2779
  const supported = currentModel.supportedReasoningEfforts;
2257
2780
 
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
2781
  if (!supported.some((option) => option.value === effort)) {
2275
2782
  const options = supported.map((option) => option.value).join("、") || "无";
2276
2783
  throw new Error(`不支持的思考强度:${effort}。支持的强度:${options}`);
@@ -2278,48 +2785,45 @@ export class ProxySessionController {
2278
2785
  await loaded.runtime.setReasoningEffort(loaded.record.localSessionId, effort);
2279
2786
  this.store.updateRuntimeSession(loaded.record.localSessionId, { reasoningEffort: effort });
2280
2787
  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
- );
2788
+ await this.openExecutionSettings(contextKey, "thinking", {
2789
+ sessionId: loaded.record.localSessionId,
2790
+ updateMessageId: options.updateMessageId,
2791
+ notice: `思考强度已切换为 ${cardCode(effort)},从下一次请求生效。`,
2792
+ });
2293
2793
  return;
2294
2794
  }
2295
2795
  await this.outbound.sendText(contextKey, `思考强度已切换为 ${effort},从下一次请求生效。`);
2296
2796
  }
2297
2797
 
2298
- private async permissions(contextKey: string, mode?: PermissionMode): Promise<void> {
2299
- const record = this.requireCurrentSession(contextKey);
2798
+ private async permissions(
2799
+ contextKey: string,
2800
+ mode?: PermissionMode,
2801
+ options: ModelCardOptions = {},
2802
+ ): Promise<void> {
2300
2803
  if (!mode) {
2301
- await this.outbound.sendText(contextKey, `当前权限模式:${record.permissionMode ?? "auto"}`);
2804
+ await this.openExecutionSettings(contextKey, "permission", options);
2302
2805
  return;
2303
2806
  }
2807
+ const record = options.sessionId
2808
+ ? this.requireSession(contextKey, options.sessionId)
2809
+ : this.requireCurrentSession(contextKey);
2304
2810
  const loaded = await this.loadSession(record);
2305
2811
  await loaded.runtime.setPermissionMode(record.localSessionId, mode);
2812
+ loaded.session.permissionMode = mode;
2306
2813
  this.store.updateRuntimeSession(record.localSessionId, { permissionMode: mode });
2814
+ if (options.updateMessageId) {
2815
+ await this.openExecutionSettings(contextKey, "permission", {
2816
+ sessionId: loaded.record.localSessionId,
2817
+ updateMessageId: options.updateMessageId,
2818
+ notice: mode === "auto"
2819
+ ? "已切换为自动执行模式,从下一次请求生效。"
2820
+ : "已切换为执行前确认模式,从下一次请求生效。",
2821
+ });
2822
+ return;
2823
+ }
2307
2824
  await this.outbound.sendText(contextKey, mode === "auto" ? "已切换为自动执行模式。" : "已切换为执行前确认模式。");
2308
2825
  }
2309
2826
 
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
2827
  private async listSessions(
2324
2828
  contextKey: string,
2325
2829
  searchTerm?: string,
@@ -2331,39 +2835,46 @@ export class ProxySessionController {
2331
2835
  const localSessions = this.store.listSessions(contextKey).filter((session) =>
2332
2836
  session.status !== "closed" && this.isCodexSession(session),
2333
2837
  );
2334
- let remoteSessions: RemoteSessionSummary[] = [];
2335
- let remoteHint: string | undefined;
2838
+ const remoteSessions: AgentRemoteSessionSummary[] = [];
2839
+ const remoteErrors: string[] = [];
2336
2840
  let remoteHasMore = false;
2337
- const runtime = this.runtimes.get("codex");
2338
- if (runtime.listRemoteSessions) {
2841
+ await Promise.all(this.runtimes.entries("codex").map(async ([agentName, runtime]) => {
2842
+ if (!runtime.listRemoteSessions) return;
2339
2843
  try {
2340
2844
  const page = await runtime.listRemoteSessions({ searchTerm, limit: visibleCount });
2341
- remoteSessions = page.sessions;
2342
- remoteHasMore = Boolean(page.nextCursor);
2845
+ remoteSessions.push(...page.sessions.map((session) => ({ agentName, session })));
2846
+ remoteHasMore ||= Boolean(page.nextCursor);
2343
2847
  } catch (error) {
2344
- this.logger.warn({ error, contextKey }, "Failed to list Codex sessions.");
2345
- remoteHint = `读取 Codex 任务失败:${error instanceof Error ? error.message : String(error)}`;
2848
+ this.logger.warn({ error, contextKey, agentName }, "Failed to list Codex sessions for an Agent.");
2849
+ remoteErrors.push(`${agentName}: ${error instanceof Error ? error.message : String(error)}`);
2346
2850
  }
2347
- }
2851
+ }));
2852
+ const remoteHint = remoteErrors.length > 0
2853
+ ? `部分 Agent 的任务读取失败:${remoteErrors.join(";")}`
2854
+ : undefined;
2348
2855
 
2349
2856
  const entries = mergeTaskList(localSessions, remoteSessions, context.currentSessionId)
2350
- .filter((entry) => !normalizedSearch || [entry.id, entry.title, entry.cwd].some((value) => value.toLowerCase().includes(normalizedSearch)))
2857
+ .filter((entry) => !normalizedSearch
2858
+ || [entry.id, entry.title, entry.cwd, entry.agentName]
2859
+ .some((value) => value.toLowerCase().includes(normalizedSearch)))
2351
2860
  .sort((left, right) => Number(right.current) - Number(left.current)
2352
2861
  || Number(right.active) - Number(left.active)
2353
2862
  || right.updatedAt - left.updatedAt);
2354
2863
  const activeCount = entries.filter((entry) => entry.active).length;
2355
2864
  const visibleEntries = entries.slice(0, visibleCount);
2356
2865
  const hasMore = remoteHasMore || entries.length > visibleCount;
2357
- this.lastSessionListings.set(contextKey, visibleEntries.map((entry) => entry.id));
2866
+ this.lastSessionListings.set(contextKey, visibleEntries.map((entry) => entry.reference));
2358
2867
  const cardEntries = visibleEntries.map((entry, index) => {
2359
2868
  const marker = entry.current ? "✅" : entry.active ? "🟢 **活跃**" : "•";
2360
- const showStop = entry.status === "外部执行中" && entry.id !== options.forceSwitchTaskId;
2869
+ const showStop = entry.status === "外部执行中"
2870
+ && entry.reference !== options.forceSwitchTaskId
2871
+ && entry.id !== options.forceSwitchTaskId;
2361
2872
  const actions: TaskListCardAction[] = entry.current ? [] : [{
2362
2873
  text: showStop ? "Stop" : "Switch",
2363
2874
  type: showStop ? "danger" as const : "default" as const,
2364
2875
  value: {
2365
2876
  action: showStop ? "session_stop" : "session_switch",
2366
- sessionId: entry.id,
2877
+ sessionId: entry.reference,
2367
2878
  ...(searchTerm ? { searchTerm } : {}),
2368
2879
  visibleCount: String(visibleCount),
2369
2880
  contextKey,
@@ -2373,7 +2884,7 @@ export class ProxySessionController {
2373
2884
  text: "New",
2374
2885
  value: {
2375
2886
  action: "session_new",
2376
- sessionId: entry.id,
2887
+ sessionId: entry.reference,
2377
2888
  ...(searchTerm ? { searchTerm } : {}),
2378
2889
  visibleCount: String(visibleCount),
2379
2890
  contextKey,
@@ -2383,7 +2894,7 @@ export class ProxySessionController {
2383
2894
  text: "NewGroup",
2384
2895
  value: {
2385
2896
  action: "session_new_group",
2386
- sessionId: entry.id,
2897
+ sessionId: entry.reference,
2387
2898
  ...(searchTerm ? { searchTerm } : {}),
2388
2899
  visibleCount: String(visibleCount),
2389
2900
  contextKey,
@@ -2393,7 +2904,7 @@ export class ProxySessionController {
2393
2904
  text: "Fork",
2394
2905
  value: {
2395
2906
  action: "session_fork",
2396
- sessionId: entry.id,
2907
+ sessionId: entry.reference,
2397
2908
  ...(searchTerm ? { searchTerm } : {}),
2398
2909
  visibleCount: String(visibleCount),
2399
2910
  contextKey,
@@ -2403,7 +2914,7 @@ export class ProxySessionController {
2403
2914
  text: "ForkGroup",
2404
2915
  value: {
2405
2916
  action: "session_fork_group",
2406
- sessionId: entry.id,
2917
+ sessionId: entry.reference,
2407
2918
  ...(searchTerm ? { searchTerm } : {}),
2408
2919
  visibleCount: String(visibleCount),
2409
2920
  contextKey,
@@ -2413,14 +2924,14 @@ export class ProxySessionController {
2413
2924
  text: "Status",
2414
2925
  value: {
2415
2926
  action: "session_status",
2416
- sessionId: entry.id,
2927
+ sessionId: entry.reference,
2417
2928
  contextKey,
2418
2929
  },
2419
2930
  });
2420
2931
  return {
2421
2932
  lines: [
2422
2933
  `**${index + 1}.** ${marker} **${cardText(entry.title)}** ${cardText(entry.id)}`,
2423
- `${entry.status} · ${entry.updatedLabel} · ${cardText(entry.cwd || "目录未知")}`,
2934
+ `${entry.status} · ${entry.updatedLabel} · ${cardText(entry.agentName)} · ${cardText(entry.cwd || "目录未知")}`,
2424
2935
  ],
2425
2936
  actions,
2426
2937
  };
@@ -2483,6 +2994,46 @@ export class ProxySessionController {
2483
2994
  });
2484
2995
  }
2485
2996
 
2997
+ private findStoredSessionByReference(reference: string, contextKey?: string): SessionRecord | undefined {
2998
+ const scoped = parseRemoteSessionReference(reference);
2999
+ return scoped
3000
+ ? this.store.findSessionByRemoteSessionId(scoped.remoteSessionId, contextKey, scoped.agentName)
3001
+ : this.store.findSessionByRemoteSessionId(reference, contextKey);
3002
+ }
3003
+
3004
+ private async resolveRemoteCodexSession(reference: string): Promise<AgentRemoteSession> {
3005
+ const scoped = parseRemoteSessionReference(reference);
3006
+ const candidates = scoped
3007
+ ? [[scoped.agentName, this.runtimes.forAgent(scoped.agentName)] as const]
3008
+ : this.runtimes.entries("codex");
3009
+ if (candidates.length === 0) throw new Error("未配置 Codex Agent。");
3010
+
3011
+ const reads = await Promise.allSettled(candidates.map(async ([agentName, runtime]) => {
3012
+ if (runtime.kind !== "codex" || !runtime.readRemoteSession) {
3013
+ throw new Error(`Agent ${agentName} 不支持读取远端任务。`);
3014
+ }
3015
+ return {
3016
+ agentName,
3017
+ runtime,
3018
+ remote: await runtime.readRemoteSession(scoped?.remoteSessionId ?? reference),
3019
+ } satisfies AgentRemoteSession;
3020
+ }));
3021
+ const matches = reads
3022
+ .filter((result): result is PromiseFulfilledResult<AgentRemoteSession> => result.status === "fulfilled")
3023
+ .map((result) => result.value);
3024
+ if (matches.length === 1) return matches[0]!;
3025
+ if (matches.length > 1) {
3026
+ throw new Error(`多个 Agent 中存在相同任务 ID:${reference}。请先使用 /sessions,再通过序号选择任务。`);
3027
+ }
3028
+ const details = reads
3029
+ .map((result, index) => result.status === "rejected"
3030
+ ? `${candidates[index]?.[0] ?? "unknown"}: ${runtimeErrorMessage(result.reason)}`
3031
+ : undefined)
3032
+ .filter((detail): detail is string => Boolean(detail))
3033
+ .join(";");
3034
+ throw new Error(`找不到 Codex 任务:${scoped?.remoteSessionId ?? reference}${details ? `(${details})` : ""}`);
3035
+ }
3036
+
2486
3037
  private async switchSession(contextKey: string, reference?: string): Promise<void> {
2487
3038
  this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent!);
2488
3039
  const taskId = this.resolveSessionReference(contextKey, reference);
@@ -2490,27 +3041,28 @@ export class ProxySessionController {
2490
3041
  if (direct?.status === "closed") {
2491
3042
  throw new Error(`找不到任务:${taskId}`);
2492
3043
  }
2493
- const existing = direct ?? this.store.findSessionByRemoteSessionId(taskId);
3044
+ const existing = direct ?? this.findStoredSessionByReference(taskId);
2494
3045
  if (existing) {
2495
- const runtime = this.runtimes.forAgent(this.ensureAgent(existing.agentName));
3046
+ const runtime = this.runtimes.forAgent(existing.agentName);
2496
3047
  await this.assertSessionTurnOwnership(existing, runtime);
2497
3048
  this.store.attachSessionToContext(contextKey, existing.localSessionId);
2498
3049
  this.store.setCurrentSession(contextKey, existing.localSessionId);
2499
- this.outbound.registerSession(existing.localSessionId, contextKey, existing.title, existing.cwd);
3050
+ this.outbound.registerSession(
3051
+ existing.localSessionId,
3052
+ contextKey,
3053
+ existing.title,
3054
+ existing.cwd,
3055
+ this.agentLabel(existing.agentName),
3056
+ );
2500
3057
  await this.outbound.sendText(contextKey, `已切换到任务:${existing.title ?? existing.remoteSessionId ?? taskId}`);
2501
3058
  return;
2502
3059
  }
2503
3060
 
2504
- const runtime = this.runtimes.get("codex");
2505
- if (!runtime.readRemoteSession) throw new Error("当前 Codex 运行时不支持读取任务。");
2506
- const remote = await runtime.readRemoteSession(taskId);
3061
+ const { agentName, remote } = await this.resolveRemoteCodexSession(taskId);
2507
3062
  if (remote.status === "active" || remote.lastTurnStatus === "inProgress") {
2508
3063
  throw new Error(`这个任务正在外部 Codex 中执行,当前不会切换。可使用 /status ${taskId} 查看进度。`);
2509
3064
  }
2510
3065
  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
3066
  const localSessionId = createId("sess");
2515
3067
  this.store.createSession({
2516
3068
  localSessionId,
@@ -2528,7 +3080,13 @@ export class ProxySessionController {
2528
3080
  lastTurnStatus: mapRemoteTurnStatus(remote.lastTurnStatus),
2529
3081
  });
2530
3082
  this.store.setCurrentSession(contextKey, localSessionId);
2531
- this.outbound.registerSession(localSessionId, contextKey, remote.title ?? remote.preview, remote.cwd);
3083
+ this.outbound.registerSession(
3084
+ localSessionId,
3085
+ contextKey,
3086
+ remote.title ?? remote.preview,
3087
+ remote.cwd,
3088
+ this.agentLabel(agentName),
3089
+ );
2532
3090
  await this.outbound.sendText(
2533
3091
  contextKey,
2534
3092
  `已切换到任务:${remote.title ?? remote.preview ?? remote.id}。历史消息不会重新发送。`,
@@ -2543,17 +3101,16 @@ export class ProxySessionController {
2543
3101
  return;
2544
3102
  }
2545
3103
 
2546
- const existing = this.store.findSessionByRemoteSessionId(taskId);
3104
+ const existing = this.findStoredSessionByReference(taskId);
2547
3105
  if (existing) {
2548
3106
  await this.cancelSession({ ...existing, contextKey });
2549
3107
  return;
2550
3108
  }
2551
3109
 
2552
- const runtime = this.runtimes.get("codex");
3110
+ const { runtime, remote } = await this.resolveRemoteCodexSession(taskId);
2553
3111
  if (!runtime.readRemoteSession || !runtime.interruptRemoteTurn) {
2554
3112
  throw new Error("当前 Codex 运行时不支持停止外部任务。");
2555
3113
  }
2556
- const remote = await runtime.readRemoteSession(taskId);
2557
3114
  const turnId = remote.status === "active" || remote.lastTurnStatus === "inProgress"
2558
3115
  ? remote.lastTurnId
2559
3116
  : undefined;
@@ -2628,11 +3185,22 @@ export class ProxySessionController {
2628
3185
  return undefined;
2629
3186
  }
2630
3187
 
2631
- private async setDefaultAgent(contextKey: string, agentName: string): Promise<void> {
3188
+ private async setDefaultAgent(
3189
+ contextKey: string,
3190
+ agentName: string,
3191
+ options: ExecutionSettingsCardOptions = {},
3192
+ ): Promise<void> {
2632
3193
  this.ensureAgent(agentName);
2633
3194
  this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent!);
2634
3195
  this.store.setDefaultAgent(contextKey, agentName);
2635
- await this.outbound.sendText(contextKey, `默认 agent 已切换为:${agentName}`);
3196
+ if (Object.keys(this.config.agents).length <= 1) {
3197
+ await this.outbound.sendText(contextKey, `当前 Agent:${agentName}\n当前没有其他 Agent 可以切换。`);
3198
+ return;
3199
+ }
3200
+ await this.openExecutionSettings(contextKey, "agent", {
3201
+ ...options,
3202
+ notice: `默认 Agent 已切换为 ${cardCode(agentName)},从下一次新建任务生效。`,
3203
+ });
2636
3204
  }
2637
3205
 
2638
3206
  private async status(
@@ -2647,7 +3215,7 @@ export class ProxySessionController {
2647
3215
  const direct = this.store.getSession(targetSessionId);
2648
3216
  current = direct
2649
3217
  ? { ...direct, contextKey }
2650
- : this.store.findSessionByRemoteSessionId(targetSessionId, contextKey);
3218
+ : this.findStoredSessionByReference(targetSessionId, contextKey);
2651
3219
  if (!current) {
2652
3220
  await this.statusForCodexTask(contextKey, targetSessionId, options);
2653
3221
  return;
@@ -2658,7 +3226,7 @@ export class ProxySessionController {
2658
3226
 
2659
3227
  if (current && current.status === "running") {
2660
3228
  try {
2661
- const runtime = this.runtimes.forAgent(this.ensureAgent(current.agentName));
3229
+ const runtime = this.runtimes.forAgent(current.agentName);
2662
3230
  if (runtime.getSession(current.localSessionId)) {
2663
3231
  await runtime.synchronizeSession(current.localSessionId);
2664
3232
  current = this.store.getSession(current.localSessionId) ?? current;
@@ -2672,7 +3240,7 @@ export class ProxySessionController {
2672
3240
  let remote: RemoteSessionSummary | undefined;
2673
3241
  let goal: RuntimeGoal | undefined;
2674
3242
  if (current?.remoteSessionId) {
2675
- const runtime = this.runtimes.forAgent(this.ensureAgent(current.agentName));
3243
+ const runtime = this.runtimes.forAgent(current.agentName);
2676
3244
  if (runtime.readRemoteSession) {
2677
3245
  try {
2678
3246
  remote = await runtime.readRemoteSession(current.remoteSessionId);
@@ -2700,7 +3268,7 @@ export class ProxySessionController {
2700
3268
  taskLines.push("无。直接发送消息即可创建一个未指定项目的 Codex 任务。");
2701
3269
  } else {
2702
3270
  const agent = this.ensureAgent(current.agentName);
2703
- const runtimeSession = this.runtimes.forAgent(agent).getSession(current.localSessionId);
3271
+ const runtimeSession = this.runtimes.forAgent(current.agentName).getSession(current.localSessionId);
2704
3272
  activeTurnId = runtimeSession?.activeTurnId;
2705
3273
  const remoteActive = isRemoteSessionActive(remote);
2706
3274
  activeTurnId = remoteActive ? remote?.lastTurnId ?? activeTurnId : activeTurnId;
@@ -2715,7 +3283,7 @@ export class ProxySessionController {
2715
3283
  taskLines.push(
2716
3284
  `**标题**:${cardCode(current.title ?? "未命名任务")}`,
2717
3285
  `**工作目录**:${cardCode(current.cwd)}`,
2718
- `**模型 / 思考强度**:${cardCode(current.model ?? "默认")} / ${cardCode(current.reasoningEffort ?? "自动")}`,
3286
+ `**Provider / 模型 / 思考强度**:${cardCode(current.modelProvider ?? "Codex 默认")} / ${cardCode(current.model ?? "默认")} / ${cardCode(current.reasoningEffort ?? "自动")}`,
2719
3287
  `**状态 / 最近结果**:${statusLabel} / ${resultLabel}`,
2720
3288
  `**Agent**:${cardCode(agent.title)}`,
2721
3289
  `**权限 / 任务范围**:${current.permissionMode === "confirm" ? "执行前确认" : "自动执行"} / ${detectProjectlessWorkspace(current.cwd) ? "未指定项目" : "指定项目"}`,
@@ -2752,12 +3320,17 @@ export class ProxySessionController {
2752
3320
  const title = targetSessionId && current
2753
3321
  ? `Codex 状态:${truncateText((current.title ?? current.remoteSessionId ?? current.localSessionId).replace(/\s+/g, " "), 40)}`
2754
3322
  : "Codex 状态";
2755
- const taskId = current?.remoteSessionId ?? current?.localSessionId;
3323
+ const taskId = current?.remoteSessionId
3324
+ ? remoteSessionReference(current.agentName, current.remoteSessionId)
3325
+ : current?.localSessionId;
2756
3326
  const isCurrent = Boolean(current && current.localSessionId === context.currentSessionId);
2757
3327
  const remoteActive = isRemoteSessionActive(remote);
2758
3328
  const botOwnsActiveTurn = Boolean(localCurrent && remote && isBotOwnedActiveTurn(localCurrent, remote));
2759
3329
  const active = Boolean(activeTurnId || current?.status === "running" || remoteActive);
2760
- const forceSwitch = Boolean(taskId && options.forceSwitchTaskId === taskId);
3330
+ const forceSwitch = Boolean(taskId && (
3331
+ options.forceSwitchTaskId === taskId
3332
+ || options.forceSwitchTaskId === current?.remoteSessionId
3333
+ ));
2761
3334
  const taskActions: TaskListCardAction[] = !taskId ? []
2762
3335
  : remoteActive && !botOwnsActiveTurn && !forceSwitch
2763
3336
  ? [statusCardAction("Stop", "danger", "session_stop", taskId, contextKey)]
@@ -2777,12 +3350,11 @@ export class ProxySessionController {
2777
3350
 
2778
3351
  private async statusForCodexTask(
2779
3352
  contextKey: string,
2780
- remoteSessionId: string,
3353
+ reference: string,
2781
3354
  options: StatusCardOptions = {},
2782
3355
  ): Promise<void> {
2783
- const runtime = this.runtimes.get("codex");
2784
- if (!runtime.readRemoteSession) throw new Error("当前 Codex 运行时不支持读取外部任务状态。");
2785
- const remote = await runtime.readRemoteSession(remoteSessionId);
3356
+ const { agentName, remote } = await this.resolveRemoteCodexSession(reference);
3357
+ const actionReference = remoteSessionReference(agentName, remote.id);
2786
3358
  const sections: CardSection[] = [
2787
3359
  {
2788
3360
  title: "指定任务",
@@ -2790,6 +3362,7 @@ export class ProxySessionController {
2790
3362
  `**标题**:${cardCode(remote.title ?? remote.preview ?? "未命名任务")}`,
2791
3363
  `**工作目录**:${cardCode(remote.cwd || "目录未知")}`,
2792
3364
  `**状态 / 当前任务**:${remoteSessionDetailStatus(remote)} / 未切换`,
3365
+ `**Agent**:${cardCode(agentName)}`,
2793
3366
  `**Codex 任务 ID**:${cardCode(remote.id)}`,
2794
3367
  `**最近回合**:${cardCode(remote.lastTurnId ?? "无")} ${remoteTurnStatusLabel(remote.lastTurnStatus)}`,
2795
3368
  `**创建时间 / 最近活动**:${formatRemoteTime(remote.createdAt)} / ${formatRemoteTime(latestRemoteTimestamp(remote.recencyAt, remote.updatedAt))}`,
@@ -2809,14 +3382,16 @@ export class ProxySessionController {
2809
3382
  },
2810
3383
  ];
2811
3384
  const title = `Codex 状态:${truncateText((remote.title ?? remote.preview ?? remote.id).replace(/\s+/g, " "), 40)}`;
2812
- const showStop = isRemoteSessionActive(remote) && options.forceSwitchTaskId !== remote.id;
3385
+ const showStop = isRemoteSessionActive(remote)
3386
+ && options.forceSwitchTaskId !== actionReference
3387
+ && options.forceSwitchTaskId !== remote.id;
2813
3388
  const actions = [
2814
- statusRefreshAction(remote.id, contextKey),
3389
+ statusRefreshAction(actionReference, contextKey),
2815
3390
  statusCardAction(
2816
3391
  showStop ? "Stop" : "Switch",
2817
3392
  showStop ? "danger" : "default",
2818
3393
  showStop ? "session_stop" : "session_switch",
2819
- remote.id,
3394
+ actionReference,
2820
3395
  contextKey,
2821
3396
  ),
2822
3397
  ];
@@ -2840,7 +3415,7 @@ export class ProxySessionController {
2840
3415
  title: "任务管理",
2841
3416
  lines: [
2842
3417
  "**/new [title] [--dir &#60;cwd&#62; | --nodir]** 使用默认 Agent 创建任务;--nodir 强制创建 Projectless 任务",
2843
- "**/newgroup [title]** 创建飞书群和新任务;继承当前项目与执行设置",
3418
+ "**/newgroup [title] [--dir &#60;cwd&#62; | --nodir]** 创建飞书群和新任务;参数与 /new 相同",
2844
3419
  "**/forkgroup [title]** 从当前任务最新已完成轮次创建分支并绑定到新群",
2845
3420
  "**/fork [序号或任务 ID]** 从当前或指定任务创建分支;运行中使用最近已完成轮次",
2846
3421
  "**/title &#60;新标题&#62;** 修改当前任务的标题",
@@ -2855,22 +3430,23 @@ export class ProxySessionController {
2855
3430
  "**/stop** 停止当前执行",
2856
3431
  "**/queue &#60;prompt&#62;**(兼容 **/nosteer**) 不追加到当前轮次,按顺序排队为后续轮次",
2857
3432
  "**/goal [目标]** 查看或创建长任务 Goal;支持 pause、resume、edit、clear",
2858
- "**/model [name]** 查看或切换模型",
2859
- "**/thinking [level]** 查看或设置思考强度",
2860
- "**/permissions [auto|confirm]** 查看或切换确认模式",
3433
+ "**/provider** 选择 AI 服务提供商;没有其他候选时仅显示当前 Provider",
3434
+ "**/model** 选择当前任务使用的模型",
3435
+ "**/thinking** 设置模型的思考强度",
3436
+ "**/permissions** 设置执行工具前是否需要确认",
2861
3437
  ],
2862
3438
  },
2863
3439
  {
2864
3440
  title: "Agent",
2865
3441
  lines: [
2866
- "**/agent [name]** 查看或设置新任务的默认 Agent",
3442
+ "**/agent [name]** 选择新任务使用的默认 Agent;仅配置一个时直接显示当前 Agent",
2867
3443
  ],
2868
3444
  },
2869
3445
  {
2870
3446
  title: "系统",
2871
3447
  lines: [
2872
3448
  "**/status [序号或任务 ID]** 查看当前或指定任务的详细状态",
2873
- "**/restart** 重启 Agent Bot 并恢复会话",
3449
+ "**/restart [--force]** 默认安全重启;--force 立即重启",
2874
3450
  "**/help** 显示本帮助",
2875
3451
  ],
2876
3452
  },
@@ -2884,9 +3460,13 @@ export class ProxySessionController {
2884
3460
  return agent;
2885
3461
  }
2886
3462
 
3463
+ private agentLabel(agentName: string): string {
3464
+ return this.ensureAgent(agentName).title;
3465
+ }
3466
+
2887
3467
  private isCodexSession(session: SessionRecord): boolean {
2888
3468
  if (session.runtimeKind) return session.runtimeKind === "codex";
2889
- return this.config.agents[session.agentName]?.kind === "codex";
3469
+ return this.config.agents[session.agentName]?.kind === "app-server";
2890
3470
  }
2891
3471
 
2892
3472
  private currentSession(contextKey: string): SessionRecord | undefined {
@@ -2904,6 +3484,17 @@ export class ProxySessionController {
2904
3484
  : current.cwd;
2905
3485
  }
2906
3486
 
3487
+ private inheritedExecutionSettings(contextKey: string, agentName: string): SessionExecutionSettings {
3488
+ const current = this.currentSession(contextKey);
3489
+ if (!current || current.agentName !== agentName) return {};
3490
+ return {
3491
+ modelProvider: current.modelProvider,
3492
+ model: current.model,
3493
+ reasoningEffort: current.reasoningEffort,
3494
+ permissionMode: current.permissionMode,
3495
+ };
3496
+ }
3497
+
2907
3498
  private currentProjectCwd(contextKey: string): string | undefined {
2908
3499
  const current = this.currentSession(contextKey);
2909
3500
  return current && !detectProjectlessWorkspace(current.cwd) ? current.cwd : undefined;
@@ -2969,7 +3560,9 @@ function statusRefreshAction(
2969
3560
  }
2970
3561
 
2971
3562
  interface UnifiedTaskListEntry {
3563
+ reference: string;
2972
3564
  id: string;
3565
+ agentName: string;
2973
3566
  title: string;
2974
3567
  cwd: string;
2975
3568
  status: string;
@@ -2981,17 +3574,17 @@ interface UnifiedTaskListEntry {
2981
3574
 
2982
3575
  function mergeTaskList(
2983
3576
  localSessions: SessionRecord[],
2984
- remoteSessions: RemoteSessionSummary[],
3577
+ remoteSessions: AgentRemoteSessionSummary[],
2985
3578
  currentLocalSessionId?: string,
2986
3579
  ): UnifiedTaskListEntry[] {
2987
3580
  const localByRemoteId = new Map(
2988
3581
  localSessions
2989
3582
  .filter((session) => session.runtimeKind === "codex" && session.remoteSessionId)
2990
- .map((session) => [session.remoteSessionId!, session]),
3583
+ .map((session) => [agentRemoteKey(session.agentName, session.remoteSessionId!), session]),
2991
3584
  );
2992
3585
  const representedLocalIds = new Set<string>();
2993
- const entries = remoteSessions.map((remote): UnifiedTaskListEntry => {
2994
- const local = localByRemoteId.get(remote.id);
3586
+ const entries = remoteSessions.map(({ agentName, session: remote }): UnifiedTaskListEntry => {
3587
+ const local = localByRemoteId.get(agentRemoteKey(agentName, remote.id));
2995
3588
  if (local) representedLocalIds.add(local.localSessionId);
2996
3589
  const active = remote.status === "active" || remote.lastTurnStatus === "inProgress";
2997
3590
  const status = remote.status === "active" || remote.lastTurnStatus === "inProgress"
@@ -2999,7 +3592,9 @@ function mergeTaskList(
2999
3592
  : remoteSessionStatusLabel(remote.status);
3000
3593
  const recencyAt = remote.recencyAt ?? remote.updatedAt;
3001
3594
  return {
3595
+ reference: remoteSessionReference(agentName, remote.id),
3002
3596
  id: remote.id,
3597
+ agentName,
3003
3598
  title: remote.title ?? remote.preview ?? local?.title ?? "未命名任务",
3004
3599
  cwd: remote.cwd || local?.cwd || "",
3005
3600
  status,
@@ -3014,7 +3609,11 @@ function mergeTaskList(
3014
3609
  if (representedLocalIds.has(local.localSessionId)) continue;
3015
3610
  const updatedAt = Date.parse(local.updatedAt);
3016
3611
  entries.push({
3612
+ reference: local.remoteSessionId
3613
+ ? remoteSessionReference(local.agentName, local.remoteSessionId)
3614
+ : local.localSessionId,
3017
3615
  id: local.remoteSessionId ?? local.localSessionId,
3616
+ agentName: local.agentName,
3018
3617
  title: local.title ?? "未命名任务",
3019
3618
  cwd: local.cwd,
3020
3619
  status: sessionStatusLabel(local.status),
@@ -3027,6 +3626,32 @@ function mergeTaskList(
3027
3626
  return entries;
3028
3627
  }
3029
3628
 
3629
+ function agentRemoteKey(agentName: string, remoteSessionId: string): string {
3630
+ return `${agentName}\u0000${remoteSessionId}`;
3631
+ }
3632
+
3633
+ function remoteSessionReference(agentName: string, remoteSessionId: string): string {
3634
+ return `${REMOTE_SESSION_REFERENCE_PREFIX}${encodeURIComponent(agentName)}:${encodeURIComponent(remoteSessionId)}`;
3635
+ }
3636
+
3637
+ function parseRemoteSessionReference(reference: string): { agentName: string; remoteSessionId: string } | undefined {
3638
+ if (!reference.startsWith(REMOTE_SESSION_REFERENCE_PREFIX)) return undefined;
3639
+ const value = reference.slice(REMOTE_SESSION_REFERENCE_PREFIX.length);
3640
+ const separator = value.indexOf(":");
3641
+ if (separator <= 0 || separator === value.length - 1) return undefined;
3642
+ try {
3643
+ const agentName = decodeURIComponent(value.slice(0, separator));
3644
+ const remoteSessionId = decodeURIComponent(value.slice(separator + 1));
3645
+ return agentName && remoteSessionId ? { agentName, remoteSessionId } : undefined;
3646
+ } catch {
3647
+ return undefined;
3648
+ }
3649
+ }
3650
+
3651
+ function runtimeErrorMessage(error: unknown): string {
3652
+ return error instanceof Error ? error.message : String(error);
3653
+ }
3654
+
3030
3655
  function isMissingRolloutError(error: unknown): boolean {
3031
3656
  const message = error instanceof Error ? error.message : String(error);
3032
3657
  return /no rollout found|rollout[^\n]*(?:not found|missing)|thread\/resume failed/i.test(message);
@@ -3208,6 +3833,21 @@ function cardCode(value: string): string {
3208
3833
  return `\`${value.replaceAll("`", "'")}\``;
3209
3834
  }
3210
3835
 
3836
+ function permissionModeValue(value: unknown): PermissionMode {
3837
+ if (value === "auto" || value === "confirm") return value;
3838
+ throw new Error("权限模式只能是 auto 或 confirm。");
3839
+ }
3840
+
3841
+ function executionSettingsTabValue(value: unknown): ExecutionSettingsTab {
3842
+ if (value === "agent" || value === "provider" || value === "model" || value === "thinking" || value === "permission") return value;
3843
+ throw new Error("设置卡片的 tab 无效,请重新发送设置命令。");
3844
+ }
3845
+
3846
+ function requiredCardMessageId(value: string | undefined): string {
3847
+ if (value) return value;
3848
+ throw new Error("无法更新设置卡片,请重新发送设置命令。");
3849
+ }
3850
+
3211
3851
  function remoteSessionStatusLabel(status: RemoteSessionSummary["status"]): string {
3212
3852
  const labels: Record<RemoteSessionSummary["status"], string> = {
3213
3853
  active: "执行中",
@@ -3243,6 +3883,10 @@ function mergeRemoteTaskStatus(record: SessionRecord, remote?: RemoteSessionSumm
3243
3883
  ...record,
3244
3884
  title: remote.title ?? record.title,
3245
3885
  cwd: remote.cwd || record.cwd,
3886
+ modelProvider: remote.modelProvider ?? record.modelProvider,
3887
+ model: remote.model ?? record.model,
3888
+ reasoningEffort: remote.reasoningEffort ?? record.reasoningEffort,
3889
+ permissionMode: remote.permissionMode ?? record.permissionMode,
3246
3890
  status: isRemoteSessionActive(remote)
3247
3891
  ? "running"
3248
3892
  : remote.lastTurnStatus
@@ -3299,9 +3943,7 @@ function isBotOwnedActiveTurn(record: SessionRecord, remote: RemoteSessionSummar
3299
3943
  function isQueueIndependentCommand(command: Command): boolean {
3300
3944
  if (["stop", "status", "restart", "help", "sessions", "goal", "nosteer"].includes(command.type)) return true;
3301
3945
  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;
3946
+ if (["model", "provider", "thinking", "permissions"].includes(command.type)) return true;
3305
3947
  return false;
3306
3948
  }
3307
3949
 
@@ -3311,7 +3953,7 @@ function parseSessionVisibleCount(value: unknown): number {
3311
3953
  }
3312
3954
 
3313
3955
  function isPromptCommand(command: Command): boolean {
3314
- return command.type === "ask" || command.type === "prompt" || command.type === "nosteer";
3956
+ return command.type === "prompt" || command.type === "nosteer";
3315
3957
  }
3316
3958
 
3317
3959
  function runtimePrompt(text: string, localImagePaths?: string[]): RuntimePrompt {
@@ -3332,8 +3974,8 @@ function formatNewGroupName(
3332
3974
  includeTimestamp: boolean,
3333
3975
  ): string {
3334
3976
  const prefix = `[${agentName}] `;
3335
- const project = formatGroupProjectDirectory(projectCwd);
3336
- const titlePrefix = `${prefix}${project} `;
3977
+ const projectPrefix = projectCwd ? `${formatGroupProjectDirectory(projectCwd)} ` : "";
3978
+ const titlePrefix = `${prefix}${projectPrefix}`;
3337
3979
  const timestampSuffix = includeTimestamp ? ` (${formatGroupDateSuffix(date)})` : "";
3338
3980
  const availableTitleLength = FEISHU_GROUP_NAME_MAX_LENGTH
3339
3981
  - Array.from(titlePrefix).length
@@ -3345,8 +3987,8 @@ function formatNewGroupName(
3345
3987
  const FEISHU_GROUP_NAME_MAX_LENGTH = 60;
3346
3988
  const GROUP_PROJECT_DIRECTORY_MAX_LENGTH = 15;
3347
3989
 
3348
- function formatGroupProjectDirectory(projectCwd: string | undefined): string {
3349
- const value = projectCwd ? abbreviateHomeDirectory(projectCwd) : "Projectless";
3990
+ function formatGroupProjectDirectory(projectCwd: string): string {
3991
+ const value = abbreviateHomeDirectory(projectCwd);
3350
3992
  if (Array.from(value).length <= GROUP_PROJECT_DIRECTORY_MAX_LENGTH) return `[${value}]`;
3351
3993
 
3352
3994
  const levels = value