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

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 (80) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/README.md +48 -15
  3. package/README.zh.md +48 -15
  4. package/config.example.yaml +1 -1
  5. package/dist/cli/AutostartManager.js +431 -0
  6. package/dist/cli/AutostartManager.js.map +1 -0
  7. package/dist/cli/FeishuAppConfiguration.js +21 -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 +94 -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 +661 -58
  27. package/dist/cli.js.map +1 -1
  28. package/dist/codex/CodexRuntime.js +8 -2
  29. package/dist/codex/CodexRuntime.js.map +1 -1
  30. package/dist/commands/CommandRouter.js +16 -1
  31. package/dist/commands/CommandRouter.js.map +1 -1
  32. package/dist/feishu/CardRenderer.js +266 -38
  33. package/dist/feishu/CardRenderer.js.map +1 -1
  34. package/dist/feishu/FeishuMarkdown.js +25 -1
  35. package/dist/feishu/FeishuMarkdown.js.map +1 -1
  36. package/dist/feishu/FeishuMessageClient.js +16 -0
  37. package/dist/feishu/FeishuMessageClient.js.map +1 -1
  38. package/dist/feishu/FeishuTurnPresenter.js +42 -6
  39. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  40. package/dist/index.js +136 -2
  41. package/dist/index.js.map +1 -1
  42. package/dist/presentation/OutboundRouter.js +7 -0
  43. package/dist/presentation/OutboundRouter.js.map +1 -1
  44. package/dist/presentation/TurnStateReducer.js +21 -7
  45. package/dist/presentation/TurnStateReducer.js.map +1 -1
  46. package/dist/proxy/ProxySessionController.js +537 -13
  47. package/dist/proxy/ProxySessionController.js.map +1 -1
  48. package/dist/state/StateStore.js +53 -0
  49. package/dist/state/StateStore.js.map +1 -1
  50. package/docs/technical-reference.md +17 -7
  51. package/docs/technical-reference.zh.md +17 -7
  52. package/npm-shrinkwrap.json +2 -2
  53. package/package.json +2 -2
  54. package/skills/agent-bot/SKILL.md +109 -188
  55. package/src/cli/AutostartManager.ts +579 -0
  56. package/src/cli/FeishuAppConfiguration.ts +26 -7
  57. package/src/cli/Initializer.ts +57 -0
  58. package/src/cli/SelfUpdateRunner.ts +817 -0
  59. package/src/cli/SelfUpdater.ts +688 -0
  60. package/src/cli/autostartOutput.ts +75 -0
  61. package/src/cli/controlProtocol.ts +119 -1
  62. package/src/cli/help.ts +94 -18
  63. package/src/cli/taskGroupOptions.ts +27 -17
  64. package/src/cli/taskListOutput.ts +10 -0
  65. package/src/cli/taskTarget.ts +168 -0
  66. package/src/cli.ts +809 -65
  67. package/src/codex/CodexRuntime.ts +8 -2
  68. package/src/commands/CommandRouter.ts +14 -1
  69. package/src/commands/commandTypes.ts +2 -0
  70. package/src/feishu/CardRenderer.ts +313 -35
  71. package/src/feishu/FeishuMarkdown.ts +27 -1
  72. package/src/feishu/FeishuMessageClient.ts +33 -0
  73. package/src/feishu/FeishuTurnPresenter.ts +43 -6
  74. package/src/feishu/types.ts +1 -0
  75. package/src/index.ts +174 -2
  76. package/src/presentation/OutboundRouter.ts +7 -0
  77. package/src/presentation/TurnStateReducer.ts +21 -7
  78. package/src/proxy/ProxySessionController.ts +661 -16
  79. package/src/runtime/types.ts +1 -0
  80. package/src/state/StateStore.ts +56 -0
@@ -1,6 +1,6 @@
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
5
  const DIRECTORY_BROWSER_ROW_COUNT = 16;
