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

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 (74) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +41 -44
  3. package/README.zh.md +41 -44
  4. package/config.example.yaml +4 -0
  5. package/dist/codex/CodexRuntime.js +8 -3
  6. package/dist/codex/CodexRuntime.js.map +1 -1
  7. package/dist/commands/CommandRouter.js +13 -0
  8. package/dist/commands/CommandRouter.js.map +1 -1
  9. package/dist/config/loadConfig.js +2 -0
  10. package/dist/config/loadConfig.js.map +1 -1
  11. package/dist/config/schema.js +2 -0
  12. package/dist/config/schema.js.map +1 -1
  13. package/dist/console/ConsoleTurnPresenter.js +2 -0
  14. package/dist/console/ConsoleTurnPresenter.js.map +1 -1
  15. package/dist/feishu/CardRenderer.js +428 -9
  16. package/dist/feishu/CardRenderer.js.map +1 -1
  17. package/dist/feishu/FeishuConnector.js +29 -8
  18. package/dist/feishu/FeishuConnector.js.map +1 -1
  19. package/dist/feishu/FeishuMessageClient.js +43 -0
  20. package/dist/feishu/FeishuMessageClient.js.map +1 -1
  21. package/dist/feishu/FeishuTurnPresenter.js +29 -1
  22. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  23. package/dist/feishu/ownerAccess.js +7 -0
  24. package/dist/feishu/ownerAccess.js.map +1 -0
  25. package/dist/index.js +14 -17
  26. package/dist/index.js.map +1 -1
  27. package/dist/presentation/OutboundRouter.js +19 -1
  28. package/dist/presentation/OutboundRouter.js.map +1 -1
  29. package/dist/proxy/ProxySessionController.js +1012 -58
  30. package/dist/proxy/ProxySessionController.js.map +1 -1
  31. package/dist/startup/StartupNotifier.js +65 -16
  32. package/dist/startup/StartupNotifier.js.map +1 -1
  33. package/dist/startup/startFeishu.js +2 -2
  34. package/dist/startup/startFeishu.js.map +1 -1
  35. package/dist/state/StateStore.js +250 -67
  36. package/dist/state/StateStore.js.map +1 -1
  37. package/dist/state/migrations.js +26 -0
  38. package/dist/state/migrations.js.map +1 -1
  39. package/dist/supervision/SafeRestartNotifier.js +17 -16
  40. package/dist/supervision/SafeRestartNotifier.js.map +1 -1
  41. package/dist/supervision/SafeRestartScheduler.js +12 -7
  42. package/dist/supervision/SafeRestartScheduler.js.map +1 -1
  43. package/dist/supervision/replacementSupervisor.js +45 -4
  44. package/dist/supervision/replacementSupervisor.js.map +1 -1
  45. package/dist/supervisor.js +30 -18
  46. package/dist/supervisor.js.map +1 -1
  47. package/docs/technical-reference.md +18 -5
  48. package/docs/technical-reference.zh.md +18 -5
  49. package/npm-shrinkwrap.json +2 -2
  50. package/package.json +1 -1
  51. package/skills/agent-bot/SKILL.md +8 -2
  52. package/src/codex/CodexRuntime.ts +9 -3
  53. package/src/commands/CommandRouter.ts +13 -0
  54. package/src/commands/commandTypes.ts +2 -0
  55. package/src/config/loadConfig.ts +2 -0
  56. package/src/config/schema.ts +2 -0
  57. package/src/console/ConsoleTurnPresenter.ts +4 -1
  58. package/src/feishu/CardRenderer.ts +533 -9
  59. package/src/feishu/FeishuConnector.ts +44 -10
  60. package/src/feishu/FeishuMessageClient.ts +58 -0
  61. package/src/feishu/FeishuTurnPresenter.ts +35 -2
  62. package/src/feishu/ownerAccess.ts +7 -0
  63. package/src/feishu/types.ts +7 -0
  64. package/src/index.ts +20 -20
  65. package/src/presentation/OutboundRouter.ts +28 -3
  66. package/src/proxy/ProxySessionController.ts +1177 -57
  67. package/src/startup/StartupNotifier.ts +67 -17
  68. package/src/startup/startFeishu.ts +5 -3
  69. package/src/state/StateStore.ts +394 -65
  70. package/src/state/migrations.ts +26 -0
  71. package/src/supervision/SafeRestartNotifier.ts +17 -17
  72. package/src/supervision/SafeRestartScheduler.ts +13 -6
  73. package/src/supervision/replacementSupervisor.ts +50 -4
  74. package/src/supervisor.ts +35 -16
