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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +82 -55
  3. package/README.zh.md +82 -55
  4. package/config.example.yaml +5 -1
  5. package/dist/cli/AutostartManager.js +431 -0
  6. package/dist/cli/AutostartManager.js.map +1 -0
  7. package/dist/cli/FeishuAppConfiguration.js +17 -13
  8. package/dist/cli/FeishuAppConfiguration.js.map +1 -1
  9. package/dist/cli/Initializer.js +35 -0
  10. package/dist/cli/Initializer.js.map +1 -1
  11. package/dist/cli/SelfUpdateRunner.js +552 -0
  12. package/dist/cli/SelfUpdateRunner.js.map +1 -0
  13. package/dist/cli/SelfUpdater.js +431 -0
  14. package/dist/cli/SelfUpdater.js.map +1 -0
  15. package/dist/cli/autostartOutput.js +63 -0
  16. package/dist/cli/autostartOutput.js.map +1 -0
  17. package/dist/cli/controlProtocol.js.map +1 -1
  18. package/dist/cli/help.js +92 -18
  19. package/dist/cli/help.js.map +1 -1
  20. package/dist/cli/taskGroupOptions.js +14 -11
  21. package/dist/cli/taskGroupOptions.js.map +1 -1
  22. package/dist/cli/taskListOutput.js +3 -0
  23. package/dist/cli/taskListOutput.js.map +1 -1
  24. package/dist/cli/taskTarget.js +99 -0
  25. package/dist/cli/taskTarget.js.map +1 -0
  26. package/dist/cli.js +643 -58
  27. package/dist/cli.js.map +1 -1
  28. package/dist/codex/CodexRuntime.js +16 -5
  29. package/dist/codex/CodexRuntime.js.map +1 -1
  30. package/dist/commands/CommandRouter.js +20 -1
  31. package/dist/commands/CommandRouter.js.map +1 -1
  32. package/dist/config/loadConfig.js +2 -0
  33. package/dist/config/loadConfig.js.map +1 -1
  34. package/dist/config/schema.js +2 -0
  35. package/dist/config/schema.js.map +1 -1
  36. package/dist/console/ConsoleTurnPresenter.js +2 -0
  37. package/dist/console/ConsoleTurnPresenter.js.map +1 -1
  38. package/dist/feishu/CardRenderer.js +584 -16
  39. package/dist/feishu/CardRenderer.js.map +1 -1
  40. package/dist/feishu/FeishuConnector.js +29 -8
  41. package/dist/feishu/FeishuConnector.js.map +1 -1
  42. package/dist/feishu/FeishuMarkdown.js +25 -1
  43. package/dist/feishu/FeishuMarkdown.js.map +1 -1
  44. package/dist/feishu/FeishuMessageClient.js +43 -0
  45. package/dist/feishu/FeishuMessageClient.js.map +1 -1
  46. package/dist/feishu/FeishuTurnPresenter.js +29 -1
  47. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  48. package/dist/feishu/ownerAccess.js +7 -0
  49. package/dist/feishu/ownerAccess.js.map +1 -0
  50. package/dist/index.js +148 -19
  51. package/dist/index.js.map +1 -1
  52. package/dist/presentation/OutboundRouter.js +19 -1
  53. package/dist/presentation/OutboundRouter.js.map +1 -1
  54. package/dist/proxy/ProxySessionController.js +1444 -66
  55. package/dist/proxy/ProxySessionController.js.map +1 -1
  56. package/dist/startup/StartupNotifier.js +65 -16
  57. package/dist/startup/StartupNotifier.js.map +1 -1
  58. package/dist/startup/startFeishu.js +2 -2
  59. package/dist/startup/startFeishu.js.map +1 -1
  60. package/dist/state/StateStore.js +268 -67
  61. package/dist/state/StateStore.js.map +1 -1
  62. package/dist/state/migrations.js +26 -0
  63. package/dist/state/migrations.js.map +1 -1
  64. package/dist/supervision/SafeRestartNotifier.js +17 -16
  65. package/dist/supervision/SafeRestartNotifier.js.map +1 -1
  66. package/dist/supervision/SafeRestartScheduler.js +12 -7
  67. package/dist/supervision/SafeRestartScheduler.js.map +1 -1
  68. package/dist/supervision/replacementSupervisor.js +45 -4
  69. package/dist/supervision/replacementSupervisor.js.map +1 -1
  70. package/dist/supervisor.js +30 -18
  71. package/dist/supervisor.js.map +1 -1
  72. package/docs/technical-reference.md +33 -10
  73. package/docs/technical-reference.zh.md +33 -10
  74. package/npm-shrinkwrap.json +2 -2
  75. package/package.json +2 -2
  76. package/skills/agent-bot/SKILL.md +107 -182
  77. package/src/cli/AutostartManager.ts +579 -0
  78. package/src/cli/FeishuAppConfiguration.ts +22 -7
  79. package/src/cli/Initializer.ts +57 -0
  80. package/src/cli/SelfUpdateRunner.ts +817 -0
  81. package/src/cli/SelfUpdater.ts +688 -0
  82. package/src/cli/autostartOutput.ts +75 -0
  83. package/src/cli/controlProtocol.ts +111 -1
  84. package/src/cli/help.ts +92 -18
  85. package/src/cli/taskGroupOptions.ts +27 -17
  86. package/src/cli/taskListOutput.ts +10 -0
  87. package/src/cli/taskTarget.ts +168 -0
  88. package/src/cli.ts +781 -65
  89. package/src/codex/CodexRuntime.ts +17 -5
  90. package/src/commands/CommandRouter.ts +19 -1
  91. package/src/commands/commandTypes.ts +3 -0
  92. package/src/config/loadConfig.ts +2 -0
  93. package/src/config/schema.ts +2 -0
  94. package/src/console/ConsoleTurnPresenter.ts +4 -1
  95. package/src/feishu/CardRenderer.ts +716 -16
  96. package/src/feishu/FeishuConnector.ts +44 -10
  97. package/src/feishu/FeishuMarkdown.ts +27 -1
  98. package/src/feishu/FeishuMessageClient.ts +58 -0
  99. package/src/feishu/FeishuTurnPresenter.ts +35 -2
  100. package/src/feishu/ownerAccess.ts +7 -0
  101. package/src/feishu/types.ts +7 -0
  102. package/src/index.ts +192 -22
  103. package/src/presentation/OutboundRouter.ts +28 -3
  104. package/src/proxy/ProxySessionController.ts +1707 -70
  105. package/src/runtime/types.ts +1 -0
  106. package/src/startup/StartupNotifier.ts +67 -17
  107. package/src/startup/startFeishu.ts +5 -3
  108. package/src/state/StateStore.ts +413 -65
  109. package/src/state/migrations.ts +26 -0
  110. package/src/supervision/SafeRestartNotifier.ts +17 -17
  111. package/src/supervision/SafeRestartScheduler.ts +13 -6
  112. package/src/supervision/replacementSupervisor.ts +50 -4
  113. package/src/supervisor.ts +35 -16
