@keyou007/agent-bot 0.1.24-alpha.4 → 0.1.24-alpha.6

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 (108) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +42 -6
  3. package/README.zh.md +42 -6
  4. package/SECURITY.md +12 -0
  5. package/config.example.yaml +5 -0
  6. package/dist/cli/controlProtocol.js.map +1 -1
  7. package/dist/cli/serverStatus.js +1 -0
  8. package/dist/cli/serverStatus.js.map +1 -1
  9. package/dist/cli.js +6 -1
  10. package/dist/cli.js.map +1 -1
  11. package/dist/codex/AppServerPermissions.js +34 -0
  12. package/dist/codex/AppServerPermissions.js.map +1 -0
  13. package/dist/codex/CodexLocalActivityDetector.js +37 -5
  14. package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
  15. package/dist/codex/CodexRuntime.js +238 -127
  16. package/dist/codex/CodexRuntime.js.map +1 -1
  17. package/dist/codex/ProviderSwitch.js +12 -13
  18. package/dist/codex/ProviderSwitch.js.map +1 -1
  19. package/dist/codex/ThreadWriterProcess.js +19 -5
  20. package/dist/codex/ThreadWriterProcess.js.map +1 -1
  21. package/dist/config/AgentExecutionDefaultsStore.js +4 -0
  22. package/dist/config/AgentExecutionDefaultsStore.js.map +1 -1
  23. package/dist/config/loadConfig.js +4 -0
  24. package/dist/config/loadConfig.js.map +1 -1
  25. package/dist/config/schema.js +3 -0
  26. package/dist/config/schema.js.map +1 -1
  27. package/dist/feishu/CardRenderer.js +146 -72
  28. package/dist/feishu/CardRenderer.js.map +1 -1
  29. package/dist/feishu/FeishuConnector.js +1 -0
  30. package/dist/feishu/FeishuConnector.js.map +1 -1
  31. package/dist/feishu/FeishuTurnPresenter.js +121 -17
  32. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  33. package/dist/index.js +43 -6
  34. package/dist/index.js.map +1 -1
  35. package/dist/local-files/CsvPreview.js +148 -0
  36. package/dist/local-files/CsvPreview.js.map +1 -0
  37. package/dist/local-files/LocalFileViewerServer.js +104 -14
  38. package/dist/local-files/LocalFileViewerServer.js.map +1 -1
  39. package/dist/local-files/TurnPreviewPage.js +128 -12
  40. package/dist/local-files/TurnPreviewPage.js.map +1 -1
  41. package/dist/presentation/TurnStateReducer.js +80 -6
  42. package/dist/presentation/TurnStateReducer.js.map +1 -1
  43. package/dist/proxy/ProxySessionController.js +767 -260
  44. package/dist/proxy/ProxySessionController.js.map +1 -1
  45. package/dist/proxy/SessionOperationQueue.js +14 -0
  46. package/dist/proxy/SessionOperationQueue.js.map +1 -0
  47. package/dist/runtime/AcpRuntimeAdapter.js +4 -2
  48. package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
  49. package/dist/state/StateStore.js +22 -0
  50. package/dist/state/StateStore.js.map +1 -1
  51. package/dist/state/TurnPreviewJournal.js +19 -7
  52. package/dist/state/TurnPreviewJournal.js.map +1 -1
  53. package/dist/state/migrations.js +6 -0
  54. package/dist/state/migrations.js.map +1 -1
  55. package/dist/supervision/SafeRestartNotifier.js +1 -0
  56. package/dist/supervision/SafeRestartNotifier.js.map +1 -1
  57. package/dist/supervision/SafeRestartScheduler.js +19 -6
  58. package/dist/supervision/SafeRestartScheduler.js.map +1 -1
  59. package/dist/terminal/TerminalCards.js +13 -0
  60. package/dist/terminal/TerminalCards.js.map +1 -0
  61. package/dist/terminal/TerminalLinks.js +76 -0
  62. package/dist/terminal/TerminalLinks.js.map +1 -0
  63. package/dist/terminal/TerminalPage.js +212 -0
  64. package/dist/terminal/TerminalPage.js.map +1 -0
  65. package/dist/terminal/TerminalProcess.js +71 -0
  66. package/dist/terminal/TerminalProcess.js.map +1 -0
  67. package/dist/terminal/TerminalServer.js +639 -0
  68. package/dist/terminal/TerminalServer.js.map +1 -0
  69. package/docs/technical-reference.md +53 -3
  70. package/docs/technical-reference.zh.md +52 -2
  71. package/npm-shrinkwrap.json +55 -5
  72. package/package.json +11 -4
  73. package/skills/agent-bot/SKILL.md +15 -2
  74. package/src/cli/controlProtocol.ts +2 -1
  75. package/src/cli/serverStatus.ts +1 -0
  76. package/src/cli.ts +6 -1
  77. package/src/codex/AppServerPermissions.ts +39 -0
  78. package/src/codex/CodexLocalActivityDetector.ts +34 -4
  79. package/src/codex/CodexRuntime.ts +243 -139
  80. package/src/codex/ProviderSwitch.ts +11 -12
  81. package/src/codex/ThreadWriterProcess.ts +17 -9
  82. package/src/config/AgentExecutionDefaultsStore.ts +2 -0
  83. package/src/config/loadConfig.ts +4 -0
  84. package/src/config/schema.ts +5 -1
  85. package/src/feishu/CardRenderer.ts +159 -72
  86. package/src/feishu/FeishuConnector.ts +1 -0
  87. package/src/feishu/FeishuTurnPresenter.ts +108 -16
  88. package/src/feishu/types.ts +1 -0
  89. package/src/index.ts +44 -7
  90. package/src/local-files/CsvPreview.ts +132 -0
  91. package/src/local-files/LocalFileViewerServer.ts +102 -15
  92. package/src/local-files/TurnPreviewPage.ts +131 -13
  93. package/src/presentation/TurnStateReducer.ts +87 -7
  94. package/src/presentation/turnViewTypes.ts +9 -0
  95. package/src/proxy/ProxySessionController.ts +762 -259
  96. package/src/proxy/SessionOperationQueue.ts +13 -0
  97. package/src/runtime/AcpRuntimeAdapter.ts +5 -3
  98. package/src/runtime/types.ts +10 -3
  99. package/src/state/StateStore.ts +38 -1
  100. package/src/state/TurnPreviewJournal.ts +19 -7
  101. package/src/state/migrations.ts +6 -0
  102. package/src/supervision/SafeRestartNotifier.ts +1 -0
  103. package/src/supervision/SafeRestartScheduler.ts +18 -6
  104. package/src/terminal/TerminalCards.ts +18 -0
  105. package/src/terminal/TerminalLinks.ts +61 -0
  106. package/src/terminal/TerminalPage.ts +213 -0
  107. package/src/terminal/TerminalProcess.ts +71 -0
  108. package/src/terminal/TerminalServer.ts +513 -0
