@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
@@ -1,8 +1,13 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import path from "node:path";
3
- import { truncateMiddle, truncateText } from "../utils/markdown.js";
3
+ import { truncateText } from "../utils/markdown.js";
4
4
  import { localCardImage } from "./LocalCardImage.js";
5
+ const DIRECTORY_BROWSER_ROW_COUNT = 16;
5
6
  export class CardRenderer {
7
+ thinkingCardLayout;
8
+ constructor(options = {}) {
9
+ this.thinkingCardLayout = options.thinkingCardLayout ?? "grouped";
10
+ }
6
11
  renderExecutionSettings(view) {
7
12
  const baseAction = {
8
13
  contextKey: view.contextKey,
@@ -596,7 +601,9 @@ export class CardRenderer {
596
601
  };
597
602
  }
598
603
  renderTurn(state) {
599
- const elements = renderTurnElements(state, "hidden");
604
+ const elements = this.thinkingCardLayout === "timeline"
605
+ ? renderTurnElements(state, "hidden")
606
+ : renderGroupedTurnElements(state, "hidden");
600
607
  if (isTurnStoppable(state.status)) {
601
608
  elements.push({ tag: "hr" }, taskActionRow([{
602
609
  text: "Stop",
@@ -622,10 +629,23 @@ export class CardRenderer {
622
629
  return turnCard(title, "blue", renderTurnElements(state, "always"), renderTurnSubtitle(state));
623
630
  }
624
631
  renderActivityHistory(state, requestedPage) {
625
- const pages = activityPages(turnActivities(state));
632
+ const grouped = this.thinkingCardLayout === "grouped";
633
+ const activities = turnActivities(state);
634
+ const allGroups = grouped ? groupTurnActivities(activities).flatMap(splitGroupedExecutionActivity) : [];
635
+ const historyGroups = grouped
636
+ ? groupedLiveActivityPage(groupedActivityPagesFromGroups(allGroups, state.projectCwd), state.projectCwd).historyGroups
637
+ : [];
638
+ const usesHistoricalPrefixPages = historyGroups.length > 0;
639
+ const pages = grouped
640
+ ? groupedActivityPagesFromGroups(usesHistoricalPrefixPages ? historyGroups : allGroups, state.projectCwd, {
641
+ fullActivityText: true,
642
+ includeReasoningHistory: true,
643
+ })
644
+ : activityPages(activities);
626
645
  if (pages.length === 0) {
627
646
  return this.renderSectionsCard("思考活动历史", [{ lines: ["没有保存到活动记录。"] }]);
628
647
  }
648
+ const totalPages = usesHistoricalPrefixPages ? pages.length + 1 : pages.length;
629
649
  const page = Math.max(0, Math.min(Math.trunc(requestedPage), pages.length - 1));
630
650
  const actions = [
631
651
  ...(pages.length > 1 ? [{
@@ -636,7 +656,7 @@ export class CardRenderer {
636
656
  text: "上一页",
637
657
  value: { action: "activity_history", turnId: state.turnId, page: String(page - 1) },
638
658
  }] : []),
639
- ...(page < pages.length - 2 ? [{
659
+ ...(page < pages.length - (usesHistoricalPrefixPages ? 1 : 2) ? [{
640
660
  text: "下一页",
641
661
  value: { action: "activity_history", turnId: state.turnId, page: String(page + 1) },
642
662
  }] : []),
@@ -644,8 +664,13 @@ export class CardRenderer {
644
664
  const elements = [];
645
665
  if (actions.length > 0)
646
666
  elements.push(taskActionRow(actions), { tag: "hr" });
647
- elements.push(...renderActivities(pages[page] ?? [], state.projectCwd, true));
648
- return sectionCard(`思考活动历史 · ${page + 1}/${pages.length}`, elements.length > 0 ? elements : [markdown("无")]);
667
+ elements.push(...(grouped
668
+ ? renderGroupedActivityGroups(pages[page] ?? [], state.projectCwd, {
669
+ fullActivityText: true,
670
+ includeReasoningHistory: true,
671
+ })
672
+ : renderActivities(pages[page] ?? [], state.projectCwd, true)));
673
+ return sectionCard(`思考活动历史 · ${page + 1}/${totalPages}`, elements.length > 0 ? elements : [markdown("无")]);
649
674
  }
650
675
  renderSessionStarted(session) {
651
676
  return this.baseCard("ACP 会话已创建", "green", [
@@ -748,6 +773,62 @@ export class CardRenderer {
748
773
  },
749
774
  };
750
775
  }
776
+ renderSessionTaskListCard(title, sectionTitle, groups, footerLines, footerActions = []) {
777
+ const elements = [markdown(`**${sectionTitle}**`)];
778
+ if (groups.length === 0) {
779
+ elements.push(markdown("无"));
780
+ }
781
+ else {
782
+ groups.forEach((group) => {
783
+ elements.push(sessionProjectRow(group));
784
+ elements.push(...group.entries.map(sessionTaskPanel));
785
+ });
786
+ }
787
+ if (footerActions.length > 0) {
788
+ elements.push(taskActionRow(footerActions));
789
+ }
790
+ if (footerLines.length > 0) {
791
+ elements.push(markdown(footerLines.join("\n")));
792
+ }
793
+ return {
794
+ schema: "2.0",
795
+ config: {
796
+ update_multi: true,
797
+ width_mode: "fill",
798
+ },
799
+ header: {
800
+ template: "blue",
801
+ title: {
802
+ tag: "plain_text",
803
+ content: title,
804
+ },
805
+ },
806
+ body: {
807
+ vertical_spacing: "4px",
808
+ elements,
809
+ },
810
+ };
811
+ }
812
+ renderDirectoryBrowserCard(view) {
813
+ const elements = [
814
+ markdown(`**当前目录**:${inlineCode(view.directory)}`),
815
+ taskActionRow(view.currentActions),
816
+ { tag: "hr" },
817
+ ];
818
+ const entryRows = view.entries.length === 0
819
+ ? [directoryBrowserEmptyRow()]
820
+ : view.entries.slice(0, DIRECTORY_BROWSER_ROW_COUNT).map(directoryBrowserEntryRow);
821
+ while (entryRows.length < DIRECTORY_BROWSER_ROW_COUNT)
822
+ entryRows.push(directoryBrowserPlaceholderRow());
823
+ elements.push(...entryRows);
824
+ if (view.navigationActions.length > 0) {
825
+ elements.push({ tag: "hr" }, taskActionRow(view.navigationActions));
826
+ }
827
+ if (view.footerLines.length > 0) {
828
+ elements.push({ tag: "hr" }, markdown(view.footerLines.join("\n")));
829
+ }
830
+ return sectionCard("文件浏览", elements, "blue", "2px");
831
+ }
751
832
  renderResetHistoryCard(view) {
752
833
  const elements = [
753
834
  {
@@ -875,6 +956,78 @@ function renderTurnElements(state, assistantTextMode) {
875
956
  elements.push(markdown(emptyTurnText(state.status, state.agentLabel)));
876
957
  return elements;
877
958
  }
959
+ function renderGroupedTurnElements(state, assistantTextMode) {
960
+ const elements = [];
961
+ const allActivities = turnActivities(state);
962
+ const livePages = groupedActivityPages(allActivities, state.projectCwd);
963
+ const visible = groupedLiveActivityPage(livePages, state.projectCwd);
964
+ const historyPages = groupedActivityPagesFromGroups(visible.historyGroups, state.projectCwd, {
965
+ fullActivityText: true,
966
+ includeReasoningHistory: true,
967
+ });
968
+ if (state.plan.length > 0)
969
+ elements.push(planPanel(state.plan));
970
+ if (historyPages.length > 0) {
971
+ elements.push(taskActionRow([{
972
+ text: `查看历史思考(共 ${historyPages.length + 1} 页)`,
973
+ value: {
974
+ action: "activity_history",
975
+ turnId: state.turnId,
976
+ page: String(historyPages.length - 1),
977
+ },
978
+ }]));
979
+ }
980
+ if (state.activitiesTruncated && visible.groups[0]?.kind !== "gap")
981
+ elements.push(markdown("…"));
982
+ elements.push(...renderGroupedActivityGroups(visible.groups, state.projectCwd));
983
+ if (state.fileSummary.length > 0)
984
+ elements.push(fileSummaryPanel(state));
985
+ if (state.approval) {
986
+ const request = state.approval;
987
+ elements.push(markdown([
988
+ `**${request.title}**`,
989
+ request.command ? codeBlock(request.command, 800) : undefined,
990
+ request.reason,
991
+ ].filter(Boolean).join("\n")));
992
+ elements.push({
993
+ tag: "column_set",
994
+ flex_mode: "flow",
995
+ horizontal_spacing: "8px",
996
+ vertical_spacing: "8px",
997
+ columns: request.options.map((option) => ({
998
+ tag: "column",
999
+ width: "auto",
1000
+ elements: [{
1001
+ tag: "button",
1002
+ text: { tag: "plain_text", content: approvalDecisionLabel(option.id) },
1003
+ type: option.id === "accept" || option.id === "acceptForSession" ? "primary" : option.id === "cancel" ? "danger" : "default",
1004
+ behaviors: [{
1005
+ type: "callback",
1006
+ value: {
1007
+ action: "approval",
1008
+ sessionId: state.sessionId,
1009
+ turnId: state.turnId,
1010
+ requestId: request.id,
1011
+ decision: option.id,
1012
+ },
1013
+ }],
1014
+ }],
1015
+ })),
1016
+ });
1017
+ }
1018
+ if (state.error)
1019
+ elements.push(markdown(codeBlock(state.error, 2_000)));
1020
+ const showAssistantText = state.assistantText && assistantTextMode === "always";
1021
+ if (showAssistantText) {
1022
+ if (elements.length > 0)
1023
+ elements.push({ tag: "hr" });
1024
+ const heading = state.status === "completed" ? "回答" : "回答生成中";
1025
+ elements.push(markdown(`**${heading}**\n${truncateText(state.assistantText, 3_000)}`));
1026
+ }
1027
+ if (elements.length === 0)
1028
+ elements.push(markdown(emptyTurnText(state.status, state.agentLabel)));
1029
+ return elements;
1030
+ }
878
1031
  function emptyTurnText(status, agentLabel) {
879
1032
  const label = agentLabel?.trim().replace(/[\r\n]+/g, " ") || "Agent";
880
1033
  if (status === "starting")
@@ -953,6 +1106,103 @@ function renderActivities(activities, projectCwd, fullAssistantText = false) {
953
1106
  flushReasoning();
954
1107
  return elements;
955
1108
  }
1109
+ function renderGroupedActivityGroups(groups, projectCwd, options = {}) {
1110
+ return groups.flatMap((group) => {
1111
+ if (group.kind === "gap")
1112
+ return [markdown("…")];
1113
+ if (group.kind === "activity") {
1114
+ return renderActivity(group.activity, projectCwd, options.fullActivityText === true);
1115
+ }
1116
+ if (group.tools.length === 0) {
1117
+ const reasonings = options.includeReasoningHistory
1118
+ ? group.reasonings
1119
+ : group.latestReasoning ? [group.latestReasoning] : [];
1120
+ return renderReasoningGroup(reasonings);
1121
+ }
1122
+ return [executionActivityPanel(group, projectCwd, options.includeReasoningHistory === true)];
1123
+ });
1124
+ }
1125
+ function groupTurnActivities(activities) {
1126
+ const groups = [];
1127
+ let segment = [];
1128
+ const flushSegment = () => {
1129
+ if (segment.length === 0)
1130
+ return;
1131
+ let execution;
1132
+ let executionPosition = -1;
1133
+ for (let index = 0; index < segment.length; index += 1) {
1134
+ const activity = segment[index];
1135
+ if (isRawReasoning(activity)) {
1136
+ execution ??= { kind: "execution", id: activity.id, reasonings: [], tools: [] };
1137
+ execution.reasonings.push(activity);
1138
+ execution.latestReasoning = activity;
1139
+ executionPosition = index;
1140
+ }
1141
+ else if (activity.kind === "tool") {
1142
+ execution ??= { kind: "execution", id: activity.id, reasonings: [], tools: [] };
1143
+ execution.tools.push(activity.tool);
1144
+ executionPosition = index;
1145
+ }
1146
+ }
1147
+ for (let index = 0; index < segment.length; index += 1) {
1148
+ const activity = segment[index];
1149
+ if (index === executionPosition && execution)
1150
+ groups.push(execution);
1151
+ if (!isRawReasoning(activity) && activity.kind !== "tool") {
1152
+ groups.push({ kind: "activity", activity });
1153
+ }
1154
+ }
1155
+ segment = [];
1156
+ };
1157
+ for (const activity of activities) {
1158
+ if (!isCommentaryActivity(activity)) {
1159
+ segment.push(activity);
1160
+ continue;
1161
+ }
1162
+ flushSegment();
1163
+ groups.push({ kind: "activity", activity });
1164
+ }
1165
+ flushSegment();
1166
+ return groups;
1167
+ }
1168
+ function isCommentaryActivity(activity) {
1169
+ return activity.kind === "assistant"
1170
+ || (activity.kind === "reasoning" && activity.id.startsWith("commentary:"));
1171
+ }
1172
+ function executionActivityPanel(group, projectCwd, includeReasoningHistory = false) {
1173
+ const status = executionActivityStatus(group.tools);
1174
+ const elements = [
1175
+ ...(includeReasoningHistory ? renderReasoningGroup(group.reasonings) : []),
1176
+ ...group.tools.map((tool) => toolPanel(tool, projectCwd)),
1177
+ ];
1178
+ return collapsiblePanel(executionActivityTitle(group, status), elements, {
1179
+ elementId: executionActivityPanelElementId(group.id),
1180
+ expanded: false,
1181
+ borderColor: status === "running" ? "blue" : "grey",
1182
+ compact: true,
1183
+ });
1184
+ }
1185
+ function executionActivityStatus(tools) {
1186
+ if (tools.some((tool) => tool.status === "running"))
1187
+ return "running";
1188
+ return "completed";
1189
+ }
1190
+ function executionActivityTitle(group, status) {
1191
+ const icon = status === "running" ? "⏳" : "💭";
1192
+ const latestReasoning = group.latestReasoning?.text
1193
+ ? removeMarkdownBold(group.latestReasoning.text).replace(/\s+/g, " ").trim()
1194
+ : "";
1195
+ const summary = latestReasoning
1196
+ ? truncateText(latestReasoning, 90)
1197
+ : status === "running"
1198
+ ? "正在执行工具"
1199
+ : "已执行工具";
1200
+ return `${icon} ${summary} · ${group.tools.length} 个工具`;
1201
+ }
1202
+ function executionActivityPanelElementId(activityId) {
1203
+ const digest = createHash("sha256").update(activityId).digest("hex").slice(0, 10);
1204
+ return `turn_exec_${digest}`;
1205
+ }
956
1206
  function renderReasoningGroup(activities) {
957
1207
  const sections = activities
958
1208
  .map((activity) => activity.text.trim())
@@ -969,6 +1219,10 @@ function isRawReasoning(activity) {
969
1219
  return activity.kind === "reasoning" && !activity.id.startsWith("commentary:");
970
1220
  }
971
1221
  const ACTIVITIES_PER_PAGE = 40;
1222
+ const GROUPED_TOOLS_PER_PANEL = 8;
1223
+ const GROUPED_PAGE_ACTIVITY_BYTES = 24 * 1024;
1224
+ const GROUPED_PAGE_ACTIVITY_COMPONENTS = 160;
1225
+ const PINNED_LIVE_COMMENTARIES = 3;
972
1226
  const MAX_LIVE_ASSISTANT_TEXT = 2_000;
973
1227
  const ACTIVITY_TEXT_CHUNK = 2_500;
974
1228
  function activityPages(activities) {
@@ -984,6 +1238,123 @@ function activityPages(activities) {
984
1238
  }
985
1239
  return pages;
986
1240
  }
1241
+ function groupedActivityPages(activities, projectCwd, renderOptions = {}) {
1242
+ const groups = groupTurnActivities(activities).flatMap(splitGroupedExecutionActivity);
1243
+ return groupedActivityPagesFromGroups(groups, projectCwd, renderOptions);
1244
+ }
1245
+ function groupedActivityPagesFromGroups(groups, projectCwd, renderOptions = {}) {
1246
+ if (groups.length === 0)
1247
+ return [];
1248
+ const newestFirst = [];
1249
+ let page = [];
1250
+ for (let index = groups.length - 1; index >= 0; index -= 1) {
1251
+ const group = groups[index];
1252
+ const candidate = [group, ...page];
1253
+ if (page.length > 0 && !groupedActivityPageFits(candidate, projectCwd, renderOptions)) {
1254
+ newestFirst.push(page);
1255
+ page = [];
1256
+ }
1257
+ page.unshift(group);
1258
+ }
1259
+ if (page.length > 0)
1260
+ newestFirst.push(page);
1261
+ return newestFirst.reverse();
1262
+ }
1263
+ function groupedActivityPageFits(groups, projectCwd, renderOptions) {
1264
+ const elements = renderGroupedActivityGroups(groups, projectCwd, renderOptions);
1265
+ return Buffer.byteLength(JSON.stringify(elements), "utf8") <= GROUPED_PAGE_ACTIVITY_BYTES
1266
+ && countCardComponents(elements) <= GROUPED_PAGE_ACTIVITY_COMPONENTS;
1267
+ }
1268
+ function groupedLiveActivityPage(pages, projectCwd) {
1269
+ const latest = pages.at(-1) ?? [];
1270
+ if (pages.length <= 1)
1271
+ return { groups: latest, historyGroups: [] };
1272
+ const groups = pages.flat();
1273
+ const latestStart = groups.length - latest.length;
1274
+ const commentaryIndexes = groups
1275
+ .map((group, index) => isCommentaryGroup(group) ? index : -1)
1276
+ .filter((index) => index >= 0);
1277
+ if (commentaryIndexes.length === 0) {
1278
+ return { groups: latest, historyGroups: groups.slice(0, latestStart) };
1279
+ }
1280
+ const selected = new Set();
1281
+ const pinnedCommentaries = commentaryIndexes.slice(0, PINNED_LIVE_COMMENTARIES);
1282
+ for (const index of pinnedCommentaries)
1283
+ selected.add(index);
1284
+ const latestCommentary = commentaryIndexes.at(-1);
1285
+ for (let index = latestCommentary; index < groups.length; index += 1)
1286
+ selected.add(index);
1287
+ const renderSelection = (selection) => ({
1288
+ groups: groupsWithGaps(groups, selection),
1289
+ historyGroups: groups.filter((_group, index) => index < latestCommentary || !selection.has(index)),
1290
+ });
1291
+ const fits = (selection) => {
1292
+ const view = renderSelection(selection);
1293
+ return groupedActivityPageFits(view.groups, projectCwd, {});
1294
+ };
1295
+ if (!fits(selected)) {
1296
+ const latestExecutions = [...selected]
1297
+ .filter((index) => index > latestCommentary && groups[index]?.kind === "execution")
1298
+ .sort((left, right) => left - right);
1299
+ const newestExecution = latestExecutions.at(-1);
1300
+ for (const index of latestExecutions) {
1301
+ if (fits(selected) || index === newestExecution)
1302
+ break;
1303
+ selected.delete(index);
1304
+ }
1305
+ }
1306
+ return renderSelection(selected);
1307
+ }
1308
+ function isCommentaryGroup(group) {
1309
+ return group.kind === "activity" && isCommentaryActivity(group.activity);
1310
+ }
1311
+ function groupsWithGaps(groups, selected) {
1312
+ const indexes = [...selected].sort((left, right) => left - right);
1313
+ const visible = [];
1314
+ const append = (group) => {
1315
+ if (group.kind === "gap" && visible.at(-1)?.kind === "gap")
1316
+ return;
1317
+ visible.push(group);
1318
+ };
1319
+ let previous = -1;
1320
+ for (const index of indexes) {
1321
+ if (previous >= 0 && index > previous + 1) {
1322
+ append({ kind: "gap", id: `gap:${previous}:${index}` });
1323
+ }
1324
+ const group = groups[index];
1325
+ if (group)
1326
+ append(group);
1327
+ previous = index;
1328
+ }
1329
+ return visible;
1330
+ }
1331
+ function countCardComponents(value) {
1332
+ if (Array.isArray(value)) {
1333
+ return value.reduce((total, entry) => total + countCardComponents(entry), 0);
1334
+ }
1335
+ if (!value || typeof value !== "object")
1336
+ return 0;
1337
+ const record = value;
1338
+ return (typeof record.tag === "string" ? 1 : 0)
1339
+ + Object.values(record).reduce((total, entry) => total + countCardComponents(entry), 0);
1340
+ }
1341
+ function splitGroupedExecutionActivity(group) {
1342
+ if (group.kind !== "execution" || group.tools.length <= GROUPED_TOOLS_PER_PANEL)
1343
+ return [group];
1344
+ const chunks = [];
1345
+ for (let index = 0; index < group.tools.length; index += GROUPED_TOOLS_PER_PANEL) {
1346
+ const tools = group.tools.slice(index, index + GROUPED_TOOLS_PER_PANEL);
1347
+ const isLast = index + GROUPED_TOOLS_PER_PANEL >= group.tools.length;
1348
+ chunks.push({
1349
+ kind: "execution",
1350
+ id: index === 0 ? group.id : tools[0]?.id ?? `${group.id}:${index}`,
1351
+ ...(isLast && group.latestReasoning ? { latestReasoning: group.latestReasoning } : {}),
1352
+ reasonings: isLast ? group.reasonings : [],
1353
+ tools,
1354
+ });
1355
+ }
1356
+ return chunks;
1357
+ }
987
1358
  function splitText(value, maxLength) {
988
1359
  if (!value)
989
1360
  return [];
@@ -1027,18 +1398,19 @@ function toolPanelElementId(toolId) {
1027
1398
  return `turn_tool_${digest}`;
1028
1399
  }
1029
1400
  function collapsiblePanel(title, content, options = {}) {
1401
+ const compact = options.compact === true;
1030
1402
  return {
1031
1403
  tag: "collapsible_panel",
1032
1404
  ...(options.elementId ? { element_id: options.elementId } : {}),
1033
1405
  direction: "vertical",
1034
- vertical_spacing: "4px",
1035
- padding: "8px",
1406
+ vertical_spacing: compact ? "2px" : "4px",
1407
+ padding: compact ? "4px 6px" : "8px",
1036
1408
  margin: "0px",
1037
1409
  expanded: options.expanded ?? false,
1038
1410
  header: {
1039
1411
  title: { tag: "plain_text", content: title },
1040
1412
  vertical_align: "center",
1041
- padding: "4px 8px 4px 8px",
1413
+ padding: compact ? "2px 4px 2px 4px" : "4px 8px 4px 8px",
1042
1414
  },
1043
1415
  border: {
1044
1416
  color: options.borderColor ?? "grey",
@@ -1047,6 +1419,64 @@ function collapsiblePanel(title, content, options = {}) {
1047
1419
  elements: typeof content === "string" ? [markdown(content)] : content,
1048
1420
  };
1049
1421
  }
1422
+ function sessionTaskPanel(entry) {
1423
+ const elements = [markdown(entry.detailLines.join("\n"))];
1424
+ if (entry.actions?.length)
1425
+ elements.push(sessionActionOverflow(entry.actions));
1426
+ return collapsiblePanel(entry.summary, elements, {
1427
+ elementId: sessionTaskPanelElementId(entry.reference),
1428
+ borderColor: entry.current ? "green" : "grey",
1429
+ compact: true,
1430
+ });
1431
+ }
1432
+ function sessionProjectRow(group) {
1433
+ return {
1434
+ tag: "column_set",
1435
+ flex_mode: "none",
1436
+ horizontal_spacing: "8px",
1437
+ vertical_align: "center",
1438
+ margin: "6px 0px 0px 0px",
1439
+ columns: [
1440
+ {
1441
+ tag: "column",
1442
+ width: "weighted",
1443
+ weight: 1,
1444
+ vertical_align: "center",
1445
+ elements: [markdown(`**${escapeCardActionText(group.title)}**`)],
1446
+ },
1447
+ ...(group.actions ?? []).map((action) => ({
1448
+ tag: "column",
1449
+ width: "auto",
1450
+ vertical_align: "center",
1451
+ elements: [sessionActionButton(action)],
1452
+ })),
1453
+ ],
1454
+ };
1455
+ }
1456
+ function sessionTaskPanelElementId(reference) {
1457
+ return `session_task_${createHash("sha256").update(reference).digest("hex").slice(0, 16)}`;
1458
+ }
1459
+ function sessionActionOverflow(actions) {
1460
+ return {
1461
+ tag: "overflow",
1462
+ options: actions.map((action) => ({
1463
+ text: { tag: "plain_text", content: action.text },
1464
+ value: JSON.stringify(action.value),
1465
+ })),
1466
+ };
1467
+ }
1468
+ function sessionActionButton(action) {
1469
+ return {
1470
+ tag: "button",
1471
+ text: { tag: "plain_text", content: action.text },
1472
+ type: action.type === "danger" ? "danger" : "default",
1473
+ size: "tiny",
1474
+ behaviors: [{
1475
+ type: "callback",
1476
+ value: action.value,
1477
+ }],
1478
+ };
1479
+ }
1050
1480
  function renderToolDetails(tool, projectCwd) {
1051
1481
  const command = tool.command ?? tool.title;
1052
1482
  const fileSummary = tool.files?.length
@@ -1057,12 +1487,21 @@ function renderToolDetails(tool, projectCwd) {
1057
1487
  const result = tool.error ?? tool.output ?? fileSummary;
1058
1488
  const normalizedCommand = stripAnsi(command).trim();
1059
1489
  const displayCommand = tool.kind === "command"
1060
- ? unwrapPowerShellCommand(normalizedCommand) ?? normalizedCommand
1490
+ ? unwrapShellCommand(normalizedCommand) ?? normalizedCommand
1061
1491
  : normalizedCommand;
1062
- const commandText = truncateText(displayCommand, 800);
1063
- const resultText = result ? truncateMiddle(stripAnsi(result).trim(), 1_200) : undefined;
1492
+ const commandText = truncateText(displayCommand, 600);
1493
+ const resultText = result ? truncateToolResult(stripAnsi(result).trim()) : undefined;
1064
1494
  return codeBlock([`$ ${commandText}`, resultText].filter((part) => part !== undefined).join("\n"), 2_003);
1065
1495
  }
1496
+ function truncateToolResult(result) {
1497
+ const maxLength = 900;
1498
+ const marker = "\n...\n";
1499
+ const tailLength = 600;
1500
+ if (result.length <= maxLength)
1501
+ return result;
1502
+ const headLength = maxLength - marker.length - tailLength;
1503
+ return `${result.slice(0, headLength)}${marker}${result.slice(-tailLength)}`;
1504
+ }
1066
1505
  function displayFilePath(filePath, projectCwd) {
1067
1506
  if (!projectCwd)
1068
1507
  return filePath;
@@ -1089,7 +1528,7 @@ function toolPanelTitle(tool) {
1089
1528
  const command = stripAnsi(tool.command ?? tool.title).trim();
1090
1529
  const meaningfulCommand = tool.kind === "web_search"
1091
1530
  ? tool.title
1092
- : unwrapPowerShellCommand(command) ?? tool.title;
1531
+ : unwrapShellCommand(command) ?? tool.title;
1093
1532
  const duration = toolDuration(tool);
1094
1533
  const prefix = `${icon} `;
1095
1534
  const suffix = duration ? ` · ${duration}` : "";
@@ -1136,6 +1575,9 @@ function formatTurnDuration(durationMs) {
1136
1575
  return `${String(totalMinutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
1137
1576
  return `${seconds}s`;
1138
1577
  }
1578
+ function unwrapShellCommand(command) {
1579
+ return unwrapPowerShellCommand(command) ?? unwrapPosixShellCommand(command);
1580
+ }
1139
1581
  function unwrapPowerShellCommand(command) {
1140
1582
  const executable = command.match(/^(?:"[^"]*(?:pwsh|powershell)\.exe"|(?:\S*[\\/])?(?:pwsh|powershell)(?:\.exe)?)(?=\s|$)/i);
1141
1583
  if (!executable)
@@ -1147,6 +1589,71 @@ function unwrapPowerShellCommand(command) {
1147
1589
  const payload = args.slice(commandFlag.index + commandFlag[0].length).trim();
1148
1590
  if (!payload)
1149
1591
  return undefined;
1592
+ return unwrapQuotedPayload(payload);
1593
+ }
1594
+ function unwrapPosixShellCommand(command) {
1595
+ let executable = readCommandWord(command, 0);
1596
+ if (!executable)
1597
+ return undefined;
1598
+ if (posixExecutableName(executable.value) === "env") {
1599
+ let cursor = executable.end;
1600
+ while (true) {
1601
+ const candidate = readCommandWord(command, cursor);
1602
+ if (!candidate)
1603
+ return undefined;
1604
+ const name = posixExecutableName(candidate.value);
1605
+ if (isPosixShell(name)) {
1606
+ executable = candidate;
1607
+ break;
1608
+ }
1609
+ if (!candidate.value.startsWith("-") && !/^[A-Za-z_][A-Za-z0-9_]*=/.test(candidate.value)) {
1610
+ return undefined;
1611
+ }
1612
+ cursor = candidate.end;
1613
+ }
1614
+ }
1615
+ else if (!isPosixShell(posixExecutableName(executable.value))) {
1616
+ return undefined;
1617
+ }
1618
+ const args = command.slice(executable.end);
1619
+ const commandFlag = /(?:^|\s)(?:-[A-Za-z]*c[A-Za-z]*|--command)(?:\s+|=)/.exec(args);
1620
+ if (!commandFlag)
1621
+ return undefined;
1622
+ const payload = args.slice(commandFlag.index + commandFlag[0].length).trim();
1623
+ if (!payload)
1624
+ return undefined;
1625
+ return unwrapQuotedPayload(payload);
1626
+ }
1627
+ function readCommandWord(command, start) {
1628
+ let cursor = start;
1629
+ while (cursor < command.length && /\s/.test(command[cursor] ?? ""))
1630
+ cursor += 1;
1631
+ if (cursor >= command.length)
1632
+ return undefined;
1633
+ const quote = command[cursor];
1634
+ if (quote === "\"" || quote === "'") {
1635
+ const endQuote = command.indexOf(quote, cursor + 1);
1636
+ if (endQuote < 0)
1637
+ return undefined;
1638
+ return { value: command.slice(cursor + 1, endQuote), end: endQuote + 1 };
1639
+ }
1640
+ const word = /^\S+/.exec(command.slice(cursor));
1641
+ if (!word)
1642
+ return undefined;
1643
+ return { value: word[0], end: cursor + word[0].length };
1644
+ }
1645
+ function posixExecutableName(executable) {
1646
+ return executable.replaceAll("\\", "/").split("/").at(-1)?.toLowerCase() ?? "";
1647
+ }
1648
+ function isPosixShell(executable) {
1649
+ return executable === "sh"
1650
+ || executable === "bash"
1651
+ || executable === "zsh"
1652
+ || executable === "dash"
1653
+ || executable === "ksh"
1654
+ || executable === "fish";
1655
+ }
1656
+ function unwrapQuotedPayload(payload) {
1150
1657
  const quote = payload[0];
1151
1658
  if ((quote === "\"" || quote === "'") && payload.at(-1) === quote)
1152
1659
  return payload.slice(1, -1).trim();
@@ -1412,19 +1919,80 @@ function taskActionRow(actions) {
1412
1919
  })),
1413
1920
  };
1414
1921
  }
1922
+ function directoryBrowserEntryRow(entry) {
1923
+ const label = `${directoryBrowserEntryIcon(entry.kind)} ${entry.name}`;
1924
+ return {
1925
+ tag: "column_set",
1926
+ flex_mode: "none",
1927
+ horizontal_spacing: "12px",
1928
+ vertical_align: "center",
1929
+ margin: "0px",
1930
+ columns: [
1931
+ {
1932
+ tag: "column",
1933
+ width: "weighted",
1934
+ weight: 1,
1935
+ vertical_align: "center",
1936
+ elements: entry.openAction
1937
+ ? [taskActionElement({ ...entry.openAction, text: label })]
1938
+ : [markdown(escapeCardHtml(label))],
1939
+ },
1940
+ ...(entry.actions ?? []).map((action) => ({
1941
+ tag: "column",
1942
+ width: "auto",
1943
+ vertical_align: "center",
1944
+ elements: [taskActionElement(action)],
1945
+ })),
1946
+ ],
1947
+ };
1948
+ }
1949
+ function directoryBrowserEmptyRow() {
1950
+ return directoryBrowserStaticRow("这个目录为空。");
1951
+ }
1952
+ function directoryBrowserPlaceholderRow() {
1953
+ return directoryBrowserStaticRow("\u00a0");
1954
+ }
1955
+ function directoryBrowserStaticRow(content) {
1956
+ return {
1957
+ tag: "column_set",
1958
+ flex_mode: "none",
1959
+ horizontal_spacing: "12px",
1960
+ vertical_align: "center",
1961
+ margin: "0px",
1962
+ columns: [{
1963
+ tag: "column",
1964
+ width: "weighted",
1965
+ weight: 1,
1966
+ vertical_align: "center",
1967
+ elements: [markdown(content)],
1968
+ }],
1969
+ };
1970
+ }
1971
+ function directoryBrowserEntryIcon(kind) {
1972
+ switch (kind) {
1973
+ case "directory": return "📁";
1974
+ case "drive": return "💽";
1975
+ case "image": return "🖼️";
1976
+ case "binary": return "📦";
1977
+ case "file": return "📄";
1978
+ }
1979
+ }
1415
1980
  function taskActionElement(action) {
1416
1981
  return {
1417
1982
  tag: "interactive_container",
1418
1983
  margin: "0px",
1419
1984
  padding: "0px",
1420
1985
  has_border: false,
1421
- elements: [markdown(`<font color='${action.type === "danger" ? "red" : "blue"}'>${escapeCardHtml(action.text)}</font>`)],
1986
+ elements: [markdown(`<font color='${action.type === "danger" ? "red" : "blue"}'>${escapeCardActionText(action.text)}</font>`)],
1422
1987
  behaviors: [{
1423
1988
  type: "callback",
1424
1989
  value: action.value,
1425
1990
  }],
1426
1991
  };
1427
1992
  }
1993
+ function escapeCardActionText(value) {
1994
+ return escapeCardHtml(value).replaceAll("\\", "&#92;");
1995
+ }
1428
1996
  function helpCommandRow(command) {
1429
1997
  const details = [
1430
1998
  command.usage ? `**${command.usage}**` : undefined,
@@ -1484,7 +2052,7 @@ function turnCard(title, template, elements, subtitle) {
1484
2052
  },
1485
2053
  };
1486
2054
  }
1487
- function sectionCard(title, elements, template = "blue") {
2055
+ function sectionCard(title, elements, template = "blue", verticalSpacing = "8px") {
1488
2056
  return {
1489
2057
  schema: "2.0",
1490
2058
  config: {
@@ -1501,7 +2069,7 @@ function sectionCard(title, elements, template = "blue") {
1501
2069
  },
1502
2070
  body: {
1503
2071
  direction: "vertical",
1504
- vertical_spacing: "8px",
2072
+ vertical_spacing: verticalSpacing,
1505
2073
  padding: "12px 12px 12px 12px",
1506
2074
  elements,
1507
2075
  },