@keyou007/agent-bot 0.1.9 → 0.1.11

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