@@ -1,24 +1,23 @@
1
- import { AppServerRequestError } from "./AppServerConnection.js";
2
1
  import type { RuntimeExecutionSettings } from "../runtime/types.js";
3
2
 
4
3
  export const PROVIDER_SWITCH_BUSY = "当前任务正在执行,暂时不能切换 Provider。请等待任务完成,或停止任务后重试。";
4
+ export const PROVIDER_SWITCH_EMPTY = "App Server 尚未保存此空会话,无法在保留任务 ID 的前提下切换 Provider。未改变会话或设置,也不会自动创建替代任务。";
5
5
 
6
- export function isMissingRolloutError(error: unknown): boolean {
7
- if (error instanceof AppServerRequestError && error.method !== "thread/resume") return false;
8
- const message = error instanceof AppServerRequestError ? error.serverMessage
9
- : error instanceof Error ? error.message : String(error);
10
- if (/paginated history lineage|missing source rollout/iu.test(message)) return false;
11
- return /no rollout found|rollout[^\n]*(?:not found|missing)/iu.test(message);
6
+ export class ProviderSwitchError extends Error {
7
+ constructor(message: string, readonly previousSettingsRestored: boolean, options?: ErrorOptions) {
8
+ super(message, options);
9
+ this.name = "ProviderSwitchError";
10
+ }
12
11
  }
13
12
 
14
13
  export function assertProviderSettingsApplied(
15
14
  actual: { modelProvider?: string; model?: string },
16
- requested: Pick<RuntimeExecutionSettings, "modelProvider" | "model">,
15
+ requested: Partial<Pick<RuntimeExecutionSettings, "modelProvider" | "model">>,
17
16
  ): void {
18
- if (actual.modelProvider !== requested.modelProvider) {
19
- throw new Error(`Provider 未生效:请求 ${requested.modelProvider},实际返回 ${actual.modelProvider ?? "未知"}。`);
17
+ if (requested.modelProvider !== undefined && actual.modelProvider !== requested.modelProvider) {
18
+ throw new Error(`Provider 未生效:请求 ${requested.modelProvider},实际返回 ${actual.modelProvider ?? "未知"}。App Server 未应用设置;可在任务空闲后安全重启再重试,不会自动 Fork 或更改历史。`);
20
19
  }
21
- if (actual.model !== requested.model) {
20
+ if (requested.model !== undefined && actual.model !== requested.model) {
22
21
  throw new Error(`模型未生效:请求 ${requested.model},实际返回 ${actual.model ?? "未知"}。请确认目标 Provider 支持该模型。`);
23
22
  }
24
23
  }
@@ -29,7 +28,7 @@ export function providerSwitchFailure(error: unknown): string {
29
28
  if (/model[^\n]*(?:unsupported|not supported|not found|unavailable|does not exist)|unknown model/iu.test(message)) {
30
29
  return `目标 Provider 不支持当前模型,请选择兼容的模型后重试。${message}`;
31
30
  }
32
- if (/unauthorized|forbidden|auth(?:entication|orization)|api.?key|provider[^\n]*(?:not found|unknown|not configured)/iu.test(message)) {
31
+ if (/unauthorized|forbidden|auth(?:enticat(?:e|ion)|orization)|api.?key|provider[^\n]*(?:not found|unknown|not configured)/iu.test(message)) {
33
32
  return `请检查目标 Provider 的配置和认证信息。${message}`;
34
33
  }
35
34
  return message;
@@ -29,8 +29,9 @@ export class SystemThreadWriterProcessController implements ThreadWriterProcessC
29
29
  async inspect(lockPath: string): Promise<ThreadWriterProcess[]> {
30
30
  try {
31
31
  await access(lockPath);
32
- } catch {
33
- return [];
32
+ } catch (error) {
33
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
34
+ throw error;
34
35
  }
35
36
  return process.platform === "win32"
36
37
  ? inspectWindowsLockOwners(lockPath)
@@ -134,12 +135,15 @@ async function inspectWindowsLockOwners(lockPath: string): Promise<ThreadWriterP
134
135
  async function inspectPosixLockOwners(lockPath: string): Promise<ThreadWriterProcess[]> {
135
136
  const pids = await posixLockOwnerPids(lockPath);
136
137
  const owners = await Promise.all(pids.map((pid) => inspectPosixProcess(pid)));
138
+ if (owners.some((owner) => !owner)) throw new Error("Cannot identify a reported thread writer; retry the inspection.");
137
139
  return owners.filter((owner): owner is ThreadWriterProcess => owner !== undefined);
138
140
  }
139
141
 
140
142
  async function posixLockOwnerPids(lockPath: string): Promise<number[]> {
141
143
  const lsof = await tryExecuteFile("lsof", ["-t", "--", lockPath]);
142
- const raw = lsof.ok ? lsof.stdout : (await tryExecuteFile("fuser", [lockPath])).stdout;
144
+ const result = lsof.ok || lsof.noMatches ? lsof : await tryExecuteFile("fuser", [lockPath]);
145
+ if (!result.ok && !result.noMatches) throw new Error("Cannot inspect thread writers: lsof and fuser failed.");
146
+ const raw = result.stdout;
143
147
  return [...new Set(raw.match(/\d+/gu)?.map(Number).filter((pid) => Number.isSafeInteger(pid) && pid > 0) ?? [])];
144
148
  }
145
149
 
@@ -233,11 +237,15 @@ function executeFile(command: string, args: string[], extraEnv: Record<string, s
233
237
  });
234
238
  }
235
239
 
236
- function tryExecuteFile(command: string, args: string[]): Promise<{ ok: boolean; stdout: string }> {
237
- return executeFile(command, args).then(
238
- (stdout) => ({ ok: true, stdout }),
239
- () => ({ ok: false, stdout: "" }),
240
- );
240
+ function tryExecuteFile(command: string, args: string[]): Promise<{ ok: boolean; stdout: string; noMatches: boolean }> {
241
+ return new Promise((resolve) => {
242
+ execFile(command, args, {
243
+ timeout: PROCESS_INSPECTION_TIMEOUT_MS, maxBuffer: MAX_PROCESS_OUTPUT_BYTES, windowsHide: true, encoding: "utf8",
244
+ }, (error, stdout, stderr) => resolve({
245
+ ok: !error, stdout,
246
+ noMatches: error?.code === 1 && !error.killed && !stdout.trim() && !stderr.trim(),
247
+ }));
248
+ });
241
249
  }
242
250
 
243
251
  function positiveInteger(value: unknown): number | undefined {
@@ -352,7 +360,7 @@ try {
352
360
  for ($index = 0; $index -lt $count; $index++) {
353
361
  $writerPid = $items[$index].Process.dwProcessId
354
362
  $writer = Get-AgentBotProcess $writerPid
355
- if ($null -eq $writer) { continue }
363
+ if ($null -eq $writer) { throw "Cannot identify reported thread writer PID $writerPid; retry the inspection." }
356
364
  $application = $writer
357
365
  $cursor = $writer
358
366
  for ($depth = 0; $depth -lt 8 -and $cursor.ParentProcessId -gt 1; $depth++) {
@@ -33,7 +33,9 @@ export function writeAgentExecutionDefaults(
33
33
  for (const key of DEFAULT_KEYS) {
34
34
  const value = defaults[key];
35
35
  if (value !== undefined) target.set(key, value);
36
+ else target.delete(key);
36
37
  }
38
+ if (target.items.length === 0) agent.delete("defaults");
37
39
 
38
40
  const updated = document.toString();
39
41
  if (updated === original) return false;
@@ -30,6 +30,10 @@ updates:
30
30
  # Daily stable update check, private 60-second cancellation window, then safe update.
31
31
  enabled: true
32
32
 
33
+ terminal:
34
+ enabled: true
35
+ # Shares fileViewer host, port and publicBaseUrl; keeps independent owner authorization.
36
+
33
37
  fileViewer:
34
38
  enabled: true
35
39
  # Use 0.0.0.0 to accept LAN connections and automatically prefer
@@ -57,6 +57,9 @@ export const agentConfigSchema = z.object({
57
57
  });
58
58
 
59
59
  export const appConfigSchema = z.object({
60
+ terminal: z.object({
61
+ enabled: z.boolean().default(true),
62
+ }).default({ enabled: true }),
60
63
  updates: z.object({ enabled: z.boolean().default(true) }).default({ enabled: true }),
61
64
  console: z
62
65
  .object({
@@ -108,6 +111,7 @@ export type AgentConfig = Omit<ParsedAgentConfig, "defaults"> & {
108
111
  defaults?: AgentExecutionDefaults;
109
112
  };
110
113
  export type GroupNameFormatConfig = z.infer<typeof groupNameFormatSchema>;
111
- export type AppConfig = Omit<z.infer<typeof appConfigSchema>, "updates"> & {
114
+ export type AppConfig = Omit<z.infer<typeof appConfigSchema>, "updates" | "terminal"> & {
112
115
  updates?: { enabled: boolean };
116
+ terminal?: z.infer<typeof appConfigSchema>["terminal"];
113
117
  };
@@ -1,9 +1,11 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { approvalActivityId } from "../presentation/TurnStateReducer.js";
2
3
  import path from "node:path";
3
4
  import type { JsonValue } from "../acp/acpTypes.js";
4
5
  import type { RuntimeSession } from "../acp/AcpSessionManager.js";
5
6
  import type {
6
7
  ApprovalDecision,
8
+ ApprovalRequest,
7
9
  ModelOption,
8
10
  ModelProviderOption,
9
11
  PermissionMode,
@@ -13,6 +15,7 @@ import type {
13
15
  import type { TurnActivity, TurnViewState, TurnViewStatus } from "../presentation/turnViewTypes.js";
14
16
  import { truncateMiddle, truncateText } from "../utils/markdown.js";
15
17
  import type { UpdateNotice } from "../updates/types.js";
18
+ import type { TerminalCardView } from "../terminal/TerminalServer.js";
16
19
  import { localCardImage } from "./LocalCardImage.js";
17
20
 
18
21
  export interface StartupStatusView {
@@ -57,6 +60,7 @@ export interface InitializationWelcomeView {
57
60
  }
58
61
 
59
62
  export interface SafeRestartStatusView {
63
+ activeTerminals?: number;
60
64
  scheduleId: number;
61
65
  reason: string;
62
66
  phase: "waiting_tasks" | "waiting_delivery" | "countdown" | "restarting" | "cancelled" | "superseded";
@@ -84,6 +88,7 @@ export interface ModelSelectorCardView {
84
88
  sessionId: string;
85
89
  contextKey: string;
86
90
  currentModel?: string;
91
+ modelPage?: number;
87
92
  reasoningEffort?: string;
88
93
  models: ModelOption[];
89
94
  modelProvider?: string;
@@ -130,7 +135,7 @@ export type ExecutionSettingsTab = "agent" | "provider" | "model" | "thinking" |
130
135
 
131
136
  const EXECUTION_SETTING_EFFECTS: Record<ExecutionSettingsTab, string> = {
132
137
  agent: "只影响新建任务,不切换当前任务。",
133
- provider: "只影响当前任务,需空闲时切换。",
138
+ provider: "只影响当前任务;运行中选择将在本轮结束后生效。",
134
139
  model: "只影响当前任务,下一轮生效。",
135
140
  thinking: "只影响当前任务,下一轮生效。",
136
141
  permission: "只影响当前任务,下一轮生效。",
@@ -142,6 +147,8 @@ export interface ExecutionSettingsAgentOption {
142
147
  }
143
148
 
144
149
  export interface ExecutionSettingsCardView {
150
+ pendingNotice?: string;
151
+ pendingFailed?: boolean;
145
152
  sessionId?: string;
146
153
  contextKey: string;
147
154
  activeTab: ExecutionSettingsTab;
@@ -151,6 +158,7 @@ export interface ExecutionSettingsCardView {
151
158
  runtimeSettingsAvailable: boolean;
152
159
  currentProvider?: string;
153
160
  currentModel?: string;
161
+ modelPage?: number;
154
162
  currentEffort?: string;
155
163
  currentPermissionMode: PermissionMode;
156
164
  providers: ModelProviderOption[];
@@ -197,6 +205,7 @@ export interface SessionTaskCardEntry {
197
205
  reference: string;
198
206
  summary: string;
199
207
  detailLines: string[];
208
+ stopAction?: TaskListCardAction;
200
209
  actions?: TaskListCardAction[];
201
210
  current?: boolean;
202
211
  }
@@ -261,6 +270,7 @@ export interface DismissGroupCardView {
261
270
  }
262
271
 
263
272
  export interface ShellCommandCardView {
273
+ terminalUrl?: string;
264
274
  jobId?: string;
265
275
  contextKey?: string;
266
276
  command: string;
@@ -506,6 +516,9 @@ export class CardRenderer {
506
516
  ...(outputWasTruncated ? ["输出过长,已保留开头和结尾并截断中间内容。"] : []),
507
517
  ].join("\n")),
508
518
  ];
519
+ if (view.terminalUrl) {
520
+ elements.push(markdown(`[Open Terminal](${view.terminalUrl}) · 独立终端,需在本会话由所有者授权`));
521
+ }
509
522
  if (view.status === "running" && view.jobId && view.contextKey) {
510
523
  elements.push({
511
524
  tag: "button",
@@ -551,6 +564,7 @@ export class CardRenderer {
551
564
  `**思考强度 / 权限**:${inlineCode(view.currentEffort ?? "自动")} / ${inlineCode(permissionModeLabel(view.currentPermissionMode))}`,
552
565
  ]
553
566
  : []),
567
+ ...(view.pendingNotice ? [view.pendingNotice] : []),
554
568
  ...(view.notice ? [view.notice] : []),
555
569
  ...(view.warning ? [`**提示**:${escapeCardHtml(view.warning)}`] : []),
556
570
  ].join("\n")),
@@ -582,7 +596,8 @@ export class CardRenderer {
582
596
  inlineCode(provider.id),
583
597
  provider.isDefault ? " · 默认" : "",
584
598
  ].join(""),
585
- current: provider.id === view.currentProvider,
599
+ current: provider.id === view.currentProvider && !view.pendingFailed,
600
+ currentLabel: view.pendingNotice ? "待生效" : undefined,
586
601
  action: {
587
602
  text: "Switch",
588
603
  value: {
@@ -597,18 +612,24 @@ export class CardRenderer {
597
612
  if (view.models.length === 0) {
598
613
  elements.push(markdown("当前运行时未返回可用模型。"));
599
614
  } else {
600
- elements.push(...view.models.map((model) => settingsOptionRow({
615
+ const page = paginateModels(view.models, view.currentModel, view.modelPage, {
616
+ action: "settings_tab_open", ...baseAction, tab: "model", provider: view.currentProvider ?? "",
617
+ });
618
+ elements.push(...page.models.map((model) => settingsOptionRow({
601
619
  label: `${inlineCode(model.id)}${model.isDefault ? " · 默认" : ""}`,
602
- current: model.id === view.currentModel,
620
+ current: model.id === view.currentModel && !view.pendingFailed,
621
+ currentLabel: view.pendingNotice ? "待生效" : undefined,
603
622
  action: {
604
623
  text: "Switch",
605
624
  value: {
606
625
  action: "settings_model_select",
607
626
  ...baseAction,
627
+ provider: view.currentProvider ?? "",
608
628
  model: model.id,
609
629
  },
610
630
  },
611
631
  })));
632
+ elements.push(...page.navigation);
612
633
  }
613
634
  } else if (view.activeTab === "thinking") {
614
635
  if (!view.currentModel) {
@@ -620,11 +641,13 @@ export class CardRenderer {
620
641
  elements.push(...view.reasoningOptions.map((option) => settingsOptionRow({
621
642
  label: inlineCode(option.value),
622
643
  current: option.value === view.currentEffort,
644
+ currentLabel: view.pendingNotice ? "待生效" : undefined,
623
645
  action: {
624
646
  text: "Switch",
625
647
  value: {
626
648
  action: "settings_thinking_select",
627
649
  ...baseAction,
650
+ provider: view.currentProvider ?? "",
628
651
  model: currentModel,
629
652
  effort: option.value,
630
653
  },
@@ -635,6 +658,7 @@ export class CardRenderer {
635
658
  elements.push(...(["auto", "confirm"] as PermissionMode[]).map((mode) => settingsOptionRow({
636
659
  label: `${inlineCode(mode)} · ${permissionModeLabel(mode)}`,
637
660
  current: mode === view.currentPermissionMode,
661
+ currentLabel: view.pendingNotice ? "待生效" : undefined,
638
662
  action: {
639
663
  text: "Switch",
640
664
  value: {
@@ -647,7 +671,7 @@ export class CardRenderer {
647
671
  }
648
672
 
649
673
  elements.push(markdown(`> ${EXECUTION_SETTING_EFFECTS[view.activeTab]}`));
650
- return sectionCard("运行设置", elements, view.warning ? "orange" : view.notice ? "green" : "blue");
674
+ return sectionCard("运行设置", elements, view.warning || view.pendingFailed ? "orange" : view.notice ? "green" : "blue");
651
675
  }
652
676
 
653
677
  renderProviderSelector(view: ProviderSelectorCardView): Record<string, unknown> {
@@ -783,7 +807,14 @@ export class CardRenderer {
783
807
  if (view.models.length === 0) {
784
808
  elements.push(markdown("当前运行时未返回可用模型。"));
785
809
  } else {
786
- elements.push(...view.models.map((model) => {
810
+ const page = paginateModels(view.models, view.currentModel, view.modelPage, {
811
+ action: view.unifiedSettings ? "provider_model_open" : "model_open",
812
+ sessionId: view.sessionId,
813
+ contextKey: view.contextKey,
814
+ ...(view.modelProvider ? { provider: view.modelProvider } : {}),
815
+ ...(view.permissionMode ? { permissionMode: view.permissionMode } : {}),
816
+ });
817
+ elements.push(...page.models.map((model) => {
787
818
  const isCurrent = model.id === view.currentModel;
788
819
  return {
789
820
  tag: "column_set",
@@ -821,6 +852,7 @@ export class CardRenderer {
821
852
  ],
822
853
  };
823
854
  }));
855
+ elements.push(...page.navigation);
824
856
  }
825
857
  if (view.unifiedSettings) {
826
858
  elements.push(
@@ -992,6 +1024,24 @@ export class CardRenderer {
992
1024
  view.status === "completed" ? "green" : view.status === "cancelled" ? "grey" : "blue");
993
1025
  }
994
1026
 
1027
+ renderTerminalAuthorization(view: TerminalCardView): Record<string, unknown> {
1028
+ const action = (decision: string) => ({ action: "terminal_authorize", contextKey: view.contextKey, requestId: view.requestId, token: view.approvalToken, decision });
1029
+ const elements: Record<string, unknown>[] = [markdown(`**目录**:${inlineCode(view.cwd)}\n**终端**:${inlineCode(view.reopenToken)}`)];
1030
+ if (view.state === "pending") {
1031
+ elements.push(markdown(`**验证码**:${inlineCode(view.code)}\n仅 AgentBot 所有者可授权。授权后,该浏览器可在本机执行任意命令,拥有 AgentBot 的系统用户权限。请核对网页验证码,仅批准本人发起的请求。5 分钟内有效。${view.recovery ? (view.shellStarted ? "恢复原 Shell,不创建新终端;授权后旧网页失去访问权限。拒绝不影响原终端。" : "继续打开同一个终端;授权后旧网页失去访问权限,Shell 在连接成功后启动。拒绝不影响原授权。") : "不执行原卡片命令。"}`),
1032
+ taskActionRow([{ text: "Allow", value: action("allow") }, { text: "Deny", type: "danger", value: action("deny") }, { text: "刷新", value: action("refresh") }]));
1033
+ } else if (view.state === "closed") {
1034
+ elements.push(markdown(`**状态**:${view.message ? escapeCardHtml(view.message) : "已关闭"}\n原终端已结束时,请从命令卡片创建新终端。`));
1035
+ } else {
1036
+ const waiting = view.state === "approved";
1037
+ const label = waiting ? "打开终端" : "重新打开终端";
1038
+ elements.push(markdown(`**状态**:${waiting ? "已授权,等待网页连接" : view.connected ? "已连接" : "网页已断开,Shell 仍在运行"}\n${waiting ? "请返回原网页连接;原网页已关闭时,点击下方“打开终端”。Shell 尚未启动。" : "点击下方“重新打开终端”恢复原 Shell。前台连接时不因空闲关闭;后台空闲 30 分钟或网页断开 3 分钟后会自动关闭 Shell,成功重连取消断开计时并重置空闲计时;“关闭终端”会中断其中的命令。"}`));
1039
+ elements.push(markdown(`[${label}](${view.reopenUrl}) · 新网页需核对验证码重新授权${waiting ? ",连接成功后启动 Shell" : ",恢复原 Shell"}`));
1040
+ elements.push(taskActionRow([{ text: "关闭终端", type: "danger", value: action("close") }, { text: "刷新", value: action("refresh") }]));
1041
+ }
1042
+ return sectionCard(view.state === "pending" ? "网页终端授权" : "网页终端管理", elements, view.state === "closed" ? "grey" : view.state === "pending" ? "orange" : "blue");
1043
+ }
1044
+
995
1045
  renderSafeRestartStatus(view: SafeRestartStatusView): Record<string, unknown> {
996
1046
  const actionable = view.phase !== "restarting" && view.phase !== "cancelled" && view.phase !== "superseded";
997
1047
  const countdown = view.phase === "countdown"
@@ -1019,6 +1069,7 @@ export class CardRenderer {
1019
1069
  `**重启原因**:${inlineCode(view.reason)}`,
1020
1070
  `**重启倒计时**:${countdown}`,
1021
1071
  `**待投递结果**:${view.pendingFinalDeliveries} 条`,
1072
+ ...(view.activeTerminals ? [`**活动终端**:${view.activeTerminals} 个,请在网页或原会话的终端管理卡中关闭。ForceRestart 会中断终端。`] : []),
1022
1073
  ];
1023
1074
  const elements: Record<string, unknown>[] = [markdown(lines.join("\n"))];
1024
1075
  if (view.waitingTasks.length > 0) {
@@ -1698,38 +1749,7 @@ function renderTurnElements(
1698
1749
  elements.push(...renderActivities(visibleActivities, state.projectCwd));
1699
1750
  if (state.fileSummary.length > 0) elements.push(fileSummaryPanel(state));
1700
1751
 
1701
- if (state.approval) {
1702
- const request = state.approval;
1703
- elements.push(markdown([
1704
- `**${request.title}**`,
1705
- request.command ? codeBlock(request.command, 800) : undefined,
1706
- request.reason,
1707
- ].filter(Boolean).join("\n")));
1708
- elements.push({
1709
- tag: "column_set",
1710
- flex_mode: "flow",
1711
- horizontal_spacing: "8px",
1712
- columns: request.options.map((option) => ({
1713
- tag: "column",
1714
- width: "auto",
1715
- elements: [{
1716
- tag: "button",
1717
- text: { tag: "plain_text", content: request.kind === "mode_change" ? option.label : approvalDecisionLabel(option.id) },
1718
- type: option.id === "accept" || option.id === "acceptForSession" ? "primary" : option.id === "cancel" ? "danger" : "default",
1719
- behaviors: [{
1720
- type: "callback",
1721
- value: {
1722
- action: "approval",
1723
- sessionId: state.sessionId,
1724
- turnId: state.turnId,
1725
- requestId: request.id,
1726
- decision: option.id,
1727
- },
1728
- }],
1729
- }],
1730
- })),
1731
- });
1732
- }
1752
+ if (state.approval) elements.push(...renderApprovalElements(state, state.approval));
1733
1753
  if (state.error) elements.push(markdown(codeBlock(state.error, 2_000)));
1734
1754
  const showAssistantText = state.assistantText && assistantTextMode === "always";
1735
1755
  if (showAssistantText) {
@@ -1769,38 +1789,7 @@ function renderGroupedTurnElements(
1769
1789
  elements.push(...renderGroupedActivityGroups(visibleGroups, state.projectCwd));
1770
1790
  if (state.fileSummary.length > 0) elements.push(fileSummaryPanel(state));
1771
1791
 
1772
- if (state.approval) {
1773
- const request = state.approval;
1774
- elements.push(markdown([
1775
- `**${request.title}**`,
1776
- request.command ? codeBlock(request.command, 800) : undefined,
1777
- request.reason,
1778
- ].filter(Boolean).join("\n")));
1779
- elements.push({
1780
- tag: "column_set",
1781
- flex_mode: "flow",
1782
- horizontal_spacing: "8px",
1783
- columns: request.options.map((option) => ({
1784
- tag: "column",
1785
- width: "auto",
1786
- elements: [{
1787
- tag: "button",
1788
- text: { tag: "plain_text", content: request.kind === "mode_change" ? option.label : approvalDecisionLabel(option.id) },
1789
- type: option.id === "accept" || option.id === "acceptForSession" ? "primary" : option.id === "cancel" ? "danger" : "default",
1790
- behaviors: [{
1791
- type: "callback",
1792
- value: {
1793
- action: "approval",
1794
- sessionId: state.sessionId,
1795
- turnId: state.turnId,
1796
- requestId: request.id,
1797
- decision: option.id,
1798
- },
1799
- }],
1800
- }],
1801
- })),
1802
- });
1803
- }
1792
+ if (state.approval) elements.push(...renderApprovalElements(state, state.approval));
1804
1793
  if (state.error) elements.push(markdown(codeBlock(state.error, 2_000)));
1805
1794
  const showAssistantText = state.assistantText && assistantTextMode === "always";
1806
1795
  if (showAssistantText) {
@@ -1812,6 +1801,83 @@ function renderGroupedTurnElements(
1812
1801
  return elements;
1813
1802
  }
1814
1803
 
1804
+ function renderApprovalElements(state: TurnViewState, request: ApprovalRequest): Record<string, unknown>[] {
1805
+ const elements: Record<string, unknown>[] = [];
1806
+ const command = request.command?.trim();
1807
+ if (command && request.kind !== "mode_change") {
1808
+ elements.push(markdown("**命令执行确认**"));
1809
+ const reason = request.reason?.trim()
1810
+ || (request.title.trim() !== command ? request.title.trim() : "");
1811
+ if (reason && reason !== command) {
1812
+ const preview = approvalTextPreview(reason, 1_000, escapeApprovalText);
1813
+ elements.push(markdown(escapeApprovalText(preview)));
1814
+ if (preview !== reason) elements.push(markdown("<font color='grey'>说明较长,请在 Preview 中查看完整内容。</font>"));
1815
+ }
1816
+ const display = displayToolCommand(command) ?? command;
1817
+ const summary = truncateText(display.replace(/\s+/gu, " "), 64);
1818
+ const preview = approvalTextPreview(command, 2_200);
1819
+ elements.push(collapsiblePanel(`查看命令 · ${summary}`, [
1820
+ markdown(codeBlock(preview, preview.length)),
1821
+ ...(preview !== command ? [markdown("<font color='grey'>命令过长,卡片仅显示开头。请在 Preview 中查看完整命令后再授权。</font>")] : []),
1822
+ ], {
1823
+ elementId: `approval_${createHash("sha256").update(request.id).digest("hex").slice(0, 11)}`,
1824
+ compact: true,
1825
+ borderColor: "grey",
1826
+ }));
1827
+ } else {
1828
+ elements.push(markdown([
1829
+ `**${request.title}**`,
1830
+ request.command ? codeBlock(request.command, 800) : undefined,
1831
+ request.reason,
1832
+ ].filter(Boolean).join("\n")));
1833
+ }
1834
+ if (request.automatic) {
1835
+ elements.push(markdown("自动执行模式:正在自动同意。"));
1836
+ return elements;
1837
+ }
1838
+ elements.push({
1839
+ tag: "column_set",
1840
+ flex_mode: "flow",
1841
+ horizontal_spacing: "8px",
1842
+ columns: request.options.map((option) => ({
1843
+ tag: "column",
1844
+ width: "auto",
1845
+ elements: [{
1846
+ tag: "button",
1847
+ text: { tag: "plain_text", content: request.kind === "mode_change" ? option.label : approvalDecisionLabel(option.id) },
1848
+ type: option.id === "accept" || option.id === "acceptForSession" ? "primary" : option.id === "cancel" ? "danger" : "default",
1849
+ behaviors: [{
1850
+ type: "callback",
1851
+ value: {
1852
+ action: "approval",
1853
+ sessionId: state.sessionId,
1854
+ turnId: state.turnId,
1855
+ requestId: request.id,
1856
+ decision: option.id,
1857
+ },
1858
+ }],
1859
+ }],
1860
+ })),
1861
+ });
1862
+ return elements;
1863
+ }
1864
+
1865
+ function approvalTextPreview(value: string, maxBytes: number, encode = (text: string): string => text): string {
1866
+ if (Buffer.byteLength(JSON.stringify(encode(value)), "utf8") <= maxBytes) return value;
1867
+ let bytes = 5;
1868
+ let result = "";
1869
+ for (const character of value) {
1870
+ bytes += Buffer.byteLength(JSON.stringify(encode(character)), "utf8") - 2;
1871
+ if (bytes > maxBytes) break;
1872
+ result += character;
1873
+ }
1874
+ return `${result}...`;
1875
+ }
1876
+
1877
+ function escapeApprovalText(value: string): string {
1878
+ return escapeCardHtml(value).replace(/[\\`*_[\]#]/gu, (character) => `&#${character.codePointAt(0)};`);
1879
+ }
1880
+
1815
1881
  function emptyTurnText(status: TurnViewStatus, agentLabel?: string): string {
1816
1882
  const label = agentLabel?.trim().replace(/[\r\n]+/g, " ") || "Agent";
1817
1883
  if (status === "starting") return `正在连接 ${label}…`;
@@ -2233,7 +2299,8 @@ function removeMarkdownBold(value: string): string {
2233
2299
  }
2234
2300
 
2235
2301
  function turnActivities(state: TurnViewState): TurnActivity[] {
2236
- if (state.activities?.length) return state.activities;
2302
+ if (state.activities?.length) return state.approval
2303
+ ? state.activities.filter((a) => a.id !== approvalActivityId(state.turnId, state.approval!.id)) : state.activities;
2237
2304
 
2238
2305
  const activities: TurnActivity[] = [];
2239
2306
  if (state.progressText) {
@@ -2298,6 +2365,7 @@ function collapsiblePanel(
2298
2365
 
2299
2366
  function sessionTaskPanel(entry: SessionTaskCardEntry): Record<string, unknown> {
2300
2367
  const elements: Record<string, unknown>[] = [markdown(entry.detailLines.join("\n"))];
2368
+ if (entry.stopAction) elements.push(sessionActionButton(entry.stopAction));
2301
2369
  if (entry.actions?.length) elements.push(sessionActionOverflow(entry.actions));
2302
2370
  return collapsiblePanel(entry.summary, elements, {
2303
2371
  elementId: sessionTaskPanelElementId(entry.reference),
@@ -2831,6 +2899,24 @@ function acpPermissionOptionLabel(option: { name: string; kind: string }): strin
2831
2899
  return labels[option.kind] ?? (/^[\x20-\x7E]+$/.test(option.name) ? option.name : "Select");
2832
2900
  }
2833
2901
 
2902
+ const MODEL_PAGE_SIZE = 20;
2903
+
2904
+ function paginateModels(
2905
+ models: ModelOption[],
2906
+ currentModel: string | undefined,
2907
+ requestedPage: number | undefined,
2908
+ action: Record<string, string>,
2909
+ ): { models: ModelOption[]; navigation: Record<string, unknown>[] } {
2910
+ const totalPages = Math.max(1, Math.ceil(models.length / MODEL_PAGE_SIZE));
2911
+ const currentPage = Math.floor(models.findIndex((model) => model.id === currentModel) / MODEL_PAGE_SIZE);
2912
+ const page = Math.max(0, Math.min(requestedPage ?? currentPage, totalPages - 1));
2913
+ const navigation = totalPages > 1 ? [taskActionRow([
2914
+ ...(page > 0 ? [{ text: "上一页", value: { ...action, page: String(page - 1) } }] : []),
2915
+ ...(page < totalPages - 1 ? [{ text: "下一页", value: { ...action, page: String(page + 1) } }] : []),
2916
+ ], `第 ${page + 1}/${totalPages} 页 · 共 ${models.length} 个模型`)] : [];
2917
+ return { models: models.slice(page * MODEL_PAGE_SIZE, (page + 1) * MODEL_PAGE_SIZE), navigation };
2918
+ }
2919
+
2834
2920
  function settingsTabRow(
2835
2921
  activeTab: ExecutionSettingsTab,
2836
2922
  baseAction: Record<string, string>,
@@ -2904,6 +2990,7 @@ function settingsTabElement(
2904
2990
  function settingsOptionRow(input: {
2905
2991
  label: string;
2906
2992
  current: boolean;
2993
+ currentLabel?: string;
2907
2994
  action: TaskListCardAction;
2908
2995
  }): Record<string, unknown> {
2909
2996
  return {
@@ -2923,7 +3010,7 @@ function settingsOptionRow(input: {
2923
3010
  tag: "column",
2924
3011
  width: "auto",
2925
3012
  vertical_align: "center",
2926
- elements: input.current ? [markdown("✅ 当前")] : [taskActionElement(input.action)],
3013
+ elements: input.current ? [markdown(`✅ ${input.currentLabel ?? "当前"}`)] : [taskActionElement(input.action)],
2927
3014
  },
2928
3015
  ],
2929
3016
  };
@@ -262,6 +262,7 @@ function toIncomingMessage(
262
262
  const messageId = message.message_id ?? `${Date.now()}`;
263
263
  return {
264
264
  messageId,
265
+ ...(typeof messageType === "string" ? { messageType } : {}),
265
266
  ...(Number.isFinite(Number(message.create_time)) && Number(message.create_time) > 0
266
267
  ? { createdAt: Number(message.create_time) } : {}),
267
268
  contextKey: chatId