@@ -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,7 +888,13 @@ 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 pages = grouped
893
+ ? groupedActivityPages(turnActivities(state), state.projectCwd, {
894
+ fullActivityText: true,
895
+ includeReasoningHistory: true,
896
+ })
897
+ : activityPages(turnActivities(state));
847
898
  if (pages.length === 0) {
848
899
  return this.renderSectionsCard("思考活动历史", [{ lines: ["没有保存到活动记录。"] }]);
849
900
  }
@@ -864,7 +915,12 @@ export class CardRenderer {
864
915
  ];
865
916
  const elements: Record<string, unknown>[] = [];
866
917
  if (actions.length > 0) elements.push(taskActionRow(actions), { tag: "hr" });
867
- elements.push(...renderActivities(pages[page] ?? [], state.projectCwd, true));
918
+ elements.push(...(grouped
919
+ ? renderGroupedActivityGroups(pages[page] as GroupedTurnActivity[] | undefined ?? [], state.projectCwd, {
920
+ fullActivityText: true,
921
+ includeReasoningHistory: true,
922
+ })
923
+ : renderActivities(pages[page] as TurnActivity[] | undefined ?? [], state.projectCwd, true)));
868
924
  return sectionCard(`思考活动历史 · ${page + 1}/${pages.length}`, elements.length > 0 ? elements : [markdown("无")]);
869
925
  }
870
926
 
@@ -990,6 +1046,68 @@ export class CardRenderer {
990
1046
  };
991
1047
  }
992
1048
 