6
6
  export class CardRenderer {
@@ -604,18 +604,24 @@ export class CardRenderer {
604
604
  const elements = this.thinkingCardLayout === "timeline"
605
605
  ? renderTurnElements(state, "hidden")
606
606
  : renderGroupedTurnElements(state, "hidden");
607
+ const footerActions = [];
607
608
  if (isTurnStoppable(state.status)) {
608
- elements.push({ tag: "hr" }, taskActionRow([{
609
- text: "Stop",
610
- type: "danger",
611
- value: { action: "turn_cancel", sessionId: state.sessionId, turnId: state.turnId },
612
- }]));
609
+ footerActions.push({
610
+ text: "Stop",
611
+ type: "danger",
612
+ value: { action: "turn_cancel", sessionId: state.sessionId, turnId: state.turnId },
613
+ });
614
+ elements.push({ tag: "hr" }, taskActionRow(footerActions));
613
615
  }
614
616
  else if (state.status === "completed") {
615
- elements.push({ tag: "hr" }, taskActionRow([{
616
- text: "Reset",
617
- value: { action: "turn_reset", sessionId: state.sessionId, turnId: state.turnId },
618
- }]), {
617
+ footerActions.push({
618
+ text: "Reset",
619
+ value: { action: "turn_reset", sessionId: state.sessionId, turnId: state.turnId },
620
+ });
621
+ elements.push({ tag: "hr" }, taskActionRow(footerActions));
622
+ }
623
+ if (state.status === "completed") {
624
+ elements.push({
619
625
  ...markdown("<font color='grey'>Reset 会将当前任务的对话上下文恢复到本轮完成时;不会回退本地文件。</font>"),
620
626
  text_size: "notation",
621
627
  });
@@ -630,15 +636,22 @@ export class CardRenderer {
630
636
  }
631
637
  renderActivityHistory(state, requestedPage) {
632
638
  const grouped = this.thinkingCardLayout === "grouped";
639
+ const activities = turnActivities(state);
640
+ const allGroups = grouped ? groupTurnActivities(activities).flatMap(splitGroupedExecutionActivity) : [];
641
+ const historyGroups = grouped
642
+ ? groupedLiveActivityPage(groupedActivityPagesFromGroups(allGroups, state.projectCwd), state.projectCwd).historyGroups
643
+ : [];
644
+ const usesHistoricalPrefixPages = historyGroups.length > 0;
633
645
  const pages = grouped
634
- ? groupedActivityPages(turnActivities(state), state.projectCwd, {
646
+ ? groupedActivityPagesFromGroups(usesHistoricalPrefixPages ? historyGroups : allGroups, state.projectCwd, {
635
647
  fullActivityText: true,
636
648
  includeReasoningHistory: true,
637
649
  })
638
- : activityPages(turnActivities(state));
650
+ : activityPages(activities);
639
651
  if (pages.length === 0) {
640
652
  return this.renderSectionsCard("思考活动历史", [{ lines: ["没有保存到活动记录。"] }]);
641
653
  }
654
+ const totalPages = usesHistoricalPrefixPages ? pages.length + 1 : pages.length;
642
655
  const page = Math.max(0, Math.min(Math.trunc(requestedPage), pages.length - 1));
643
656
  const actions = [
644
657
  ...(pages.length > 1 ? [{
@@ -649,7 +662,7 @@ export class CardRenderer {
649
662
  text: "上一页",
650
663
  value: { action: "activity_history", turnId: state.turnId, page: String(page - 1) },
651
664
  }] : []),
652
- ...(page < pages.length - 2 ? [{
665
+ ...(page < pages.length - (usesHistoricalPrefixPages ? 1 : 2) ? [{
653
666
  text: "下一页",
654
667
  value: { action: "activity_history", turnId: state.turnId, page: String(page + 1) },
655
668
  }] : []),
@@ -663,7 +676,7 @@ export class CardRenderer {
663
676
  includeReasoningHistory: true,
664
677
  })
665
678
  : renderActivities(pages[page] ?? [], state.projectCwd, true)));
666
- return sectionCard(`思考活动历史 · ${page + 1}/${pages.length}`, elements.length > 0 ? elements : [markdown("无")]);
679
+ return sectionCard(`思考活动历史 · ${page + 1}/${totalPages}`, elements.length > 0 ? elements : [markdown("无")]);
667
680
  }
668
681
  renderSessionStarted(session) {
669
682
  return this.baseCard("ACP 会话已创建", "green", [
@@ -701,6 +714,51 @@ export class CardRenderer {
701
714
  renderStatus(status) {
702
715
  return this.baseCard("Agent Bot 状态", "blue", [markdown(status)]);
703
716
  }
717
+ renderDismissGroupConfirmation(view) {
718
+ const actionValue = {
719
+ contextKey: view.contextKey,
720
+ sessionId: view.sessionId,
721
+ requestedBy: view.requestedBy,
722
+ };
723
+ const button = (content, type, action) => ({
724
+ tag: "button",
725
+ width: "fill",
726
+ text: { tag: "plain_text", content },
727
+ type,
728
+ behaviors: [{
729
+ type: "callback",
730
+ value: { action, ...actionValue },
731
+ }],
732
+ });
733
+ return compactCard("解散当前群聊", "blue", [
734
+ markdown([
735
+ "确定要解散当前群聊吗?",
736
+ `当前任务「${escapeCardHtml(view.taskTitle)}」将同时归档。此操作无法撤销。`,
737
+ ].join("\n")),
738
+ {
739
+ tag: "column_set",
740
+ flex_mode: "none",
741
+ horizontal_spacing: "8px",
742
+ columns: [
743
+ {
744
+ tag: "column",
745
+ width: "weighted",
746
+ weight: 1,
747
+ elements: [button("Dismiss", "danger", "group_dismiss_confirm")],
748
+ },
749
+ {
750
+ tag: "column",
751
+ width: "weighted",
752
+ weight: 1,
753
+ elements: [button("Keep", "default", "group_dismiss_keep")],
754
+ },
755
+ ],
756
+ },
757
+ ]);
758
+ }
759
+ renderDismissGroupKept() {
760
+ return compactCard("已保留当前群聊", "grey", [markdown("已取消解散群聊。")]);
761
+ }
704
762
  renderSectionsCard(title, sections, actions = []) {
705
763
  const elements = [];
706
764
  sections.forEach((section, index) => {
@@ -868,16 +926,19 @@ export class CardRenderer {
868
926
  }
869
927
  }
870
928
  function renderTurnSubtitle(state) {
871
- const elapsed = state.durationMs ?? Math.max(0, Date.now() - state.startedAt);
872
929
  const activityTools = turnActivities(state).filter((activity) => activity.kind === "tool").length;
873
930
  const totalTools = state.totalToolCount ?? activityTools;
874
931
  return [
875
- `耗时 ${formatTurnDuration(elapsed)}`,
932
+ `耗时 ${renderTurnDuration(state)}`,
876
933
  state.totalTokens !== undefined ? `${formatTokenCount(state.totalTokens)} tokens` : undefined,
877
934
  totalTools > 0 ? `${totalTools} 个工具` : undefined,
878
935
  state.fileSummary.length > 0 ? `${state.fileSummary.length} 个文件` : undefined,
879
936
  ].filter(Boolean).join(" · ");
880
937
  }
938
+ function renderTurnDuration(state) {
939
+ const endedAt = state.completedAt ?? Date.now();
940
+ return formatTurnDuration(state.durationMs ?? Math.max(0, endedAt - state.startedAt));
941
+ }
881
942
  function isTurnStoppable(status) {
882
943
  return status === "running" || status === "tool_running" || status === "waiting_for_approval";
883
944
  }
@@ -953,26 +1014,26 @@ function renderGroupedTurnElements(state, assistantTextMode) {
953
1014
  const elements = [];
954
1015
  const allActivities = turnActivities(state);
955
1016
  const livePages = groupedActivityPages(allActivities, state.projectCwd);
956
- const historyPages = groupedActivityPages(allActivities, state.projectCwd, {
1017
+ const visible = groupedLiveActivityPage(livePages, state.projectCwd);
1018
+ const historyPages = groupedActivityPagesFromGroups(visible.historyGroups, state.projectCwd, {
957
1019
  fullActivityText: true,
958
1020
  includeReasoningHistory: true,
959
1021
  });
960
- const visibleGroups = livePages.at(-1) ?? [];
961
1022
  if (state.plan.length > 0)
962
1023
  elements.push(planPanel(state.plan));
963
- if (historyPages.length > 1) {
1024
+ if (historyPages.length > 0) {
964
1025
  elements.push(taskActionRow([{
965
- text: `查看历史思考(共 ${historyPages.length} 页)`,
1026
+ text: `查看历史思考(共 ${historyPages.length + 1} 页)`,
966
1027
  value: {
967
1028
  action: "activity_history",
968
1029
  turnId: state.turnId,
969
- page: String(historyPages.length - 2),
1030
+ page: String(historyPages.length - 1),
970
1031
  },
971
1032
  }]));
972
1033
  }
973
- if (state.activitiesTruncated || livePages.length > 1 || historyPages.length > 1)
1034
+ if (state.activitiesTruncated && visible.groups[0]?.kind !== "gap")
974
1035
  elements.push(markdown("…"));
975
- elements.push(...renderGroupedActivityGroups(visibleGroups, state.projectCwd));
1036
+ elements.push(...renderGroupedActivityGroups(visible.groups, state.projectCwd));
976
1037
  if (state.fileSummary.length > 0)
977
1038
  elements.push(fileSummaryPanel(state));
978
1039
  if (state.approval) {
@@ -1101,6 +1162,8 @@ function renderActivities(activities, projectCwd, fullAssistantText = false) {
1101
1162
  }
1102
1163
  function renderGroupedActivityGroups(groups, projectCwd, options = {}) {
1103
1164
  return groups.flatMap((group) => {
1165
+ if (group.kind === "gap")
1166
+ return [markdown("…")];
1104
1167
  if (group.kind === "activity") {
1105
1168
  return renderActivity(group.activity, projectCwd, options.fullActivityText === true);
1106
1169
  }
@@ -1169,19 +1232,17 @@ function executionActivityPanel(group, projectCwd, includeReasoningHistory = fal
1169
1232
  return collapsiblePanel(executionActivityTitle(group, status), elements, {
1170
1233
  elementId: executionActivityPanelElementId(group.id),
1171
1234
  expanded: false,
1172
- borderColor: status === "failed" ? "red" : status === "running" ? "blue" : "grey",
1235
+ borderColor: status === "running" ? "blue" : "grey",
1173
1236
  compact: true,
1174
1237
  });
1175
1238
  }
1176
1239
  function executionActivityStatus(tools) {
1177
1240
  if (tools.some((tool) => tool.status === "running"))
1178
1241
  return "running";
1179
- if (tools.some((tool) => tool.status === "failed"))
1180
- return "failed";
1181
1242
  return "completed";
1182
1243
  }
1183
1244
  function executionActivityTitle(group, status) {
1184
- const icon = status === "running" ? "⏳" : status === "failed" ? "❌" : "💭";
1245
+ const icon = status === "running" ? "⏳" : "💭";
1185
1246
  const latestReasoning = group.latestReasoning?.text
1186
1247
  ? removeMarkdownBold(group.latestReasoning.text).replace(/\s+/g, " ").trim()
1187
1248
  : "";
@@ -1189,9 +1250,7 @@ function executionActivityTitle(group, status) {
1189
1250
  ? truncateText(latestReasoning, 90)
1190
1251
  : status === "running"
1191
1252
  ? "正在执行工具"
1192
- : status === "failed"
1193
- ? "工具执行失败"
1194
- : "已执行工具";
1253
+ : "已执行工具";
1195
1254
  return `${icon} ${summary} · ${group.tools.length} 个工具`;
1196
1255
  }
1197
1256
  function executionActivityPanelElementId(activityId) {
@@ -1217,6 +1276,7 @@ const ACTIVITIES_PER_PAGE = 40;
1217
1276
  const GROUPED_TOOLS_PER_PANEL = 8;
1218
1277
  const GROUPED_PAGE_ACTIVITY_BYTES = 24 * 1024;
1219
1278
  const GROUPED_PAGE_ACTIVITY_COMPONENTS = 160;
1279
+ const PINNED_LIVE_COMMENTARIES = 3;
1220
1280
  const MAX_LIVE_ASSISTANT_TEXT = 2_000;
1221
1281
  const ACTIVITY_TEXT_CHUNK = 2_500;
1222
1282
  function activityPages(activities) {
@@ -1234,6 +1294,9 @@ function activityPages(activities) {
1234
1294
  }
1235
1295
  function groupedActivityPages(activities, projectCwd, renderOptions = {}) {
1236
1296
  const groups = groupTurnActivities(activities).flatMap(splitGroupedExecutionActivity);
1297
+ return groupedActivityPagesFromGroups(groups, projectCwd, renderOptions);
1298
+ }
1299
+ function groupedActivityPagesFromGroups(groups, projectCwd, renderOptions = {}) {
1237
1300
  if (groups.length === 0)
1238
1301
  return [];
1239
1302
  const newestFirst = [];
@@ -1256,6 +1319,69 @@ function groupedActivityPageFits(groups, projectCwd, renderOptions) {
1256
1319
  return Buffer.byteLength(JSON.stringify(elements), "utf8") <= GROUPED_PAGE_ACTIVITY_BYTES
1257
1320
  && countCardComponents(elements) <= GROUPED_PAGE_ACTIVITY_COMPONENTS;
1258
1321
  }
1322
+ function groupedLiveActivityPage(pages, projectCwd) {
1323
+ const latest = pages.at(-1) ?? [];
1324
+ if (pages.length <= 1)
1325
+ return { groups: latest, historyGroups: [] };
1326
+ const groups = pages.flat();
1327
+ const latestStart = groups.length - latest.length;
1328
+ const commentaryIndexes = groups
1329
+ .map((group, index) => isCommentaryGroup(group) ? index : -1)
1330
+ .filter((index) => index >= 0);
1331
+ if (commentaryIndexes.length === 0) {
1332
+ return { groups: latest, historyGroups: groups.slice(0, latestStart) };
1333
+ }
1334
+ const selected = new Set();
1335
+ const pinnedCommentaries = commentaryIndexes.slice(0, PINNED_LIVE_COMMENTARIES);
1336
+ for (const index of pinnedCommentaries)
1337
+ selected.add(index);
1338
+ const latestCommentary = commentaryIndexes.at(-1);
1339
+ for (let index = latestCommentary; index < groups.length; index += 1)
1340
+ selected.add(index);
1341
+ const renderSelection = (selection) => ({
1342
+ groups: groupsWithGaps(groups, selection),
1343
+ historyGroups: groups.filter((_group, index) => index < latestCommentary || !selection.has(index)),
1344
+ });
1345
+ const fits = (selection) => {
1346
+ const view = renderSelection(selection);
1347
+ return groupedActivityPageFits(view.groups, projectCwd, {});
1348
+ };
1349
+ if (!fits(selected)) {
1350
+ const latestExecutions = [...selected]
1351
+ .filter((index) => index > latestCommentary && groups[index]?.kind === "execution")
1352
+ .sort((left, right) => left - right);
1353
+ const newestExecution = latestExecutions.at(-1);
1354
+ for (const index of latestExecutions) {
1355
+ if (fits(selected) || index === newestExecution)
1356
+ break;
1357
+ selected.delete(index);
1358
+ }
1359
+ }
1360
+ return renderSelection(selected);
1361
+ }
1362
+ function isCommentaryGroup(group) {
1363
+ return group.kind === "activity" && isCommentaryActivity(group.activity);
1364
+ }
1365
+ function groupsWithGaps(groups, selected) {
1366
+ const indexes = [...selected].sort((left, right) => left - right);
1367
+ const visible = [];
1368
+ const append = (group) => {
1369
+ if (group.kind === "gap" && visible.at(-1)?.kind === "gap")
1370
+ return;
1371
+ visible.push(group);
1372
+ };
1373
+ let previous = -1;
1374
+ for (const index of indexes) {
1375
+ if (previous >= 0 && index > previous + 1) {
1376
+ append({ kind: "gap", id: `gap:${previous}:${index}` });
1377
+ }
1378
+ const group = groups[index];
1379
+ if (group)
1380
+ append(group);
1381
+ previous = index;
1382
+ }
1383
+ return visible;
1384
+ }
1259
1385
  function countCardComponents(value) {
1260
1386
  if (Array.isArray(value)) {
1261
1387
  return value.reduce((total, entry) => total + countCardComponents(entry), 0);
@@ -1415,12 +1541,21 @@ function renderToolDetails(tool, projectCwd) {
1415
1541
  const result = tool.error ?? tool.output ?? fileSummary;
1416
1542
  const normalizedCommand = stripAnsi(command).trim();
1417
1543
  const displayCommand = tool.kind === "command"
1418
- ? unwrapPowerShellCommand(normalizedCommand) ?? normalizedCommand
1544
+ ? unwrapShellCommand(normalizedCommand) ?? normalizedCommand
1419
1545
  : normalizedCommand;
1420
- const commandText = truncateText(displayCommand, 800);
1421
- const resultText = result ? truncateMiddle(stripAnsi(result).trim(), 1_200) : undefined;
1546
+ const commandText = truncateText(displayCommand, 600);
1547
+ const resultText = result ? truncateToolResult(stripAnsi(result).trim()) : undefined;
1422
1548
  return codeBlock([`$ ${commandText}`, resultText].filter((part) => part !== undefined).join("\n"), 2_003);
1423
1549
  }
1550
+ function truncateToolResult(result) {
1551
+ const maxLength = 900;
1552
+ const marker = "\n...\n";
1553
+ const tailLength = 600;
1554
+ if (result.length <= maxLength)
1555
+ return result;
1556
+ const headLength = maxLength - marker.length - tailLength;
1557
+ return `${result.slice(0, headLength)}${marker}${result.slice(-tailLength)}`;
1558
+ }
1424
1559
  function displayFilePath(filePath, projectCwd) {
1425
1560
  if (!projectCwd)
1426
1561
  return filePath;
@@ -1447,7 +1582,7 @@ function toolPanelTitle(tool) {
1447
1582
  const command = stripAnsi(tool.command ?? tool.title).trim();
1448
1583
  const meaningfulCommand = tool.kind === "web_search"
1449
1584
  ? tool.title
1450
- : unwrapPowerShellCommand(command) ?? tool.title;
1585
+ : unwrapShellCommand(command) ?? tool.title;
1451
1586
  const duration = toolDuration(tool);
1452
1587
  const prefix = `${icon} `;
1453
1588
  const suffix = duration ? ` · ${duration}` : "";
@@ -1494,6 +1629,9 @@ function formatTurnDuration(durationMs) {
1494
1629
  return `${String(totalMinutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
1495
1630
  return `${seconds}s`;
1496
1631
  }
1632
+ function unwrapShellCommand(command) {
1633
+ return unwrapPowerShellCommand(command) ?? unwrapPosixShellCommand(command);
1634
+ }
1497
1635
  function unwrapPowerShellCommand(command) {
1498
1636
  const executable = command.match(/^(?:"[^"]*(?:pwsh|powershell)\.exe"|(?:\S*[\\/])?(?:pwsh|powershell)(?:\.exe)?)(?=\s|$)/i);
1499
1637
  if (!executable)
@@ -1505,6 +1643,71 @@ function unwrapPowerShellCommand(command) {
1505
1643
  const payload = args.slice(commandFlag.index + commandFlag[0].length).trim();
1506
1644
  if (!payload)
1507
1645
  return undefined;
1646
+ return unwrapQuotedPayload(payload);
1647
+ }
1648
+ function unwrapPosixShellCommand(command) {
1649
+ let executable = readCommandWord(command, 0);
1650
+ if (!executable)
1651
+ return undefined;
1652
+ if (posixExecutableName(executable.value) === "env") {
1653
+ let cursor = executable.end;
1654
+ while (true) {
1655
+ const candidate = readCommandWord(command, cursor);
1656
+ if (!candidate)
1657
+ return undefined;
1658
+ const name = posixExecutableName(candidate.value);
1659
+ if (isPosixShell(name)) {
1660
+ executable = candidate;
1661
+ break;
1662
+ }
1663
+ if (!candidate.value.startsWith("-") && !/^[A-Za-z_][A-Za-z0-9_]*=/.test(candidate.value)) {
1664
+ return undefined;
1665
+ }
1666
+ cursor = candidate.end;
1667
+ }
1668
+ }
1669
+ else if (!isPosixShell(posixExecutableName(executable.value))) {
1670
+ return undefined;
1671
+ }
1672
+ const args = command.slice(executable.end);
1673
+ const commandFlag = /(?:^|\s)(?:-[A-Za-z]*c[A-Za-z]*|--command)(?:\s+|=)/.exec(args);
1674
+ if (!commandFlag)
1675
+ return undefined;
1676
+ const payload = args.slice(commandFlag.index + commandFlag[0].length).trim();
1677
+ if (!payload)
1678
+ return undefined;
1679
+ return unwrapQuotedPayload(payload);
1680
+ }
1681
+ function readCommandWord(command, start) {
1682
+ let cursor = start;
1683
+ while (cursor < command.length && /\s/.test(command[cursor] ?? ""))
1684
+ cursor += 1;
1685
+ if (cursor >= command.length)
1686
+ return undefined;
1687
+ const quote = command[cursor];
1688
+ if (quote === "\"" || quote === "'") {
1689
+ const endQuote = command.indexOf(quote, cursor + 1);
1690
+ if (endQuote < 0)
1691
+ return undefined;
1692
+ return { value: command.slice(cursor + 1, endQuote), end: endQuote + 1 };
1693
+ }
1694
+ const word = /^\S+/.exec(command.slice(cursor));
1695
+ if (!word)
1696
+ return undefined;
1697
+ return { value: word[0], end: cursor + word[0].length };
1698
+ }
1699
+ function posixExecutableName(executable) {
1700
+ return executable.replaceAll("\\", "/").split("/").at(-1)?.toLowerCase() ?? "";
1701
+ }
1702
+ function isPosixShell(executable) {
1703
+ return executable === "sh"
1704
+ || executable === "bash"
1705
+ || executable === "zsh"
1706
+ || executable === "dash"
1707
+ || executable === "ksh"
1708
+ || executable === "fish";
1709
+ }
1710
+ function unwrapQuotedPayload(payload) {
1508
1711
  const quote = payload[0];
1509
1712
  if ((quote === "\"" || quote === "'") && payload.at(-1) === quote)
1510
1713
  return payload.slice(1, -1).trim();
@@ -1889,10 +2092,12 @@ function turnCard(title, template, elements, subtitle) {
1889
2092
  tag: "plain_text",
1890
2093
  content: title,
1891
2094
  },
1892
- subtitle: {
1893
- tag: "plain_text",
1894
- content: subtitle,
1895
- },
2095
+ ...(subtitle ? {
2096
+ subtitle: {
2097
+ tag: "plain_text",
2098
+ content: subtitle,
2099
+ },
2100
+ } : {}),
1896
2101
  padding: "12px 12px 12px 12px",
1897
2102
  },
1898
2103
  body: {
@@ -1926,6 +2131,29 @@ function sectionCard(title, elements, template = "blue", verticalSpacing = "8px"
1926
2131
  },
1927
2132
  };
1928
2133
  }
2134
+ function compactCard(title, template, elements) {
2135
+ return {
2136
+ schema: "2.0",
2137
+ config: {
2138
+ update_multi: true,
2139
+ width_mode: "compact",
2140
+ },
2141
+ header: {
2142
+ template,
2143
+ title: {
2144
+ tag: "plain_text",
2145
+ content: title,
2146
+ },
2147
+ padding: "12px 12px 12px 12px",
2148
+ },
2149
+ body: {
2150
+ direction: "vertical",
2151
+ vertical_spacing: "12px",
2152
+ padding: "12px 12px 12px 12px",
2153
+ elements,
2154
+ },
2155
+ };
2156
+ }
1929
2157
  function formatUpdate(update) {
1930
2158
  const updateType = update.sessionUpdate;
1931
2159
  if (updateType === "agent_message_chunk" && isObject(update.content)) {