@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.
- package/CHANGELOG.md +26 -0
- package/README.md +82 -55
- package/README.zh.md +82 -55
- package/config.example.yaml +5 -1
- package/dist/cli/AutostartManager.js +431 -0
- package/dist/cli/AutostartManager.js.map +1 -0
- package/dist/cli/FeishuAppConfiguration.js +17 -13
- package/dist/cli/FeishuAppConfiguration.js.map +1 -1
- package/dist/cli/Initializer.js +35 -0
- package/dist/cli/Initializer.js.map +1 -1
- package/dist/cli/SelfUpdateRunner.js +552 -0
- package/dist/cli/SelfUpdateRunner.js.map +1 -0
- package/dist/cli/SelfUpdater.js +431 -0
- package/dist/cli/SelfUpdater.js.map +1 -0
- package/dist/cli/autostartOutput.js +63 -0
- package/dist/cli/autostartOutput.js.map +1 -0
- package/dist/cli/controlProtocol.js.map +1 -1
- package/dist/cli/help.js +92 -18
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/taskGroupOptions.js +14 -11
- package/dist/cli/taskGroupOptions.js.map +1 -1
- package/dist/cli/taskListOutput.js +3 -0
- package/dist/cli/taskListOutput.js.map +1 -1
- package/dist/cli/taskTarget.js +99 -0
- package/dist/cli/taskTarget.js.map +1 -0
- package/dist/cli.js +643 -58
- package/dist/cli.js.map +1 -1
- package/dist/codex/CodexRuntime.js +16 -5
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/commands/CommandRouter.js +20 -1
- package/dist/commands/CommandRouter.js.map +1 -1
- package/dist/config/loadConfig.js +2 -0
- package/dist/config/loadConfig.js.map +1 -1
- package/dist/config/schema.js +2 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/console/ConsoleTurnPresenter.js +2 -0
- package/dist/console/ConsoleTurnPresenter.js.map +1 -1
- package/dist/feishu/CardRenderer.js +584 -16
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +29 -8
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/feishu/FeishuMarkdown.js +25 -1
- package/dist/feishu/FeishuMarkdown.js.map +1 -1
- package/dist/feishu/FeishuMessageClient.js +43 -0
- package/dist/feishu/FeishuMessageClient.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +29 -1
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/feishu/ownerAccess.js +7 -0
- package/dist/feishu/ownerAccess.js.map +1 -0
- package/dist/index.js +148 -19
- package/dist/index.js.map +1 -1
- package/dist/presentation/OutboundRouter.js +19 -1
- package/dist/presentation/OutboundRouter.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +1444 -66
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/startup/StartupNotifier.js +65 -16
- package/dist/startup/StartupNotifier.js.map +1 -1
- package/dist/startup/startFeishu.js +2 -2
- package/dist/startup/startFeishu.js.map +1 -1
- package/dist/state/StateStore.js +268 -67
- package/dist/state/StateStore.js.map +1 -1
- package/dist/state/migrations.js +26 -0
- package/dist/state/migrations.js.map +1 -1
- package/dist/supervision/SafeRestartNotifier.js +17 -16
- package/dist/supervision/SafeRestartNotifier.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +12 -7
- package/dist/supervision/SafeRestartScheduler.js.map +1 -1
- package/dist/supervision/replacementSupervisor.js +45 -4
- package/dist/supervision/replacementSupervisor.js.map +1 -1
- package/dist/supervisor.js +30 -18
- package/dist/supervisor.js.map +1 -1
- package/docs/technical-reference.md +33 -10
- package/docs/technical-reference.zh.md +33 -10
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -2
- package/skills/agent-bot/SKILL.md +107 -182
- package/src/cli/AutostartManager.ts +579 -0
- package/src/cli/FeishuAppConfiguration.ts +22 -7
- package/src/cli/Initializer.ts +57 -0
- package/src/cli/SelfUpdateRunner.ts +817 -0
- package/src/cli/SelfUpdater.ts +688 -0
- package/src/cli/autostartOutput.ts +75 -0
- package/src/cli/controlProtocol.ts +111 -1
- package/src/cli/help.ts +92 -18
- package/src/cli/taskGroupOptions.ts +27 -17
- package/src/cli/taskListOutput.ts +10 -0
- package/src/cli/taskTarget.ts +168 -0
- package/src/cli.ts +781 -65
- package/src/codex/CodexRuntime.ts +17 -5
- package/src/commands/CommandRouter.ts +19 -1
- package/src/commands/commandTypes.ts +3 -0
- package/src/config/loadConfig.ts +2 -0
- package/src/config/schema.ts +2 -0
- package/src/console/ConsoleTurnPresenter.ts +4 -1
- package/src/feishu/CardRenderer.ts +716 -16
- package/src/feishu/FeishuConnector.ts +44 -10
- package/src/feishu/FeishuMarkdown.ts +27 -1
- package/src/feishu/FeishuMessageClient.ts +58 -0
- package/src/feishu/FeishuTurnPresenter.ts +35 -2
- package/src/feishu/ownerAccess.ts +7 -0
- package/src/feishu/types.ts +7 -0
- package/src/index.ts +192 -22
- package/src/presentation/OutboundRouter.ts +28 -3
- package/src/proxy/ProxySessionController.ts +1707 -70
- package/src/runtime/types.ts +1 -0
- package/src/startup/StartupNotifier.ts +67 -17
- package/src/startup/startFeishu.ts +5 -3
- package/src/state/StateStore.ts +413 -65
- package/src/state/migrations.ts +26 -0
- package/src/supervision/SafeRestartNotifier.ts +17 -17
- package/src/supervision/SafeRestartScheduler.ts +13 -6
- package/src/supervision/replacementSupervisor.ts +50 -4
- 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 {
|
|
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 =
|
|
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
|
|
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(...
|
|
868
|
-
|
|
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: {
|
|
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
|
-
?
|
|
1892
|
+
? unwrapShellCommand(normalizedCommand) ?? normalizedCommand
|
|
1339
1893
|
: normalizedCommand;
|
|
1340
|
-
const commandText = truncateText(displayCommand,
|
|
1341
|
-
const resultText = result ?
|
|
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
|
-
:
|
|
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"}'>${
|
|
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("\\", "\");
|
|
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:
|
|
2532
|
+
vertical_spacing: verticalSpacing,
|
|
1833
2533
|
padding: "12px 12px 12px 12px",
|
|
1834
2534
|
elements,
|
|
1835
2535
|
},
|