1049
+ renderSessionTaskListCard(
1050
+ title: string,
1051
+ sectionTitle: string,
1052
+ groups: SessionTaskCardGroup[],
1053
+ footerLines: string[],
1054
+ footerActions: TaskListCardAction[] = [],
1055
+ ): Record<string, unknown> {
1056
+ const elements: Record<string, unknown>[] = [markdown(`**${sectionTitle}**`)];
1057
+ if (groups.length === 0) {
1058
+ elements.push(markdown("无"));
1059
+ } else {
1060
+ groups.forEach((group) => {
1061
+ elements.push(sessionProjectRow(group));
1062
+ elements.push(...group.entries.map(sessionTaskPanel));
1063
+ });
1064
+ }
1065
+ if (footerActions.length > 0) {
1066
+ elements.push(taskActionRow(footerActions));
1067
+ }
1068
+ if (footerLines.length > 0) {
1069
+ elements.push(markdown(footerLines.join("\n")));
1070
+ }
1071
+ return {
1072
+ schema: "2.0",
1073
+ config: {
1074
+ update_multi: true,
1075
+ width_mode: "fill",
1076
+ },
1077
+ header: {
1078
+ template: "blue",
1079
+ title: {
1080
+ tag: "plain_text",
1081
+ content: title,
1082
+ },
1083
+ },
1084
+ body: {
1085
+ vertical_spacing: "4px",
1086
+ elements,
1087
+ },
1088
+ };
1089
+ }
1090
+
1091
+ renderDirectoryBrowserCard(view: DirectoryBrowserCardView): Record<string, unknown> {
1092
+ const elements: Record<string, unknown>[] = [
1093
+ markdown(`**当前目录**:${inlineCode(view.directory)}`),
1094
+ taskActionRow(view.currentActions),
1095
+ { tag: "hr" },
1096
+ ];
1097
+ const entryRows = view.entries.length === 0
1098
+ ? [directoryBrowserEmptyRow()]
1099
+ : view.entries.slice(0, DIRECTORY_BROWSER_ROW_COUNT).map(directoryBrowserEntryRow);
1100
+ while (entryRows.length < DIRECTORY_BROWSER_ROW_COUNT) entryRows.push(directoryBrowserPlaceholderRow());
1101
+ elements.push(...entryRows);
1102
+ if (view.navigationActions.length > 0) {
1103
+ elements.push({ tag: "hr" }, taskActionRow(view.navigationActions));
1104
+ }
1105
+ if (view.footerLines.length > 0) {
1106
+ elements.push({ tag: "hr" }, markdown(view.footerLines.join("\n")));
1107
+ }
1108
+ return sectionCard("文件浏览", elements, "blue", "2px");
1109
+ }
1110
+
993
1111
  renderResetHistoryCard(view: ResetHistoryCardView): Record<string, unknown> {
994
1112
  const elements: Record<string, unknown>[] = [
995
1113
  {
@@ -1124,6 +1242,77 @@ function renderTurnElements(
1124
1242
  return elements;
1125
1243
  }
1126
1244
 
1245
+ function renderGroupedTurnElements(
1246
+ state: TurnViewState,
1247
+ assistantTextMode: "hidden" | "always",
1248
+ ): Record<string, unknown>[] {
1249
+ const elements: Record<string, unknown>[] = [];
1250
+ const allActivities = turnActivities(state);
1251
+ const livePages = groupedActivityPages(allActivities, state.projectCwd);
1252
+ const historyPages = groupedActivityPages(allActivities, state.projectCwd, {
1253
+ fullActivityText: true,
1254
+ includeReasoningHistory: true,
1255
+ });
1256
+ const visibleGroups = livePages.at(-1) ?? [];
1257
+ if (state.plan.length > 0) elements.push(planPanel(state.plan));
1258
+ if (historyPages.length > 1) {
1259
+ elements.push(taskActionRow([{
1260
+ text: `查看历史思考(共 ${historyPages.length} 页)`,
1261
+ value: {
1262
+ action: "activity_history",
1263
+ turnId: state.turnId,
1264
+ page: String(historyPages.length - 2),
1265
+ },
1266
+ }]));
1267
+ }
1268
+ if (state.activitiesTruncated || livePages.length > 1 || historyPages.length > 1) elements.push(markdown("…"));
1269
+ elements.push(...renderGroupedActivityGroups(visibleGroups, state.projectCwd));
1270
+ if (state.fileSummary.length > 0) elements.push(fileSummaryPanel(state));
1271
+
1272
+ if (state.approval) {
1273
+ const request = state.approval;
1274
+ elements.push(markdown([
1275
+ `**${request.title}**`,
1276
+ request.command ? codeBlock(request.command, 800) : undefined,
1277
+ request.reason,
1278
+ ].filter(Boolean).join("\n")));
1279
+ elements.push({
1280
+ tag: "column_set",
1281
+ flex_mode: "flow",
1282
+ horizontal_spacing: "8px",
1283
+ vertical_spacing: "8px",
1284
+ columns: request.options.map((option) => ({
1285
+ tag: "column",
1286
+ width: "auto",
1287
+ elements: [{
1288
+ tag: "button",
1289
+ text: { tag: "plain_text", content: approvalDecisionLabel(option.id) },
1290
+ type: option.id === "accept" || option.id === "acceptForSession" ? "primary" : option.id === "cancel" ? "danger" : "default",
1291
+ behaviors: [{
1292
+ type: "callback",
1293
+ value: {
1294
+ action: "approval",
1295
+ sessionId: state.sessionId,
1296
+ turnId: state.turnId,
1297
+ requestId: request.id,
1298
+ decision: option.id,
1299
+ },
1300
+ }],
1301
+ }],
1302
+ })),
1303
+ });
1304
+ }
1305
+ if (state.error) elements.push(markdown(codeBlock(state.error, 2_000)));
1306
+ const showAssistantText = state.assistantText && assistantTextMode === "always";
1307
+ if (showAssistantText) {
1308
+ if (elements.length > 0) elements.push({ tag: "hr" });
1309
+ const heading = state.status === "completed" ? "回答" : "回答生成中";
1310
+ elements.push(markdown(`**${heading}**\n${truncateText(state.assistantText, 3_000)}`));
1311
+ }
1312
+ if (elements.length === 0) elements.push(markdown(emptyTurnText(state.status, state.agentLabel)));
1313
+ return elements;
1314
+ }
1315
+
1127
1316
  function emptyTurnText(status: TurnViewStatus, agentLabel?: string): string {
1128
1317
  const label = agentLabel?.trim().replace(/[\r\n]+/g, " ") || "Agent";
1129
1318
  if (status === "starting") return `正在连接 ${label}…`;
@@ -1216,6 +1405,136 @@ function renderActivities(
1216
1405
  return elements;
1217
1406
  }
1218
1407
 
1408
+ type GroupedTurnActivity =
1409
+ | { kind: "activity"; activity: TurnActivity }
1410
+ | {
1411
+ kind: "execution";
1412
+ id: string;
1413
+ latestReasoning?: Extract<TurnActivity, { kind: "reasoning" }>;
1414
+ reasonings: Array<Extract<TurnActivity, { kind: "reasoning" }>>;
1415
+ tools: ToolState[];
1416
+ };
1417
+
1418
+ function renderGroupedActivityGroups(
1419
+ groups: GroupedTurnActivity[],
1420
+ projectCwd?: string,
1421
+ options: {
1422
+ fullActivityText?: boolean;
1423
+ includeReasoningHistory?: boolean;
1424
+ } = {},
1425
+ ): Record<string, unknown>[] {
1426
+ return groups.flatMap((group) => {
1427
+ if (group.kind === "activity") {
1428
+ return renderActivity(group.activity, projectCwd, options.fullActivityText === true);
1429
+ }
1430
+ if (group.tools.length === 0) {
1431
+ const reasonings = options.includeReasoningHistory
1432
+ ? group.reasonings
1433
+ : group.latestReasoning ? [group.latestReasoning] : [];
1434
+ return renderReasoningGroup(reasonings);
1435
+ }
1436
+ return [executionActivityPanel(group, projectCwd, options.includeReasoningHistory === true)];
1437
+ });
1438
+ }
1439
+
1440
+ function groupTurnActivities(activities: TurnActivity[]): GroupedTurnActivity[] {
1441
+ const groups: GroupedTurnActivity[] = [];
1442
+ let segment: TurnActivity[] = [];
1443
+ const flushSegment = (): void => {
1444
+ if (segment.length === 0) return;
1445
+ let execution: Extract<GroupedTurnActivity, { kind: "execution" }> | undefined;
1446
+ let executionPosition = -1;
1447
+ for (let index = 0; index < segment.length; index += 1) {
1448
+ const activity = segment[index]!;
1449
+ if (isRawReasoning(activity)) {
1450
+ execution ??= { kind: "execution", id: activity.id, reasonings: [], tools: [] };
1451
+ execution.reasonings.push(activity);
1452
+ execution.latestReasoning = activity;
1453
+ executionPosition = index;
1454
+ } else if (activity.kind === "tool") {
1455
+ execution ??= { kind: "execution", id: activity.id, reasonings: [], tools: [] };
1456
+ execution.tools.push(activity.tool);
1457
+ executionPosition = index;
1458
+ }
1459
+ }
1460
+ for (let index = 0; index < segment.length; index += 1) {
1461
+ const activity = segment[index]!;
1462
+ if (index === executionPosition && execution) groups.push(execution);
1463
+ if (!isRawReasoning(activity) && activity.kind !== "tool") {
1464
+ groups.push({ kind: "activity", activity });
1465
+ }
1466
+ }
1467
+ segment = [];
1468
+ };
1469
+
1470
+ for (const activity of activities) {
1471
+ if (!isCommentaryActivity(activity)) {
1472
+ segment.push(activity);
1473
+ continue;
1474
+ }
1475
+ flushSegment();
1476
+ groups.push({ kind: "activity", activity });
1477
+ }
1478
+ flushSegment();
1479
+ return groups;
1480
+ }
1481
+
1482
+ function isCommentaryActivity(activity: TurnActivity): boolean {
1483
+ return activity.kind === "assistant"
1484
+ || (activity.kind === "reasoning" && activity.id.startsWith("commentary:"));
1485
+ }
1486
+
1487
+ function executionActivityPanel(
1488
+ group: Extract<GroupedTurnActivity, { kind: "execution" }>,
1489
+ projectCwd: string | undefined,
1490
+ includeReasoningHistory = false,
1491
+ ): Record<string, unknown> {
1492
+ const status = executionActivityStatus(group.tools);
1493
+ const elements = [
1494
+ ...(includeReasoningHistory ? renderReasoningGroup(group.reasonings) : []),
1495
+ ...group.tools.map((tool) => toolPanel(tool, projectCwd)),
1496
+ ];
1497
+ return collapsiblePanel(
1498
+ executionActivityTitle(group, status),
1499
+ elements,
1500
+ {
1501
+ elementId: executionActivityPanelElementId(group.id),
1502
+ expanded: false,
1503
+ borderColor: status === "failed" ? "red" : status === "running" ? "blue" : "grey",
1504
+ compact: true,
1505
+ },
1506
+ );
1507
+ }
1508
+
1509
+ function executionActivityStatus(tools: ToolState[]): ToolState["status"] {
1510
+ if (tools.some((tool) => tool.status === "running")) return "running";
1511
+ if (tools.some((tool) => tool.status === "failed")) return "failed";
1512
+ return "completed";
1513
+ }
1514
+
1515
+ function executionActivityTitle(
1516
+ group: Extract<GroupedTurnActivity, { kind: "execution" }>,
1517
+ status: ToolState["status"],
1518
+ ): string {
1519
+ const icon = status === "running" ? "⏳" : status === "failed" ? "❌" : "💭";
1520
+ const latestReasoning = group.latestReasoning?.text
1521
+ ? removeMarkdownBold(group.latestReasoning.text).replace(/\s+/g, " ").trim()
1522
+ : "";
1523
+ const summary = latestReasoning
1524
+ ? truncateText(latestReasoning, 90)
1525
+ : status === "running"
1526
+ ? "正在执行工具"
1527
+ : status === "failed"
1528
+ ? "工具执行失败"
1529
+ : "已执行工具";
1530
+ return `${icon} ${summary} · ${group.tools.length} 个工具`;
1531
+ }
1532
+
1533
+ function executionActivityPanelElementId(activityId: string): string {
1534
+ const digest = createHash("sha256").update(activityId).digest("hex").slice(0, 10);
1535
+ return `turn_exec_${digest}`;
1536
+ }
1537
+
1219
1538
  function renderReasoningGroup(
1220
1539
  activities: Array<Extract<TurnActivity, { kind: "reasoning" }>>,
1221
1540
  ): Record<string, unknown>[] {
@@ -1237,6 +1556,9 @@ function isRawReasoning(
1237
1556
  }
1238
1557
 
1239
1558
  const ACTIVITIES_PER_PAGE = 40;
1559
+ const GROUPED_TOOLS_PER_PANEL = 8;
1560
+ const GROUPED_PAGE_ACTIVITY_BYTES = 24 * 1024;
1561
+ const GROUPED_PAGE_ACTIVITY_COMPONENTS = 160;
1240
1562
  const MAX_LIVE_ASSISTANT_TEXT = 2_000;
1241
1563
  const ACTIVITY_TEXT_CHUNK = 2_500;
1242
1564
 
@@ -1252,6 +1574,72 @@ function activityPages(activities: TurnActivity[]): TurnActivity[][] {
1252
1574
  return pages;
1253
1575
  }
1254
1576
 
1577
+ function groupedActivityPages(
1578
+ activities: TurnActivity[],
1579
+ projectCwd?: string,
1580
+ renderOptions: {
1581
+ fullActivityText?: boolean;
1582
+ includeReasoningHistory?: boolean;
1583
+ } = {},
1584
+ ): GroupedTurnActivity[][] {
1585
+ const groups = groupTurnActivities(activities).flatMap(splitGroupedExecutionActivity);
1586
+ if (groups.length === 0) return [];
1587
+
1588
+ const newestFirst: GroupedTurnActivity[][] = [];
1589
+ let page: GroupedTurnActivity[] = [];
1590
+ for (let index = groups.length - 1; index >= 0; index -= 1) {
1591
+ const group = groups[index]!;
1592
+ const candidate = [group, ...page];
1593
+ if (page.length > 0 && !groupedActivityPageFits(candidate, projectCwd, renderOptions)) {
1594
+ newestFirst.push(page);
1595
+ page = [];
1596
+ }
1597
+ page.unshift(group);
1598
+ }
1599
+ if (page.length > 0) newestFirst.push(page);
1600
+ return newestFirst.reverse();
1601
+ }
1602
+
1603
+ function groupedActivityPageFits(
1604
+ groups: GroupedTurnActivity[],
1605
+ projectCwd: string | undefined,
1606
+ renderOptions: {
1607
+ fullActivityText?: boolean;
1608
+ includeReasoningHistory?: boolean;
1609
+ },
1610
+ ): boolean {
1611
+ const elements = renderGroupedActivityGroups(groups, projectCwd, renderOptions);
1612
+ return Buffer.byteLength(JSON.stringify(elements), "utf8") <= GROUPED_PAGE_ACTIVITY_BYTES
1613
+ && countCardComponents(elements) <= GROUPED_PAGE_ACTIVITY_COMPONENTS;
1614
+ }
1615
+
1616
+ function countCardComponents(value: unknown): number {
1617
+ if (Array.isArray(value)) {
1618
+ return value.reduce((total, entry) => total + countCardComponents(entry), 0);
1619
+ }
1620
+ if (!value || typeof value !== "object") return 0;
1621
+ const record = value as Record<string, unknown>;
1622
+ return (typeof record.tag === "string" ? 1 : 0)
1623
+ + Object.values(record).reduce<number>((total, entry) => total + countCardComponents(entry), 0);
1624
+ }
1625
+
1626
+ function splitGroupedExecutionActivity(group: GroupedTurnActivity): GroupedTurnActivity[] {
1627
+ if (group.kind !== "execution" || group.tools.length <= GROUPED_TOOLS_PER_PANEL) return [group];
1628
+ const chunks: Array<Extract<GroupedTurnActivity, { kind: "execution" }>> = [];
1629
+ for (let index = 0; index < group.tools.length; index += GROUPED_TOOLS_PER_PANEL) {
1630
+ const tools = group.tools.slice(index, index + GROUPED_TOOLS_PER_PANEL);
1631
+ const isLast = index + GROUPED_TOOLS_PER_PANEL >= group.tools.length;
1632
+ chunks.push({
1633
+ kind: "execution",
1634
+ id: index === 0 ? group.id : tools[0]?.id ?? `${group.id}:${index}`,
1635
+ ...(isLast && group.latestReasoning ? { latestReasoning: group.latestReasoning } : {}),
1636
+ reasonings: isLast ? group.reasonings : [],
1637
+ tools,
1638
+ });
1639
+ }
1640
+ return chunks;
1641
+ }
1642
+
1255
1643
  function splitText(value: string, maxLength: number): string[] {
1256
1644
  if (!value) return [];
1257
1645
  const chunks: string[] = [];
@@ -1302,20 +1690,26 @@ function toolPanelElementId(toolId: string): string {
1302
1690
  function collapsiblePanel(
1303
1691
  title: string,
1304
1692
  content: string | Record<string, unknown>[],
1305
- options: { expanded?: boolean; borderColor?: string; elementId?: string } = {},
1693
+ options: {
1694
+ expanded?: boolean;
1695
+ borderColor?: string;
1696
+ elementId?: string;
1697
+ compact?: boolean;
1698
+ } = {},
1306
1699
  ): Record<string, unknown> {
1700
+ const compact = options.compact === true;
1307
1701
  return {
1308
1702
  tag: "collapsible_panel",
1309
1703
  ...(options.elementId ? { element_id: options.elementId } : {}),
1310
1704
  direction: "vertical",
1311
- vertical_spacing: "4px",
1312
- padding: "8px",
1705
+ vertical_spacing: compact ? "2px" : "4px",
1706
+ padding: compact ? "4px 6px" : "8px",
1313
1707
  margin: "0px",
1314
1708
  expanded: options.expanded ?? false,
1315
1709
  header: {
1316
1710
  title: { tag: "plain_text", content: title },
1317
1711
  vertical_align: "center",
1318
- padding: "4px 8px 4px 8px",
1712
+ padding: compact ? "2px 4px 2px 4px" : "4px 8px 4px 8px",
1319
1713
  },
1320
1714
  border: {
1321
1715
  color: options.borderColor ?? "grey",
@@ -1325,6 +1719,68 @@ function collapsiblePanel(
1325
1719
  };
1326
1720
  }
1327
1721
 
1722
+ function sessionTaskPanel(entry: SessionTaskCardEntry): Record<string, unknown> {
1723
+ const elements: Record<string, unknown>[] = [markdown(entry.detailLines.join("\n"))];
1724
+ if (entry.actions?.length) elements.push(sessionActionOverflow(entry.actions));
1725
+ return collapsiblePanel(entry.summary, elements, {
1726
+ elementId: sessionTaskPanelElementId(entry.reference),
1727
+ borderColor: entry.current ? "green" : "grey",
1728
+ compact: true,
1729
+ });
1730
+ }
1731
+
1732
+ function sessionProjectRow(group: SessionTaskCardGroup): Record<string, unknown> {
1733
+ return {
1734
+ tag: "column_set",
1735
+ flex_mode: "none",
1736
+ horizontal_spacing: "8px",
1737
+ vertical_align: "center",
1738
+ margin: "6px 0px 0px 0px",
1739
+ columns: [
1740
+ {
1741
+ tag: "column",
1742
+ width: "weighted",
1743
+ weight: 1,
1744
+ vertical_align: "center",
1745
+ elements: [markdown(`**${escapeCardActionText(group.title)}**`)],
1746
+ },
1747
+ ...(group.actions ?? []).map((action) => ({
1748
+ tag: "column",
1749
+ width: "auto",
1750
+ vertical_align: "center",
1751
+ elements: [sessionActionButton(action)],
1752
+ })),
1753
+ ],
1754
+ };
1755
+ }
1756
+
1757
+ function sessionTaskPanelElementId(reference: string): string {
1758
+ return `session_task_${createHash("sha256").update(reference).digest("hex").slice(0, 16)}`;
1759
+ }
1760
+
1761
+ function sessionActionOverflow(actions: TaskListCardAction[]): Record<string, unknown> {
1762
+ return {
1763
+ tag: "overflow",
1764
+ options: actions.map((action) => ({
1765
+ text: { tag: "plain_text", content: action.text },
1766
+ value: JSON.stringify(action.value),
1767
+ })),
1768
+ };
1769
+ }
1770
+
1771
+ function sessionActionButton(action: TaskListCardAction): Record<string, unknown> {
1772
+ return {
1773
+ tag: "button",
1774
+ text: { tag: "plain_text", content: action.text },
1775
+ type: action.type === "danger" ? "danger" : "default",
1776
+ size: "tiny",
1777
+ behaviors: [{
1778
+ type: "callback",
1779
+ value: action.value,
1780
+ }],
1781
+ };
1782
+ }
1783
+
1328
1784
  function renderToolDetails(tool: ToolState, projectCwd?: string): string {
1329
1785
  const command = tool.command ?? tool.title;
1330
1786
  const fileSummary = tool.files?.length
@@ -1726,6 +2182,69 @@ function taskActionRow(actions: TaskListCardAction[]): Record<string, unknown> {
1726
2182
  };
1727
2183
  }
1728
2184
 
2185
+ function directoryBrowserEntryRow(entry: DirectoryBrowserCardEntry): Record<string, unknown> {
2186
+ const label = `${directoryBrowserEntryIcon(entry.kind)} ${entry.name}`;
2187
+ return {
2188
+ tag: "column_set",
2189
+ flex_mode: "none",
2190
+ horizontal_spacing: "12px",
2191
+ vertical_align: "center",
2192
+ margin: "0px",
2193
+ columns: [
2194
+ {
2195
+ tag: "column",
2196
+ width: "weighted",
2197
+ weight: 1,
2198
+ vertical_align: "center",
2199
+ elements: entry.openAction
2200
+ ? [taskActionElement({ ...entry.openAction, text: label })]
2201
+ : [markdown(escapeCardHtml(label))],
2202
+ },
2203
+ ...(entry.actions ?? []).map((action) => ({
2204
+ tag: "column",
2205
+ width: "auto",
2206
+ vertical_align: "center",
2207
+ elements: [taskActionElement(action)],
2208
+ })),
2209
+ ],
2210
+ };
2211
+ }
2212
+
2213
+ function directoryBrowserEmptyRow(): Record<string, unknown> {
2214
+ return directoryBrowserStaticRow("这个目录为空。");
2215
+ }
2216
+
2217
+ function directoryBrowserPlaceholderRow(): Record<string, unknown> {
2218
+ return directoryBrowserStaticRow("\u00a0");
2219
+ }
2220
+
2221
+ function directoryBrowserStaticRow(content: string): Record<string, unknown> {
2222
+ return {
2223
+ tag: "column_set",
2224
+ flex_mode: "none",
2225
+ horizontal_spacing: "12px",
2226
+ vertical_align: "center",
2227
+ margin: "0px",
2228
+ columns: [{
2229
+ tag: "column",
2230
+ width: "weighted",
2231
+ weight: 1,
2232
+ vertical_align: "center",
2233
+ elements: [markdown(content)],
2234
+ }],
2235
+ };
2236
+ }
2237
+
2238
+ function directoryBrowserEntryIcon(kind: DirectoryBrowserCardEntry["kind"]): string {
2239
+ switch (kind) {
2240
+ case "directory": return "📁";
2241
+ case "drive": return "💽";
2242
+ case "image": return "🖼️";
2243
+ case "binary": return "📦";
2244
+ case "file": return "📄";
2245
+ }
2246
+ }
2247
+
1729
2248
  function taskActionElement(action: TaskListCardAction): Record<string, unknown> {
1730
2249
  return {
1731
2250
  tag: "interactive_container",
@@ -1733,7 +2252,7 @@ function taskActionElement(action: TaskListCardAction): Record<string, unknown>
1733
2252
  padding: "0px",
1734
2253
  has_border: false,
1735
2254
  elements: [markdown(
1736
- `<font color='${action.type === "danger" ? "red" : "blue"}'>${escapeCardHtml(action.text)}</font>`,
2255
+ `<font color='${action.type === "danger" ? "red" : "blue"}'>${escapeCardActionText(action.text)}</font>`,
1737
2256
  )],
1738
2257
  behaviors: [{
1739
2258
  type: "callback",
@@ -1742,6 +2261,10 @@ function taskActionElement(action: TaskListCardAction): Record<string, unknown>
1742
2261
  };
1743
2262
  }
1744
2263
 
2264
+ function escapeCardActionText(value: string): string {
2265
+ return escapeCardHtml(value).replaceAll("\\", "&#92;");
2266
+ }
2267
+
1745
2268
  function helpCommandRow(command: HelpCardCommand): Record<string, unknown> {
1746
2269
  const details = [
1747
2270
  command.usage ? `**${command.usage}**` : undefined,
@@ -1812,6 +2335,7 @@ function sectionCard(
1812
2335
  title: string,
1813
2336
  elements: Record<string, unknown>[],
1814
2337
  template = "blue",
2338
+ verticalSpacing = "8px",
1815
2339
  ): Record<string, unknown> {
1816
2340
  return {
1817
2341
  schema: "2.0",
@@ -1829,7 +2353,7 @@ function sectionCard(
1829
2353
  },
1830
2354
  body: {
1831
2355
  direction: "vertical",
1832
- vertical_spacing: "8px",
2356
+ vertical_spacing: verticalSpacing,
1833
2357
  padding: "12px 12px 12px 12px",
1834
2358
  elements,
1835
2359
  },