@@ -11,7 +11,7 @@ import type {
11
11
  ToolState,
12
12
  } from "../runtime/types.js";
13
13
  import type { TurnActivity, TurnViewState, TurnViewStatus } from "../presentation/turnViewTypes.js";
14
- import { truncateMiddle, truncateText } from "../utils/markdown.js";
14
+ import { truncateText } from "../utils/markdown.js";
15
15
  import { localCardImage } from "./LocalCardImage.js";
16
16
 
17
17
  export interface StartupStatusView {
@@ -177,6 +177,43 @@ export interface TaskListCardEntry {
177
177
  current?: boolean;
178
178
  }
179
179
 
180
+ export interface SessionTaskCardEntry {
181
+ reference: string;
182
+ summary: string;
183
+ detailLines: string[];
184
+ actions?: TaskListCardAction[];
185
+ current?: boolean;
186
+ }
187
+
188
+ export interface SessionTaskCardGroup {
189
+ title: string;
190
+ entries: SessionTaskCardEntry[];
191
+ actions?: TaskListCardAction[];
192
+ }
193
+
194
+ export interface DirectoryBrowserCardEntry {
195
+ name: string;
196
+ kind: "directory" | "drive" | "file" | "image" | "binary";
197
+ openAction?: TaskListCardAction;
198
+ actions?: TaskListCardAction[];
199
+ }
200
+
201
+ export interface DirectoryBrowserCardView {
202
+ directory: string;
203
+ entries: DirectoryBrowserCardEntry[];
204
+ currentActions: TaskListCardAction[];
205
+ navigationActions: TaskListCardAction[];
206
+ footerLines: string[];
207
+ }
208
+
209
+ const DIRECTORY_BROWSER_ROW_COUNT = 16;
210
+
211
+ export type ThinkingCardLayout = "grouped" | "timeline";
212
+
213
+ export interface CardRendererOptions {
214
+ thinkingCardLayout?: ThinkingCardLayout;
215
+ }
216
+
180
217
  export interface ResetHistoryCardEntry extends TaskListCardEntry {
181
218
  sequence: number;
182
219
  graphNodeLine: string;
@@ -190,6 +227,12 @@ export interface ResetHistoryCardView {
190
227
  }
191
228
 
192
229
  export class CardRenderer {
230
+ private readonly thinkingCardLayout: ThinkingCardLayout;
231
+
232
+ constructor(options: CardRendererOptions = {}) {
233
+ this.thinkingCardLayout = options.thinkingCardLayout ?? "grouped";
234
+ }
235
+
193
236
  renderExecutionSettings(view: ExecutionSettingsCardView): Record<string, unknown> {
194
237
  const baseAction: Record<string, string> = {
195
238
  contextKey: view.contextKey,
@@ -807,7 +850,9 @@ export class CardRenderer {
807
850
  }
808
851
 
809
852
  renderTurn(state: TurnViewState): Record<string, unknown> {
810
- const elements = renderTurnElements(state, "hidden");
853
+ const elements = this.thinkingCardLayout === "timeline"
854
+ ? renderTurnElements(state, "hidden")
855
+ : renderGroupedTurnElements(state, "hidden");
811
856
  if (isTurnStoppable(state.status)) {
812
857
  elements.push({ tag: "hr" }, taskActionRow([{
813
858
  text: "Stop",
@@ -843,10 +888,27 @@ export class CardRenderer {
843
888
  }
844
889
 
845
890
  renderActivityHistory(state: TurnViewState, requestedPage: number): Record<string, unknown> {
846
- const pages = activityPages(turnActivities(state));
891
+ const grouped = this.thinkingCardLayout === "grouped";
892
+ const activities = turnActivities(state);
893
+ const allGroups = grouped ? groupTurnActivities(activities).flatMap(splitGroupedExecutionActivity) : [];
894
+ const historyGroups = grouped
895
+ ? groupedLiveActivityPage(groupedActivityPagesFromGroups(allGroups, state.projectCwd), state.projectCwd).historyGroups
896
+ : [];
897
+ const usesHistoricalPrefixPages = historyGroups.length > 0;
898
+ const pages = grouped
899
+ ? groupedActivityPagesFromGroups(
900
+ usesHistoricalPrefixPages ? historyGroups : allGroups,
901
+ state.projectCwd,
902
+ {
903
+ fullActivityText: true,
904
+ includeReasoningHistory: true,
905
+ },
906
+ )
907
+ : activityPages(activities);
847
908
  if (pages.length === 0) {
848
909
  return this.renderSectionsCard("思考活动历史", [{ lines: ["没有保存到活动记录。"] }]);
849
910
  }
911
+ const totalPages = usesHistoricalPrefixPages ? pages.length + 1 : pages.length;
850
912
  const page = Math.max(0, Math.min(Math.trunc(requestedPage), pages.length - 1));
851
913
  const actions: TaskListCardAction[] = [
852
914
  ...(pages.length > 1 ? [{
@@ -857,15 +919,20 @@ export class CardRenderer {
857
919
  text: "上一页",
858
920
  value: { action: "activity_history", turnId: state.turnId, page: String(page - 1) },
859
921
  }] : []),
860
- ...(page < pages.length - 2 ? [{
922
+ ...(page < pages.length - (usesHistoricalPrefixPages ? 1 : 2) ? [{
861
923
  text: "下一页",
862
924
  value: { action: "activity_history", turnId: state.turnId, page: String(page + 1) },
863
925
  }] : []),
864
926
  ];
865
927
  const elements: Record<string, unknown>[] = [];
866
928
  if (actions.length > 0) elements.push(taskActionRow(actions), { tag: "hr" });
867
- elements.push(...renderActivities(pages[page] ?? [], state.projectCwd, true));
868
- return sectionCard(`思考活动历史 · ${page + 1}/${pages.length}`, elements.length > 0 ? elements : [markdown("无")]);
929
+ elements.push(...(grouped
930
+ ? renderGroupedActivityGroups(pages[page] as GroupedTurnActivity[] | undefined ?? [], state.projectCwd, {
931
+ fullActivityText: true,
932
+ includeReasoningHistory: true,
933
+ })
934
+ : renderActivities(pages[page] as TurnActivity[] | undefined ?? [], state.projectCwd, true)));
935
+ return sectionCard(`思考活动历史 · ${page + 1}/${totalPages}`, elements.length > 0 ? elements : [markdown("无")]);
869
936
  }
870
937
 
871
938
  renderSessionStarted(session: RuntimeSession): Record<string, unknown> {
@@ -990,6 +1057,68 @@ export class CardRenderer {
990
1057
  };
991
1058
  }
992
1059
 
1060
+ renderSessionTaskListCard(
1061
+ title: string,
1062
+ sectionTitle: string,
1063
+ groups: SessionTaskCardGroup[],
1064
+ footerLines: string[],
1065
+ footerActions: TaskListCardAction[] = [],
1066
+ ): Record<string, unknown> {
1067
+ const elements: Record<string, unknown>[] = [markdown(`**${sectionTitle}**`)];
1068
+ if (groups.length === 0) {
1069
+ elements.push(markdown("无"));
1070
+ } else {
1071
+ groups.forEach((group) => {
1072
+ elements.push(sessionProjectRow(group));
1073
+ elements.push(...group.entries.map(sessionTaskPanel));
1074
+ });
1075
+ }
1076
+ if (footerActions.length > 0) {
1077
+ elements.push(taskActionRow(footerActions));
1078
+ }
1079
+ if (footerLines.length > 0) {
1080
+ elements.push(markdown(footerLines.join("\n")));
1081
+ }
1082
+ return {
1083
+ schema: "2.0",
1084
+ config: {
1085
+ update_multi: true,
1086
+ width_mode: "fill",
1087
+ },
1088
+ header: {
1089
+ template: "blue",
1090
+ title: {
1091
+ tag: "plain_text",
1092
+ content: title,
1093
+ },
1094
+ },
1095
+ body: {
1096
+ vertical_spacing: "4px",
1097
+ elements,
1098
+ },
1099
+ };
1100
+ }
1101
+
1102
+ renderDirectoryBrowserCard(view: DirectoryBrowserCardView): Record<string, unknown> {
1103
+ const elements: Record<string, unknown>[] = [
1104
+ markdown(`**当前目录**:${inlineCode(view.directory)}`),
1105
+ taskActionRow(view.currentActions),
1106
+ { tag: "hr" },
1107
+ ];
1108
+ const entryRows = view.entries.length === 0
1109
+ ? [directoryBrowserEmptyRow()]
1110
+ : view.entries.slice(0, DIRECTORY_BROWSER_ROW_COUNT).map(directoryBrowserEntryRow);
1111
+ while (entryRows.length < DIRECTORY_BROWSER_ROW_COUNT) entryRows.push(directoryBrowserPlaceholderRow());
1112
+ elements.push(...entryRows);
1113
+ if (view.navigationActions.length > 0) {
1114
+ elements.push({ tag: "hr" }, taskActionRow(view.navigationActions));
1115
+ }
1116
+ if (view.footerLines.length > 0) {
1117
+ elements.push({ tag: "hr" }, markdown(view.footerLines.join("\n")));
1118
+ }
1119
+ return sectionCard("文件浏览", elements, "blue", "2px");
1120
+ }
1121
+
993
1122
  renderResetHistoryCard(view: ResetHistoryCardView): Record<string, unknown> {
994
1123
  const elements: Record<string, unknown>[] = [
995
1124
  {
@@ -1124,6 +1253,77 @@ function renderTurnElements(
1124
1253
  return elements;
1125
1254
  }
1126
1255
 
1256
+ function renderGroupedTurnElements(
1257
+ state: TurnViewState,
1258
+ assistantTextMode: "hidden" | "always",
1259
+ ): Record<string, unknown>[] {
1260
+ const elements: Record<string, unknown>[] = [];
1261
+ const allActivities = turnActivities(state);
1262
+ const livePages = groupedActivityPages(allActivities, state.projectCwd);
1263
+ const visible = groupedLiveActivityPage(livePages, state.projectCwd);
1264
+ const historyPages = groupedActivityPagesFromGroups(visible.historyGroups, state.projectCwd, {
1265
+ fullActivityText: true,
1266
+ includeReasoningHistory: true,
1267
+ });
1268
+ if (state.plan.length > 0) elements.push(planPanel(state.plan));
1269
+ if (historyPages.length > 0) {
1270
+ elements.push(taskActionRow([{
1271
+ text: `查看历史思考(共 ${historyPages.length + 1} 页)`,
1272
+ value: {
1273
+ action: "activity_history",
1274
+ turnId: state.turnId,
1275
+ page: String(historyPages.length - 1),
1276
+ },
1277
+ }]));
1278
+ }
1279
+ if (state.activitiesTruncated && visible.groups[0]?.kind !== "gap") elements.push(markdown("…"));
1280
+ elements.push(...renderGroupedActivityGroups(visible.groups, state.projectCwd));
1281
+ if (state.fileSummary.length > 0) elements.push(fileSummaryPanel(state));
1282
+
1283
+ if (state.approval) {
1284
+ const request = state.approval;
1285
+ elements.push(markdown([
1286
+ `**${request.title}**`,
1287
+ request.command ? codeBlock(request.command, 800) : undefined,
1288
+ request.reason,
1289
+ ].filter(Boolean).join("\n")));
1290
+ elements.push({
1291
+ tag: "column_set",
1292
+ flex_mode: "flow",
1293
+ horizontal_spacing: "8px",
1294
+ vertical_spacing: "8px",
1295
+ columns: request.options.map((option) => ({
1296
+ tag: "column",
1297
+ width: "auto",
1298
+ elements: [{
1299
+ tag: "button",
1300
+ text: { tag: "plain_text", content: approvalDecisionLabel(option.id) },
1301
+ type: option.id === "accept" || option.id === "acceptForSession" ? "primary" : option.id === "cancel" ? "danger" : "default",
1302
+ behaviors: [{
1303
+ type: "callback",
1304
+ value: {
1305
+ action: "approval",
1306
+ sessionId: state.sessionId,
1307
+ turnId: state.turnId,
1308
+ requestId: request.id,
1309
+ decision: option.id,
1310
+ },
1311
+ }],
1312
+ }],
1313
+ })),
1314
+ });
1315
+ }
1316
+ if (state.error) elements.push(markdown(codeBlock(state.error, 2_000)));
1317
+ const showAssistantText = state.assistantText && assistantTextMode === "always";
1318
+ if (showAssistantText) {
1319
+ if (elements.length > 0) elements.push({ tag: "hr" });
1320
+ const heading = state.status === "completed" ? "回答" : "回答生成中";
1321
+ elements.push(markdown(`**${heading}**\n${truncateText(state.assistantText, 3_000)}`));
1322
+ }
1323
+ if (elements.length === 0) elements.push(markdown(emptyTurnText(state.status, state.agentLabel)));
1324
+ return elements;
1325
+ }
1326
+
1127
1327
  function emptyTurnText(status: TurnViewStatus, agentLabel?: string): string {
1128
1328
  const label = agentLabel?.trim().replace(/[\r\n]+/g, " ") || "Agent";
1129
1329
  if (status === "starting") return `正在连接 ${label}…`;
@@ -1216,6 +1416,135 @@ function renderActivities(
1216
1416
  return elements;
1217
1417
  }
1218
1418
 
1419
+ type GroupedTurnActivity =
1420
+ | { kind: "activity"; activity: TurnActivity }
1421
+ | { kind: "gap"; id: string }
1422
+ | {
1423
+ kind: "execution";
1424
+ id: string;
1425
+ latestReasoning?: Extract<TurnActivity, { kind: "reasoning" }>;
1426
+ reasonings: Array<Extract<TurnActivity, { kind: "reasoning" }>>;
1427
+ tools: ToolState[];
1428
+ };
1429
+
1430
+ function renderGroupedActivityGroups(
1431
+ groups: GroupedTurnActivity[],
1432
+ projectCwd?: string,
1433
+ options: {
1434
+ fullActivityText?: boolean;
1435
+ includeReasoningHistory?: boolean;
1436
+ } = {},
1437
+ ): Record<string, unknown>[] {
1438
+ return groups.flatMap((group) => {
1439
+ if (group.kind === "gap") return [markdown("…")];
1440
+ if (group.kind === "activity") {
1441
+ return renderActivity(group.activity, projectCwd, options.fullActivityText === true);
1442
+ }
1443
+ if (group.tools.length === 0) {
1444
+ const reasonings = options.includeReasoningHistory
1445
+ ? group.reasonings
1446
+ : group.latestReasoning ? [group.latestReasoning] : [];
1447
+ return renderReasoningGroup(reasonings);
1448
+ }
1449
+ return [executionActivityPanel(group, projectCwd, options.includeReasoningHistory === true)];
1450
+ });
1451
+ }
1452
+
1453
+ function groupTurnActivities(activities: TurnActivity[]): GroupedTurnActivity[] {
1454
+ const groups: GroupedTurnActivity[] = [];
1455
+ let segment: TurnActivity[] = [];
1456
+ const flushSegment = (): void => {
1457
+ if (segment.length === 0) return;
1458
+ let execution: Extract<GroupedTurnActivity, { kind: "execution" }> | undefined;
1459
+ let executionPosition = -1;
1460
+ for (let index = 0; index < segment.length; index += 1) {
1461
+ const activity = segment[index]!;
1462
+ if (isRawReasoning(activity)) {
1463
+ execution ??= { kind: "execution", id: activity.id, reasonings: [], tools: [] };
1464
+ execution.reasonings.push(activity);
1465
+ execution.latestReasoning = activity;
1466
+ executionPosition = index;
1467
+ } else if (activity.kind === "tool") {
1468
+ execution ??= { kind: "execution", id: activity.id, reasonings: [], tools: [] };
1469
+ execution.tools.push(activity.tool);
1470
+ executionPosition = index;
1471
+ }
1472
+ }
1473
+ for (let index = 0; index < segment.length; index += 1) {
1474
+ const activity = segment[index]!;
1475
+ if (index === executionPosition && execution) groups.push(execution);
1476
+ if (!isRawReasoning(activity) && activity.kind !== "tool") {
1477
+ groups.push({ kind: "activity", activity });
1478
+ }
1479
+ }
1480
+ segment = [];
1481
+ };
1482
+
1483
+ for (const activity of activities) {
1484
+ if (!isCommentaryActivity(activity)) {
1485
+ segment.push(activity);
1486
+ continue;
1487
+ }
1488
+ flushSegment();
1489
+ groups.push({ kind: "activity", activity });
1490
+ }
1491
+ flushSegment();
1492
+ return groups;
1493
+ }
1494
+
1495
+ function isCommentaryActivity(activity: TurnActivity): boolean {
1496
+ return activity.kind === "assistant"
1497
+ || (activity.kind === "reasoning" && activity.id.startsWith("commentary:"));
1498
+ }
1499
+
1500
+ function executionActivityPanel(
1501
+ group: Extract<GroupedTurnActivity, { kind: "execution" }>,
1502
+ projectCwd: string | undefined,
1503
+ includeReasoningHistory = false,
1504
+ ): Record<string, unknown> {
1505
+ const status = executionActivityStatus(group.tools);
1506
+ const elements = [
1507
+ ...(includeReasoningHistory ? renderReasoningGroup(group.reasonings) : []),
1508
+ ...group.tools.map((tool) => toolPanel(tool, projectCwd)),
1509
+ ];
1510
+ return collapsiblePanel(
1511
+ executionActivityTitle(group, status),
1512
+ elements,
1513
+ {
1514
+ elementId: executionActivityPanelElementId(group.id),
1515
+ expanded: false,
1516
+ borderColor: status === "running" ? "blue" : "grey",
1517
+ compact: true,
1518
+ },
1519
+ );
1520
+ }
1521
+
1522
+ function executionActivityStatus(tools: ToolState[]): ToolState["status"] {
1523
+ if (tools.some((tool) => tool.status === "running")) return "running";
1524
+ return "completed";
1525
+ }
1526
+
1527
+ function executionActivityTitle(
1528
+ group: Extract<GroupedTurnActivity, { kind: "execution" }>,
1529
+ status: ToolState["status"],
1530
+ ): string {
1531
+ const icon = status === "running" ? "⏳" : "💭";
1532
+ const latestReasoning = group.latestReasoning?.text
1533
+ ? removeMarkdownBold(group.latestReasoning.text).replace(/\s+/g, " ").trim()
1534
+ : "";
1535
+ const summary = latestReasoning
1536
+ ? truncateText(latestReasoning, 90)
1537
+ : status === "running"
1538
+ ? "正在执行工具"
1539
+ : "已执行工具";
1540
+ return `${icon} ${summary} · ${group.tools.length} 个工具`;
1541
+ }
1542
+
1543
+ function executionActivityPanelElementId(activityId: string): string {
1544
+ const digest = createHash("sha256").update(activityId).digest("hex").slice(0, 10);
1545
+ return `turn_exec_${digest}`;
1546
+ }
1547
+
1219
1548
  function renderReasoningGroup(
1220
1549
  activities: Array<Extract<TurnActivity, { kind: "reasoning" }>>,
1221
1550
  ): Record<string, unknown>[] {
@@ -1237,6 +1566,10 @@ function isRawReasoning(
1237
1566
  }
1238
1567
 
1239
1568
  const ACTIVITIES_PER_PAGE = 40;
1569
+ const GROUPED_TOOLS_PER_PANEL = 8;
1570
+ const GROUPED_PAGE_ACTIVITY_BYTES = 24 * 1024;
1571
+ const GROUPED_PAGE_ACTIVITY_COMPONENTS = 160;
1572
+ const PINNED_LIVE_COMMENTARIES = 3;
1240
1573
  const MAX_LIVE_ASSISTANT_TEXT = 2_000;
1241
1574
  const ACTIVITY_TEXT_CHUNK = 2_500;
1242
1575
 
@@ -1252,6 +1585,159 @@ function activityPages(activities: TurnActivity[]): TurnActivity[][] {
1252
1585
  return pages;
1253
1586
  }
1254
1587
 
1588
+ function groupedActivityPages(
1589
+ activities: TurnActivity[],
1590
+ projectCwd?: string,
1591
+ renderOptions: {
1592
+ fullActivityText?: boolean;
1593
+ includeReasoningHistory?: boolean;
1594
+ } = {},
1595
+ ): GroupedTurnActivity[][] {
1596
+ const groups = groupTurnActivities(activities).flatMap(splitGroupedExecutionActivity);
1597
+ return groupedActivityPagesFromGroups(groups, projectCwd, renderOptions);
1598
+ }
1599
+
1600
+ function groupedActivityPagesFromGroups(
1601
+ groups: GroupedTurnActivity[],
1602
+ projectCwd?: string,
1603
+ renderOptions: {
1604
+ fullActivityText?: boolean;
1605
+ includeReasoningHistory?: boolean;
1606
+ } = {},
1607
+ ): GroupedTurnActivity[][] {
1608
+ if (groups.length === 0) return [];
1609
+
1610
+ const newestFirst: GroupedTurnActivity[][] = [];
1611
+ let page: GroupedTurnActivity[] = [];
1612
+ for (let index = groups.length - 1; index >= 0; index -= 1) {
1613
+ const group = groups[index]!;
1614
+ const candidate = [group, ...page];
1615
+ if (page.length > 0 && !groupedActivityPageFits(candidate, projectCwd, renderOptions)) {
1616
+ newestFirst.push(page);
1617
+ page = [];
1618
+ }
1619
+ page.unshift(group);
1620
+ }
1621
+ if (page.length > 0) newestFirst.push(page);
1622
+ return newestFirst.reverse();
1623
+ }
1624
+
1625
+ function groupedActivityPageFits(
1626
+ groups: GroupedTurnActivity[],
1627
+ projectCwd: string | undefined,
1628
+ renderOptions: {
1629
+ fullActivityText?: boolean;
1630
+ includeReasoningHistory?: boolean;
1631
+ },
1632
+ ): boolean {
1633
+ const elements = renderGroupedActivityGroups(groups, projectCwd, renderOptions);
1634
+ return Buffer.byteLength(JSON.stringify(elements), "utf8") <= GROUPED_PAGE_ACTIVITY_BYTES
1635
+ && countCardComponents(elements) <= GROUPED_PAGE_ACTIVITY_COMPONENTS;
1636
+ }
1637
+
1638
+ interface LiveGroupedActivityPage {
1639
+ groups: GroupedTurnActivity[];
1640
+ historyGroups: GroupedTurnActivity[];
1641
+ }
1642
+
1643
+ function groupedLiveActivityPage(
1644
+ pages: GroupedTurnActivity[][],
1645
+ projectCwd?: string,
1646
+ ): LiveGroupedActivityPage {
1647
+ const latest = pages.at(-1) ?? [];
1648
+ if (pages.length <= 1) return { groups: latest, historyGroups: [] };
1649
+
1650
+ const groups = pages.flat();
1651
+ const latestStart = groups.length - latest.length;
1652
+ const commentaryIndexes = groups
1653
+ .map((group, index) => isCommentaryGroup(group) ? index : -1)
1654
+ .filter((index) => index >= 0);
1655
+ if (commentaryIndexes.length === 0) {
1656
+ return { groups: latest, historyGroups: groups.slice(0, latestStart) };
1657
+ }
1658
+
1659
+ const selected = new Set<number>();
1660
+ const pinnedCommentaries = commentaryIndexes.slice(0, PINNED_LIVE_COMMENTARIES);
1661
+ for (const index of pinnedCommentaries) selected.add(index);
1662
+ const latestCommentary = commentaryIndexes.at(-1)!;
1663
+ for (let index = latestCommentary; index < groups.length; index += 1) selected.add(index);
1664
+
1665
+ const renderSelection = (selection: ReadonlySet<number>): LiveGroupedActivityPage => ({
1666
+ groups: groupsWithGaps(groups, selection),
1667
+ historyGroups: groups.filter((_group, index) => index < latestCommentary || !selection.has(index)),
1668
+ });
1669
+ const fits = (selection: ReadonlySet<number>): boolean => {
1670
+ const view = renderSelection(selection);
1671
+ return groupedActivityPageFits(view.groups, projectCwd, {});
1672
+ };
1673
+
1674
+ if (!fits(selected)) {
1675
+ const latestExecutions = [...selected]
1676
+ .filter((index) => index > latestCommentary && groups[index]?.kind === "execution")
1677
+ .sort((left, right) => left - right);
1678
+ const newestExecution = latestExecutions.at(-1);
1679
+ for (const index of latestExecutions) {
1680
+ if (fits(selected) || index === newestExecution) break;
1681
+ selected.delete(index);
1682
+ }
1683
+ }
1684
+
1685
+ return renderSelection(selected);
1686
+ }
1687
+
1688
+ function isCommentaryGroup(group: GroupedTurnActivity): boolean {
1689
+ return group.kind === "activity" && isCommentaryActivity(group.activity);
1690
+ }
1691
+
1692
+ function groupsWithGaps(
1693
+ groups: GroupedTurnActivity[],
1694
+ selected: ReadonlySet<number>,
1695
+ ): GroupedTurnActivity[] {
1696
+ const indexes = [...selected].sort((left, right) => left - right);
1697
+ const visible: GroupedTurnActivity[] = [];
1698
+ const append = (group: GroupedTurnActivity): void => {
1699
+ if (group.kind === "gap" && visible.at(-1)?.kind === "gap") return;
1700
+ visible.push(group);
1701
+ };
1702
+ let previous = -1;
1703
+ for (const index of indexes) {
1704
+ if (previous >= 0 && index > previous + 1) {
1705
+ append({ kind: "gap", id: `gap:${previous}:${index}` });
1706
+ }
1707
+ const group = groups[index];
1708
+ if (group) append(group);
1709
+ previous = index;
1710
+ }
1711
+ return visible;
1712
+ }
1713
+
1714
+ function countCardComponents(value: unknown): number {
1715
+ if (Array.isArray(value)) {
1716
+ return value.reduce((total, entry) => total + countCardComponents(entry), 0);
1717
+ }
1718
+ if (!value || typeof value !== "object") return 0;
1719
+ const record = value as Record<string, unknown>;
1720
+ return (typeof record.tag === "string" ? 1 : 0)
1721
+ + Object.values(record).reduce<number>((total, entry) => total + countCardComponents(entry), 0);
1722
+ }
1723
+
1724
+ function splitGroupedExecutionActivity(group: GroupedTurnActivity): GroupedTurnActivity[] {
1725
+ if (group.kind !== "execution" || group.tools.length <= GROUPED_TOOLS_PER_PANEL) return [group];
1726
+ const chunks: Array<Extract<GroupedTurnActivity, { kind: "execution" }>> = [];
1727
+ for (let index = 0; index < group.tools.length; index += GROUPED_TOOLS_PER_PANEL) {
1728
+ const tools = group.tools.slice(index, index + GROUPED_TOOLS_PER_PANEL);
1729
+ const isLast = index + GROUPED_TOOLS_PER_PANEL >= group.tools.length;
1730
+ chunks.push({
1731
+ kind: "execution",
1732
+ id: index === 0 ? group.id : tools[0]?.id ?? `${group.id}:${index}`,
1733
+ ...(isLast && group.latestReasoning ? { latestReasoning: group.latestReasoning } : {}),
1734
+ reasonings: isLast ? group.reasonings : [],
1735
+ tools,
1736
+ });
1737
+ }
1738
+ return chunks;
1739
+ }
1740
+
1255
1741
  function splitText(value: string, maxLength: number): string[] {
1256
1742
  if (!value) return [];
1257
1743
  const chunks: string[] = [];
@@ -1302,20 +1788,26 @@ function toolPanelElementId(toolId: string): string {
1302
1788
  function collapsiblePanel(
1303
1789
  title: string,
1304
1790
  content: string | Record<string, unknown>[],
1305
- options: { expanded?: boolean; borderColor?: string; elementId?: string } = {},
1791
+ options: {
1792
+ expanded?: boolean;
1793
+ borderColor?: string;
1794
+ elementId?: string;
1795
+ compact?: boolean;
1796
+ } = {},
1306
1797
  ): Record<string, unknown> {
1798
+ const compact = options.compact === true;
1307
1799
  return {
1308
1800
  tag: "collapsible_panel",
1309
1801
  ...(options.elementId ? { element_id: options.elementId } : {}),
1310
1802
  direction: "vertical",
1311
- vertical_spacing: "4px",
1312
- padding: "8px",
1803
+ vertical_spacing: compact ? "2px" : "4px",
1804
+ padding: compact ? "4px 6px" : "8px",
1313
1805
  margin: "0px",
1314
1806
  expanded: options.expanded ?? false,
1315
1807
  header: {
1316
1808
  title: { tag: "plain_text", content: title },
1317
1809
  vertical_align: "center",
1318
- padding: "4px 8px 4px 8px",
1810
+ padding: compact ? "2px 4px 2px 4px" : "4px 8px 4px 8px",
1319
1811
  },
1320
1812
  border: {
1321
1813
  color: options.borderColor ?? "grey",
@@ -1325,6 +1817,68 @@ function collapsiblePanel(
1325
1817
  };
1326
1818
  }
1327
1819
 
1820
+ function sessionTaskPanel(entry: SessionTaskCardEntry): Record<string, unknown> {
1821
+ const elements: Record<string, unknown>[] = [markdown(entry.detailLines.join("\n"))];
1822
+ if (entry.actions?.length) elements.push(sessionActionOverflow(entry.actions));
1823
+ return collapsiblePanel(entry.summary, elements, {
1824
+ elementId: sessionTaskPanelElementId(entry.reference),
1825
+ borderColor: entry.current ? "green" : "grey",
1826
+ compact: true,
1827
+ });
1828
+ }
1829
+
1830
+ function sessionProjectRow(group: SessionTaskCardGroup): Record<string, unknown> {
1831
+ return {
1832
+ tag: "column_set",
1833
+ flex_mode: "none",
1834
+ horizontal_spacing: "8px",
1835
+ vertical_align: "center",
1836
+ margin: "6px 0px 0px 0px",
1837
+ columns: [
1838
+ {
1839
+ tag: "column",
1840
+ width: "weighted",
1841
+ weight: 1,
1842
+ vertical_align: "center",
1843
+ elements: [markdown(`**${escapeCardActionText(group.title)}**`)],
1844
+ },
1845
+ ...(group.actions ?? []).map((action) => ({
1846
+ tag: "column",
1847
+ width: "auto",
1848
+ vertical_align: "center",
1849
+ elements: [sessionActionButton(action)],
1850
+ })),
1851
+ ],
1852
+ };
1853
+ }
1854
+
1855
+ function sessionTaskPanelElementId(reference: string): string {
1856
+ return `session_task_${createHash("sha256").update(reference).digest("hex").slice(0, 16)}`;
1857
+ }
1858
+
1859
+ function sessionActionOverflow(actions: TaskListCardAction[]): Record<string, unknown> {
1860
+ return {
1861
+ tag: "overflow",
1862
+ options: actions.map((action) => ({
1863
+ text: { tag: "plain_text", content: action.text },
1864
+ value: JSON.stringify(action.value),
1865
+ })),
1866
+ };
1867
+ }
1868
+
1869
+ function sessionActionButton(action: TaskListCardAction): Record<string, unknown> {
1870
+ return {
1871
+ tag: "button",
1872
+ text: { tag: "plain_text", content: action.text },
1873
+ type: action.type === "danger" ? "danger" : "default",
1874
+ size: "tiny",
1875
+ behaviors: [{
1876
+ type: "callback",
1877
+ value: action.value,
1878
+ }],
1879
+ };
1880
+ }
1881
+
1328
1882
  function renderToolDetails(tool: ToolState, projectCwd?: string): string {
1329
1883
  const command = tool.command ?? tool.title;
1330
1884
  const fileSummary = tool.files?.length
@@ -1335,13 +1889,22 @@ function renderToolDetails(tool: ToolState, projectCwd?: string): string {
1335
1889
  const result = tool.error ?? tool.output ?? fileSummary;
1336
1890
  const normalizedCommand = stripAnsi(command).trim();
1337
1891
  const displayCommand = tool.kind === "command"
1338
- ? unwrapPowerShellCommand(normalizedCommand) ?? normalizedCommand
1892
+ ? unwrapShellCommand(normalizedCommand) ?? normalizedCommand
1339
1893
  : normalizedCommand;
1340
- const commandText = truncateText(displayCommand, 800);
1341
- const resultText = result ? truncateMiddle(stripAnsi(result).trim(), 1_200) : undefined;
1894
+ const commandText = truncateText(displayCommand, 600);
1895
+ const resultText = result ? truncateToolResult(stripAnsi(result).trim()) : undefined;
1342
1896
  return codeBlock([`$ ${commandText}`, resultText].filter((part): part is string => part !== undefined).join("\n"), 2_003);
1343
1897
  }
1344
1898
 
1899
+ function truncateToolResult(result: string): string {
1900
+ const maxLength = 900;
1901
+ const marker = "\n...\n";
1902
+ const tailLength = 600;
1903
+ if (result.length <= maxLength) return result;
1904
+ const headLength = maxLength - marker.length - tailLength;
1905
+ return `${result.slice(0, headLength)}${marker}${result.slice(-tailLength)}`;
1906
+ }
1907
+
1345
1908
  function displayFilePath(filePath: string, projectCwd?: string): string {
1346
1909
  if (!projectCwd) return filePath;
1347
1910
  const pathApi = usesWindowsPaths(projectCwd, filePath) ? path.win32 : path;
@@ -1370,7 +1933,7 @@ function toolPanelTitle(tool: ToolState): string {
1370
1933
  const command = stripAnsi(tool.command ?? tool.title).trim();
1371
1934
  const meaningfulCommand = tool.kind === "web_search"
1372
1935
  ? tool.title
1373
- : unwrapPowerShellCommand(command) ?? tool.title;
1936
+ : unwrapShellCommand(command) ?? tool.title;
1374
1937
  const duration = toolDuration(tool);
1375
1938
  const prefix = `${icon} `;
1376
1939
  const suffix = duration ? ` · ${duration}` : "";
@@ -1419,6 +1982,10 @@ function formatTurnDuration(durationMs: number): string {
1419
1982
  return `${seconds}s`;
1420
1983
  }
1421
1984
 
1985
+ function unwrapShellCommand(command: string): string | undefined {
1986
+ return unwrapPowerShellCommand(command) ?? unwrapPosixShellCommand(command);
1987
+ }
1988
+
1422
1989
  function unwrapPowerShellCommand(command: string): string | undefined {
1423
1990
  const executable = command.match(
1424
1991
  /^(?:"[^"]*(?:pwsh|powershell)\.exe"|(?:\S*[\\/])?(?:pwsh|powershell)(?:\.exe)?)(?=\s|$)/i,
@@ -1429,6 +1996,71 @@ function unwrapPowerShellCommand(command: string): string | undefined {
1429
1996
  if (!commandFlag) return undefined;
1430
1997
  const payload = args.slice(commandFlag.index + commandFlag[0].length).trim();
1431
1998
  if (!payload) return undefined;
1999
+ return unwrapQuotedPayload(payload);
2000
+ }
2001
+
2002
+ function unwrapPosixShellCommand(command: string): string | undefined {
2003
+ let executable = readCommandWord(command, 0);
2004
+ if (!executable) return undefined;
2005
+
2006
+ if (posixExecutableName(executable.value) === "env") {
2007
+ let cursor = executable.end;
2008
+ while (true) {
2009
+ const candidate = readCommandWord(command, cursor);
2010
+ if (!candidate) return undefined;
2011
+ const name = posixExecutableName(candidate.value);
2012
+ if (isPosixShell(name)) {
2013
+ executable = candidate;
2014
+ break;
2015
+ }
2016
+ if (!candidate.value.startsWith("-") && !/^[A-Za-z_][A-Za-z0-9_]*=/.test(candidate.value)) {
2017
+ return undefined;
2018
+ }
2019
+ cursor = candidate.end;
2020
+ }
2021
+ } else if (!isPosixShell(posixExecutableName(executable.value))) {
2022
+ return undefined;
2023
+ }
2024
+
2025
+ const args = command.slice(executable.end);
2026
+ const commandFlag = /(?:^|\s)(?:-[A-Za-z]*c[A-Za-z]*|--command)(?:\s+|=)/.exec(args);
2027
+ if (!commandFlag) return undefined;
2028
+ const payload = args.slice(commandFlag.index + commandFlag[0].length).trim();
2029
+ if (!payload) return undefined;
2030
+ return unwrapQuotedPayload(payload);
2031
+ }
2032
+
2033
+ function readCommandWord(command: string, start: number): { value: string; end: number } | undefined {
2034
+ let cursor = start;
2035
+ while (cursor < command.length && /\s/.test(command[cursor] ?? "")) cursor += 1;
2036
+ if (cursor >= command.length) return undefined;
2037
+
2038
+ const quote = command[cursor];
2039
+ if (quote === "\"" || quote === "'") {
2040
+ const endQuote = command.indexOf(quote, cursor + 1);
2041
+ if (endQuote < 0) return undefined;
2042
+ return { value: command.slice(cursor + 1, endQuote), end: endQuote + 1 };
2043
+ }
2044
+
2045
+ const word = /^\S+/.exec(command.slice(cursor));
2046
+ if (!word) return undefined;
2047
+ return { value: word[0], end: cursor + word[0].length };
2048
+ }
2049
+
2050
+ function posixExecutableName(executable: string): string {
2051
+ return executable.replaceAll("\\", "/").split("/").at(-1)?.toLowerCase() ?? "";
2052
+ }
2053
+
2054
+ function isPosixShell(executable: string): boolean {
2055
+ return executable === "sh"
2056
+ || executable === "bash"
2057
+ || executable === "zsh"
2058
+ || executable === "dash"
2059
+ || executable === "ksh"
2060
+ || executable === "fish";
2061
+ }
2062
+
2063
+ function unwrapQuotedPayload(payload: string): string {
1432
2064
  const quote = payload[0];
1433
2065
  if ((quote === "\"" || quote === "'") && payload.at(-1) === quote) return payload.slice(1, -1).trim();
1434
2066
  return payload;
@@ -1726,6 +2358,69 @@ function taskActionRow(actions: TaskListCardAction[]): Record<string, unknown> {
1726
2358
  };
1727
2359
  }
1728
2360
 
2361
+ function directoryBrowserEntryRow(entry: DirectoryBrowserCardEntry): Record<string, unknown> {
2362
+ const label = `${directoryBrowserEntryIcon(entry.kind)} ${entry.name}`;
2363
+ return {
2364
+ tag: "column_set",
2365
+ flex_mode: "none",
2366
+ horizontal_spacing: "12px",
2367
+ vertical_align: "center",
2368
+ margin: "0px",
2369
+ columns: [
2370
+ {
2371
+ tag: "column",
2372
+ width: "weighted",
2373
+ weight: 1,
2374
+ vertical_align: "center",
2375
+ elements: entry.openAction
2376
+ ? [taskActionElement({ ...entry.openAction, text: label })]
2377
+ : [markdown(escapeCardHtml(label))],
2378
+ },
2379
+ ...(entry.actions ?? []).map((action) => ({
2380
+ tag: "column",
2381
+ width: "auto",
2382
+ vertical_align: "center",
2383
+ elements: [taskActionElement(action)],
2384
+ })),
2385
+ ],
2386
+ };
2387
+ }
2388
+
2389
+ function directoryBrowserEmptyRow(): Record<string, unknown> {
2390
+ return directoryBrowserStaticRow("这个目录为空。");
2391
+ }
2392
+
2393
+ function directoryBrowserPlaceholderRow(): Record<string, unknown> {
2394
+ return directoryBrowserStaticRow("\u00a0");
2395
+ }
2396
+
2397
+ function directoryBrowserStaticRow(content: string): Record<string, unknown> {
2398
+ return {
2399
+ tag: "column_set",
2400
+ flex_mode: "none",
2401
+ horizontal_spacing: "12px",
2402
+ vertical_align: "center",
2403
+ margin: "0px",
2404
+ columns: [{
2405
+ tag: "column",
2406
+ width: "weighted",
2407
+ weight: 1,
2408
+ vertical_align: "center",
2409
+ elements: [markdown(content)],
2410
+ }],
2411
+ };
2412
+ }
2413
+
2414
+ function directoryBrowserEntryIcon(kind: DirectoryBrowserCardEntry["kind"]): string {
2415
+ switch (kind) {
2416
+ case "directory": return "📁";
2417
+ case "drive": return "💽";
2418
+ case "image": return "🖼️";
2419
+ case "binary": return "📦";
2420
+ case "file": return "📄";
2421
+ }
2422
+ }
2423
+
1729
2424
  function taskActionElement(action: TaskListCardAction): Record<string, unknown> {
1730
2425
  return {
1731
2426
  tag: "interactive_container",
@@ -1733,7 +2428,7 @@ function taskActionElement(action: TaskListCardAction): Record<string, unknown>
1733
2428
  padding: "0px",
1734
2429
  has_border: false,
1735
2430
  elements: [markdown(
1736
- `<font color='${action.type === "danger" ? "red" : "blue"}'>${escapeCardHtml(action.text)}</font>`,
2431
+ `<font color='${action.type === "danger" ? "red" : "blue"}'>${escapeCardActionText(action.text)}</font>`,
1737
2432
  )],
1738
2433
  behaviors: [{
1739
2434
  type: "callback",
@@ -1742,6 +2437,10 @@ function taskActionElement(action: TaskListCardAction): Record<string, unknown>
1742
2437
  };
1743
2438
  }
1744
2439
 
2440
+ function escapeCardActionText(value: string): string {
2441
+ return escapeCardHtml(value).replaceAll("\\", "&#92;");
2442
+ }
2443
+
1745
2444
  function helpCommandRow(command: HelpCardCommand): Record<string, unknown> {
1746
2445
  const details = [
1747
2446
  command.usage ? `**${command.usage}**` : undefined,
@@ -1812,6 +2511,7 @@ function sectionCard(
1812
2511
  title: string,
1813
2512
  elements: Record<string, unknown>[],
1814
2513
  template = "blue",
2514
+ verticalSpacing = "8px",
1815
2515
  ): Record<string, unknown> {
1816
2516
  return {
1817
2517
  schema: "2.0",
@@ -1829,7 +2529,7 @@ function sectionCard(
1829
2529
  },
1830
2530
  body: {
1831
2531
  direction: "vertical",
1832
- vertical_spacing: "8px",
2532
+ vertical_spacing: verticalSpacing,
1833
2533
  padding: "12px 12px 12px 12px",
1834
2534
  elements,
1835
2535
  },