@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.
- package/CHANGELOG.md +19 -1
- package/README.md +48 -15
- package/README.zh.md +48 -15
- package/config.example.yaml +1 -1
- package/dist/cli/AutostartManager.js +431 -0
- package/dist/cli/AutostartManager.js.map +1 -0
- package/dist/cli/FeishuAppConfiguration.js +21 -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 +94 -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 +661 -58
- package/dist/cli.js.map +1 -1
- package/dist/codex/CodexRuntime.js +8 -2
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/commands/CommandRouter.js +16 -1
- package/dist/commands/CommandRouter.js.map +1 -1
- package/dist/feishu/CardRenderer.js +266 -38
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuMarkdown.js +25 -1
- package/dist/feishu/FeishuMarkdown.js.map +1 -1
- package/dist/feishu/FeishuMessageClient.js +16 -0
- package/dist/feishu/FeishuMessageClient.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +42 -6
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/index.js +136 -2
- package/dist/index.js.map +1 -1
- package/dist/presentation/OutboundRouter.js +7 -0
- package/dist/presentation/OutboundRouter.js.map +1 -1
- package/dist/presentation/TurnStateReducer.js +21 -7
- package/dist/presentation/TurnStateReducer.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +537 -13
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/state/StateStore.js +53 -0
- package/dist/state/StateStore.js.map +1 -1
- package/docs/technical-reference.md +17 -7
- package/docs/technical-reference.zh.md +17 -7
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -2
- package/skills/agent-bot/SKILL.md +109 -188
- package/src/cli/AutostartManager.ts +579 -0
- package/src/cli/FeishuAppConfiguration.ts +26 -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 +119 -1
- package/src/cli/help.ts +94 -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 +809 -65
- package/src/codex/CodexRuntime.ts +8 -2
- package/src/commands/CommandRouter.ts +14 -1
- package/src/commands/commandTypes.ts +2 -0
- package/src/feishu/CardRenderer.ts +313 -35
- package/src/feishu/FeishuMarkdown.ts +27 -1
- package/src/feishu/FeishuMessageClient.ts +33 -0
- package/src/feishu/FeishuTurnPresenter.ts +43 -6
- package/src/feishu/types.ts +1 -0
- package/src/index.ts +174 -2
- package/src/presentation/OutboundRouter.ts +7 -0
- package/src/presentation/TurnStateReducer.ts +21 -7
- package/src/proxy/ProxySessionController.ts +661 -16
- package/src/runtime/types.ts +1 -0
- package/src/state/StateStore.ts +56 -0
|
@@ -355,12 +355,18 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
355
355
|
|
|
356
356
|
async closeSession(sessionId: string): Promise<void> {
|
|
357
357
|
const session = this.requireSession(sessionId);
|
|
358
|
+
await this.archiveRemoteSession(session.remoteSessionId);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async archiveRemoteSession(remoteSessionId: string): Promise<void> {
|
|
358
362
|
await (await this.client()).request(
|
|
359
363
|
"thread/archive",
|
|
360
|
-
{ threadId:
|
|
364
|
+
{ threadId: remoteSessionId },
|
|
361
365
|
CONTROL_REQUEST_TIMEOUT_MS,
|
|
362
366
|
);
|
|
363
|
-
this.sessions
|
|
367
|
+
for (const [localSessionId, session] of this.sessions) {
|
|
368
|
+
if (session.remoteSessionId === remoteSessionId) this.sessions.delete(localSessionId);
|
|
369
|
+
}
|
|
364
370
|
}
|
|
365
371
|
|
|
366
372
|
async setTitle(sessionId: string, title: string): Promise<void> {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { Command } from "./commandTypes.js";
|
|
2
2
|
|
|
3
|
-
const COMMAND_NAMES = [
|
|
3
|
+
export const COMMAND_NAMES = [
|
|
4
4
|
"agent",
|
|
5
|
+
"archive",
|
|
5
6
|
"dir",
|
|
7
|
+
"dismiss",
|
|
6
8
|
"fork",
|
|
7
9
|
"forkgroup",
|
|
8
10
|
"goal",
|
|
@@ -28,6 +30,7 @@ const COMMAND_NAMES = [
|
|
|
28
30
|
type CommandName = (typeof COMMAND_NAMES)[number];
|
|
29
31
|
|
|
30
32
|
const COMMAND_INITIALISMS: Partial<Record<CommandName, string>> = {
|
|
33
|
+
dir: "di",
|
|
31
34
|
forkgroup: "fg",
|
|
32
35
|
newgroup: "ng",
|
|
33
36
|
nosteer: "ns",
|
|
@@ -76,6 +79,13 @@ export class CommandRouter {
|
|
|
76
79
|
return args[0] ? { type: "switch", sessionId: args[0] } : { type: "switch" };
|
|
77
80
|
case "agent":
|
|
78
81
|
return args[0] ? { type: "agent", agent: args[0] } : { type: "agent" };
|
|
82
|
+
case "archive":
|
|
83
|
+
rejectOptions("/archive", args);
|
|
84
|
+
if (args.length > 1) throw new Error("/archive 只接受一个可选的任务序号或任务 ID。");
|
|
85
|
+
return args[0] ? { type: "archive", sessionId: args[0] } : { type: "archive" };
|
|
86
|
+
case "dismiss":
|
|
87
|
+
if (args.length > 0) throw new Error("/dismiss 不接受参数,请在确认卡片中完成操作。");
|
|
88
|
+
return { type: "dismiss" };
|
|
79
89
|
case "stop":
|
|
80
90
|
return { type: "stop" };
|
|
81
91
|
case "status":
|
|
@@ -132,6 +142,9 @@ function resolveCommandName(rawCommand: string): CommandName {
|
|
|
132
142
|
const exact = COMMAND_NAMES.find((command) => command === input);
|
|
133
143
|
if (exact) return exact;
|
|
134
144
|
|
|
145
|
+
const initialism = COMMAND_NAMES.find((command) => COMMAND_INITIALISMS[command] === input);
|
|
146
|
+
if (initialism) return initialism;
|
|
147
|
+
|
|
135
148
|
const matches = COMMAND_NAMES.filter(
|
|
136
149
|
(command) => command.startsWith(input) || COMMAND_INITIALISMS[command] === input,
|
|
137
150
|
);
|
|
@@ -10,6 +10,8 @@ export type Command =
|
|
|
10
10
|
| { type: "sessions"; searchTerm?: string }
|
|
11
11
|
| { type: "switch"; sessionId?: string }
|
|
12
12
|
| { type: "agent"; agent?: string }
|
|
13
|
+
| { type: "archive"; sessionId?: string }
|
|
14
|
+
| { type: "dismiss" }
|
|
13
15
|
| { type: "stop" }
|
|
14
16
|
| { type: "status"; sessionId?: string }
|
|
15
17
|
| { type: "goal"; action: "show" }
|
|
@@ -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 {
|
|
@@ -226,6 +226,13 @@ export interface ResetHistoryCardView {
|
|
|
226
226
|
pageActions: TaskListCardAction[];
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
export interface DismissGroupCardView {
|
|
230
|
+
contextKey: string;
|
|
231
|
+
sessionId: string;
|
|
232
|
+
taskTitle: string;
|
|
233
|
+
requestedBy: string;
|
|
234
|
+
}
|
|
235
|
+
|
|
229
236
|
export class CardRenderer {
|
|
230
237
|
private readonly thinkingCardLayout: ThinkingCardLayout;
|
|
231
238
|
|
|
@@ -853,19 +860,29 @@ export class CardRenderer {
|
|
|
853
860
|
const elements = this.thinkingCardLayout === "timeline"
|
|
854
861
|
? renderTurnElements(state, "hidden")
|
|
855
862
|
: renderGroupedTurnElements(state, "hidden");
|
|
863
|
+
const footerActions: TaskListCardAction[] = [];
|
|
856
864
|
if (isTurnStoppable(state.status)) {
|
|
857
|
-
|
|
865
|
+
footerActions.push({
|
|
858
866
|
text: "Stop",
|
|
859
867
|
type: "danger",
|
|
860
868
|
value: { action: "turn_cancel", sessionId: state.sessionId, turnId: state.turnId },
|
|
861
|
-
}
|
|
869
|
+
});
|
|
870
|
+
elements.push(
|
|
871
|
+
{ tag: "hr" },
|
|
872
|
+
taskActionRow(footerActions),
|
|
873
|
+
);
|
|
862
874
|
} else if (state.status === "completed") {
|
|
875
|
+
footerActions.push({
|
|
876
|
+
text: "Reset",
|
|
877
|
+
value: { action: "turn_reset", sessionId: state.sessionId, turnId: state.turnId },
|
|
878
|
+
});
|
|
863
879
|
elements.push(
|
|
864
880
|
{ tag: "hr" },
|
|
865
|
-
taskActionRow(
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
881
|
+
taskActionRow(footerActions),
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
if (state.status === "completed") {
|
|
885
|
+
elements.push(
|
|
869
886
|
{
|
|
870
887
|
...markdown("<font color='grey'>Reset 会将当前任务的对话上下文恢复到本轮完成时;不会回退本地文件。</font>"),
|
|
871
888
|
text_size: "notation",
|
|
@@ -889,15 +906,26 @@ export class CardRenderer {
|
|
|
889
906
|
|
|
890
907
|
renderActivityHistory(state: TurnViewState, requestedPage: number): Record<string, unknown> {
|
|
891
908
|
const grouped = this.thinkingCardLayout === "grouped";
|
|
909
|
+
const activities = turnActivities(state);
|
|
910
|
+
const allGroups = grouped ? groupTurnActivities(activities).flatMap(splitGroupedExecutionActivity) : [];
|
|
911
|
+
const historyGroups = grouped
|
|
912
|
+
? groupedLiveActivityPage(groupedActivityPagesFromGroups(allGroups, state.projectCwd), state.projectCwd).historyGroups
|
|
913
|
+
: [];
|
|
914
|
+
const usesHistoricalPrefixPages = historyGroups.length > 0;
|
|
892
915
|
const pages = grouped
|
|
893
|
-
?
|
|
916
|
+
? groupedActivityPagesFromGroups(
|
|
917
|
+
usesHistoricalPrefixPages ? historyGroups : allGroups,
|
|
918
|
+
state.projectCwd,
|
|
919
|
+
{
|
|
894
920
|
fullActivityText: true,
|
|
895
921
|
includeReasoningHistory: true,
|
|
896
|
-
|
|
897
|
-
|
|
922
|
+
},
|
|
923
|
+
)
|
|
924
|
+
: activityPages(activities);
|
|
898
925
|
if (pages.length === 0) {
|
|
899
926
|
return this.renderSectionsCard("思考活动历史", [{ lines: ["没有保存到活动记录。"] }]);
|
|
900
927
|
}
|
|
928
|
+
const totalPages = usesHistoricalPrefixPages ? pages.length + 1 : pages.length;
|
|
901
929
|
const page = Math.max(0, Math.min(Math.trunc(requestedPage), pages.length - 1));
|
|
902
930
|
const actions: TaskListCardAction[] = [
|
|
903
931
|
...(pages.length > 1 ? [{
|
|
@@ -908,7 +936,7 @@ export class CardRenderer {
|
|
|
908
936
|
text: "上一页",
|
|
909
937
|
value: { action: "activity_history", turnId: state.turnId, page: String(page - 1) },
|
|
910
938
|
}] : []),
|
|
911
|
-
...(page < pages.length - 2 ? [{
|
|
939
|
+
...(page < pages.length - (usesHistoricalPrefixPages ? 1 : 2) ? [{
|
|
912
940
|
text: "下一页",
|
|
913
941
|
value: { action: "activity_history", turnId: state.turnId, page: String(page + 1) },
|
|
914
942
|
}] : []),
|
|
@@ -921,7 +949,7 @@ export class CardRenderer {
|
|
|
921
949
|
includeReasoningHistory: true,
|
|
922
950
|
})
|
|
923
951
|
: renderActivities(pages[page] as TurnActivity[] | undefined ?? [], state.projectCwd, true)));
|
|
924
|
-
return sectionCard(`思考活动历史 · ${page + 1}/${
|
|
952
|
+
return sectionCard(`思考活动历史 · ${page + 1}/${totalPages}`, elements.length > 0 ? elements : [markdown("无")]);
|
|
925
953
|
}
|
|
926
954
|
|
|
927
955
|
renderSessionStarted(session: RuntimeSession): Record<string, unknown> {
|
|
@@ -969,6 +997,57 @@ export class CardRenderer {
|
|
|
969
997
|
return this.baseCard("Agent Bot 状态", "blue", [markdown(status)]);
|
|
970
998
|
}
|
|
971
999
|
|
|
1000
|
+
renderDismissGroupConfirmation(view: DismissGroupCardView): Record<string, unknown> {
|
|
1001
|
+
const actionValue = {
|
|
1002
|
+
contextKey: view.contextKey,
|
|
1003
|
+
sessionId: view.sessionId,
|
|
1004
|
+
requestedBy: view.requestedBy,
|
|
1005
|
+
};
|
|
1006
|
+
const button = (
|
|
1007
|
+
content: string,
|
|
1008
|
+
type: "default" | "danger",
|
|
1009
|
+
action: "group_dismiss_confirm" | "group_dismiss_keep",
|
|
1010
|
+
): Record<string, unknown> => ({
|
|
1011
|
+
tag: "button",
|
|
1012
|
+
width: "fill",
|
|
1013
|
+
text: { tag: "plain_text", content },
|
|
1014
|
+
type,
|
|
1015
|
+
behaviors: [{
|
|
1016
|
+
type: "callback",
|
|
1017
|
+
value: { action, ...actionValue },
|
|
1018
|
+
}],
|
|
1019
|
+
});
|
|
1020
|
+
return compactCard("解散当前群聊", "blue", [
|
|
1021
|
+
markdown([
|
|
1022
|
+
"确定要解散当前群聊吗?",
|
|
1023
|
+
`当前任务「${escapeCardHtml(view.taskTitle)}」将同时归档。此操作无法撤销。`,
|
|
1024
|
+
].join("\n")),
|
|
1025
|
+
{
|
|
1026
|
+
tag: "column_set",
|
|
1027
|
+
flex_mode: "none",
|
|
1028
|
+
horizontal_spacing: "8px",
|
|
1029
|
+
columns: [
|
|
1030
|
+
{
|
|
1031
|
+
tag: "column",
|
|
1032
|
+
width: "weighted",
|
|
1033
|
+
weight: 1,
|
|
1034
|
+
elements: [button("Dismiss", "danger", "group_dismiss_confirm")],
|
|
1035
|
+
},
|
|
1036
|
+
{
|
|
1037
|
+
tag: "column",
|
|
1038
|
+
width: "weighted",
|
|
1039
|
+
weight: 1,
|
|
1040
|
+
elements: [button("Keep", "default", "group_dismiss_keep")],
|
|
1041
|
+
},
|
|
1042
|
+
],
|
|
1043
|
+
},
|
|
1044
|
+
]);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
renderDismissGroupKept(): Record<string, unknown> {
|
|
1048
|
+
return compactCard("已保留当前群聊", "grey", [markdown("已取消解散群聊。")]);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
972
1051
|
renderSectionsCard(
|
|
973
1052
|
title: string,
|
|
974
1053
|
sections: CardSection[],
|
|
@@ -1160,17 +1239,21 @@ export class CardRenderer {
|
|
|
1160
1239
|
}
|
|
1161
1240
|
|
|
1162
1241
|
function renderTurnSubtitle(state: TurnViewState): string {
|
|
1163
|
-
const elapsed = state.durationMs ?? Math.max(0, Date.now() - state.startedAt);
|
|
1164
1242
|
const activityTools = turnActivities(state).filter((activity) => activity.kind === "tool").length;
|
|
1165
1243
|
const totalTools = state.totalToolCount ?? activityTools;
|
|
1166
1244
|
return [
|
|
1167
|
-
`耗时 ${
|
|
1245
|
+
`耗时 ${renderTurnDuration(state)}`,
|
|
1168
1246
|
state.totalTokens !== undefined ? `${formatTokenCount(state.totalTokens)} tokens` : undefined,
|
|
1169
1247
|
totalTools > 0 ? `${totalTools} 个工具` : undefined,
|
|
1170
1248
|
state.fileSummary.length > 0 ? `${state.fileSummary.length} 个文件` : undefined,
|
|
1171
1249
|
].filter(Boolean).join(" · ");
|
|
1172
1250
|
}
|
|
1173
1251
|
|
|
1252
|
+
function renderTurnDuration(state: TurnViewState): string {
|
|
1253
|
+
const endedAt = state.completedAt ?? Date.now();
|
|
1254
|
+
return formatTurnDuration(state.durationMs ?? Math.max(0, endedAt - state.startedAt));
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1174
1257
|
function isTurnStoppable(status: TurnViewStatus): boolean {
|
|
1175
1258
|
return status === "running" || status === "tool_running" || status === "waiting_for_approval";
|
|
1176
1259
|
}
|
|
@@ -1249,24 +1332,24 @@ function renderGroupedTurnElements(
|
|
|
1249
1332
|
const elements: Record<string, unknown>[] = [];
|
|
1250
1333
|
const allActivities = turnActivities(state);
|
|
1251
1334
|
const livePages = groupedActivityPages(allActivities, state.projectCwd);
|
|
1252
|
-
const
|
|
1335
|
+
const visible = groupedLiveActivityPage(livePages, state.projectCwd);
|
|
1336
|
+
const historyPages = groupedActivityPagesFromGroups(visible.historyGroups, state.projectCwd, {
|
|
1253
1337
|
fullActivityText: true,
|
|
1254
1338
|
includeReasoningHistory: true,
|
|
1255
1339
|
});
|
|
1256
|
-
const visibleGroups = livePages.at(-1) ?? [];
|
|
1257
1340
|
if (state.plan.length > 0) elements.push(planPanel(state.plan));
|
|
1258
|
-
if (historyPages.length >
|
|
1341
|
+
if (historyPages.length > 0) {
|
|
1259
1342
|
elements.push(taskActionRow([{
|
|
1260
|
-
text: `查看历史思考(共 ${historyPages.length} 页)`,
|
|
1343
|
+
text: `查看历史思考(共 ${historyPages.length + 1} 页)`,
|
|
1261
1344
|
value: {
|
|
1262
1345
|
action: "activity_history",
|
|
1263
1346
|
turnId: state.turnId,
|
|
1264
|
-
page: String(historyPages.length -
|
|
1347
|
+
page: String(historyPages.length - 1),
|
|
1265
1348
|
},
|
|
1266
1349
|
}]));
|
|
1267
1350
|
}
|
|
1268
|
-
if (state.activitiesTruncated
|
|
1269
|
-
elements.push(...renderGroupedActivityGroups(
|
|
1351
|
+
if (state.activitiesTruncated && visible.groups[0]?.kind !== "gap") elements.push(markdown("…"));
|
|
1352
|
+
elements.push(...renderGroupedActivityGroups(visible.groups, state.projectCwd));
|
|
1270
1353
|
if (state.fileSummary.length > 0) elements.push(fileSummaryPanel(state));
|
|
1271
1354
|
|
|
1272
1355
|
if (state.approval) {
|
|
@@ -1407,6 +1490,7 @@ function renderActivities(
|
|
|
1407
1490
|
|
|
1408
1491
|
type GroupedTurnActivity =
|
|
1409
1492
|
| { kind: "activity"; activity: TurnActivity }
|
|
1493
|
+
| { kind: "gap"; id: string }
|
|
1410
1494
|
| {
|
|
1411
1495
|
kind: "execution";
|
|
1412
1496
|
id: string;
|
|
@@ -1424,6 +1508,7 @@ function renderGroupedActivityGroups(
|
|
|
1424
1508
|
} = {},
|
|
1425
1509
|
): Record<string, unknown>[] {
|
|
1426
1510
|
return groups.flatMap((group) => {
|
|
1511
|
+
if (group.kind === "gap") return [markdown("…")];
|
|
1427
1512
|
if (group.kind === "activity") {
|
|
1428
1513
|
return renderActivity(group.activity, projectCwd, options.fullActivityText === true);
|
|
1429
1514
|
}
|
|
@@ -1500,7 +1585,7 @@ function executionActivityPanel(
|
|
|
1500
1585
|
{
|
|
1501
1586
|
elementId: executionActivityPanelElementId(group.id),
|
|
1502
1587
|
expanded: false,
|
|
1503
|
-
borderColor: status === "
|
|
1588
|
+
borderColor: status === "running" ? "blue" : "grey",
|
|
1504
1589
|
compact: true,
|
|
1505
1590
|
},
|
|
1506
1591
|
);
|
|
@@ -1508,7 +1593,6 @@ function executionActivityPanel(
|
|
|
1508
1593
|
|
|
1509
1594
|
function executionActivityStatus(tools: ToolState[]): ToolState["status"] {
|
|
1510
1595
|
if (tools.some((tool) => tool.status === "running")) return "running";
|
|
1511
|
-
if (tools.some((tool) => tool.status === "failed")) return "failed";
|
|
1512
1596
|
return "completed";
|
|
1513
1597
|
}
|
|
1514
1598
|
|
|
@@ -1516,7 +1600,7 @@ function executionActivityTitle(
|
|
|
1516
1600
|
group: Extract<GroupedTurnActivity, { kind: "execution" }>,
|
|
1517
1601
|
status: ToolState["status"],
|
|
1518
1602
|
): string {
|
|
1519
|
-
const icon = status === "running" ? "⏳" :
|
|
1603
|
+
const icon = status === "running" ? "⏳" : "💭";
|
|
1520
1604
|
const latestReasoning = group.latestReasoning?.text
|
|
1521
1605
|
? removeMarkdownBold(group.latestReasoning.text).replace(/\s+/g, " ").trim()
|
|
1522
1606
|
: "";
|
|
@@ -1524,9 +1608,7 @@ function executionActivityTitle(
|
|
|
1524
1608
|
? truncateText(latestReasoning, 90)
|
|
1525
1609
|
: status === "running"
|
|
1526
1610
|
? "正在执行工具"
|
|
1527
|
-
:
|
|
1528
|
-
? "工具执行失败"
|
|
1529
|
-
: "已执行工具";
|
|
1611
|
+
: "已执行工具";
|
|
1530
1612
|
return `${icon} ${summary} · ${group.tools.length} 个工具`;
|
|
1531
1613
|
}
|
|
1532
1614
|
|
|
@@ -1559,6 +1641,7 @@ const ACTIVITIES_PER_PAGE = 40;
|
|
|
1559
1641
|
const GROUPED_TOOLS_PER_PANEL = 8;
|
|
1560
1642
|
const GROUPED_PAGE_ACTIVITY_BYTES = 24 * 1024;
|
|
1561
1643
|
const GROUPED_PAGE_ACTIVITY_COMPONENTS = 160;
|
|
1644
|
+
const PINNED_LIVE_COMMENTARIES = 3;
|
|
1562
1645
|
const MAX_LIVE_ASSISTANT_TEXT = 2_000;
|
|
1563
1646
|
const ACTIVITY_TEXT_CHUNK = 2_500;
|
|
1564
1647
|
|
|
@@ -1583,6 +1666,17 @@ function groupedActivityPages(
|
|
|
1583
1666
|
} = {},
|
|
1584
1667
|
): GroupedTurnActivity[][] {
|
|
1585
1668
|
const groups = groupTurnActivities(activities).flatMap(splitGroupedExecutionActivity);
|
|
1669
|
+
return groupedActivityPagesFromGroups(groups, projectCwd, renderOptions);
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
function groupedActivityPagesFromGroups(
|
|
1673
|
+
groups: GroupedTurnActivity[],
|
|
1674
|
+
projectCwd?: string,
|
|
1675
|
+
renderOptions: {
|
|
1676
|
+
fullActivityText?: boolean;
|
|
1677
|
+
includeReasoningHistory?: boolean;
|
|
1678
|
+
} = {},
|
|
1679
|
+
): GroupedTurnActivity[][] {
|
|
1586
1680
|
if (groups.length === 0) return [];
|
|
1587
1681
|
|
|
1588
1682
|
const newestFirst: GroupedTurnActivity[][] = [];
|
|
@@ -1613,6 +1707,82 @@ function groupedActivityPageFits(
|
|
|
1613
1707
|
&& countCardComponents(elements) <= GROUPED_PAGE_ACTIVITY_COMPONENTS;
|
|
1614
1708
|
}
|
|
1615
1709
|
|
|
1710
|
+
interface LiveGroupedActivityPage {
|
|
1711
|
+
groups: GroupedTurnActivity[];
|
|
1712
|
+
historyGroups: GroupedTurnActivity[];
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
function groupedLiveActivityPage(
|
|
1716
|
+
pages: GroupedTurnActivity[][],
|
|
1717
|
+
projectCwd?: string,
|
|
1718
|
+
): LiveGroupedActivityPage {
|
|
1719
|
+
const latest = pages.at(-1) ?? [];
|
|
1720
|
+
if (pages.length <= 1) return { groups: latest, historyGroups: [] };
|
|
1721
|
+
|
|
1722
|
+
const groups = pages.flat();
|
|
1723
|
+
const latestStart = groups.length - latest.length;
|
|
1724
|
+
const commentaryIndexes = groups
|
|
1725
|
+
.map((group, index) => isCommentaryGroup(group) ? index : -1)
|
|
1726
|
+
.filter((index) => index >= 0);
|
|
1727
|
+
if (commentaryIndexes.length === 0) {
|
|
1728
|
+
return { groups: latest, historyGroups: groups.slice(0, latestStart) };
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
const selected = new Set<number>();
|
|
1732
|
+
const pinnedCommentaries = commentaryIndexes.slice(0, PINNED_LIVE_COMMENTARIES);
|
|
1733
|
+
for (const index of pinnedCommentaries) selected.add(index);
|
|
1734
|
+
const latestCommentary = commentaryIndexes.at(-1)!;
|
|
1735
|
+
for (let index = latestCommentary; index < groups.length; index += 1) selected.add(index);
|
|
1736
|
+
|
|
1737
|
+
const renderSelection = (selection: ReadonlySet<number>): LiveGroupedActivityPage => ({
|
|
1738
|
+
groups: groupsWithGaps(groups, selection),
|
|
1739
|
+
historyGroups: groups.filter((_group, index) => index < latestCommentary || !selection.has(index)),
|
|
1740
|
+
});
|
|
1741
|
+
const fits = (selection: ReadonlySet<number>): boolean => {
|
|
1742
|
+
const view = renderSelection(selection);
|
|
1743
|
+
return groupedActivityPageFits(view.groups, projectCwd, {});
|
|
1744
|
+
};
|
|
1745
|
+
|
|
1746
|
+
if (!fits(selected)) {
|
|
1747
|
+
const latestExecutions = [...selected]
|
|
1748
|
+
.filter((index) => index > latestCommentary && groups[index]?.kind === "execution")
|
|
1749
|
+
.sort((left, right) => left - right);
|
|
1750
|
+
const newestExecution = latestExecutions.at(-1);
|
|
1751
|
+
for (const index of latestExecutions) {
|
|
1752
|
+
if (fits(selected) || index === newestExecution) break;
|
|
1753
|
+
selected.delete(index);
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
return renderSelection(selected);
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
function isCommentaryGroup(group: GroupedTurnActivity): boolean {
|
|
1761
|
+
return group.kind === "activity" && isCommentaryActivity(group.activity);
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
function groupsWithGaps(
|
|
1765
|
+
groups: GroupedTurnActivity[],
|
|
1766
|
+
selected: ReadonlySet<number>,
|
|
1767
|
+
): GroupedTurnActivity[] {
|
|
1768
|
+
const indexes = [...selected].sort((left, right) => left - right);
|
|
1769
|
+
const visible: GroupedTurnActivity[] = [];
|
|
1770
|
+
const append = (group: GroupedTurnActivity): void => {
|
|
1771
|
+
if (group.kind === "gap" && visible.at(-1)?.kind === "gap") return;
|
|
1772
|
+
visible.push(group);
|
|
1773
|
+
};
|
|
1774
|
+
let previous = -1;
|
|
1775
|
+
for (const index of indexes) {
|
|
1776
|
+
if (previous >= 0 && index > previous + 1) {
|
|
1777
|
+
append({ kind: "gap", id: `gap:${previous}:${index}` });
|
|
1778
|
+
}
|
|
1779
|
+
const group = groups[index];
|
|
1780
|
+
if (group) append(group);
|
|
1781
|
+
previous = index;
|
|
1782
|
+
}
|
|
1783
|
+
return visible;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1616
1786
|
function countCardComponents(value: unknown): number {
|
|
1617
1787
|
if (Array.isArray(value)) {
|
|
1618
1788
|
return value.reduce((total, entry) => total + countCardComponents(entry), 0);
|
|
@@ -1791,13 +1961,22 @@ function renderToolDetails(tool: ToolState, projectCwd?: string): string {
|
|
|
1791
1961
|
const result = tool.error ?? tool.output ?? fileSummary;
|
|
1792
1962
|
const normalizedCommand = stripAnsi(command).trim();
|
|
1793
1963
|
const displayCommand = tool.kind === "command"
|
|
1794
|
-
?
|
|
1964
|
+
? unwrapShellCommand(normalizedCommand) ?? normalizedCommand
|
|
1795
1965
|
: normalizedCommand;
|
|
1796
|
-
const commandText = truncateText(displayCommand,
|
|
1797
|
-
const resultText = result ?
|
|
1966
|
+
const commandText = truncateText(displayCommand, 600);
|
|
1967
|
+
const resultText = result ? truncateToolResult(stripAnsi(result).trim()) : undefined;
|
|
1798
1968
|
return codeBlock([`$ ${commandText}`, resultText].filter((part): part is string => part !== undefined).join("\n"), 2_003);
|
|
1799
1969
|
}
|
|
1800
1970
|
|
|
1971
|
+
function truncateToolResult(result: string): string {
|
|
1972
|
+
const maxLength = 900;
|
|
1973
|
+
const marker = "\n...\n";
|
|
1974
|
+
const tailLength = 600;
|
|
1975
|
+
if (result.length <= maxLength) return result;
|
|
1976
|
+
const headLength = maxLength - marker.length - tailLength;
|
|
1977
|
+
return `${result.slice(0, headLength)}${marker}${result.slice(-tailLength)}`;
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1801
1980
|
function displayFilePath(filePath: string, projectCwd?: string): string {
|
|
1802
1981
|
if (!projectCwd) return filePath;
|
|
1803
1982
|
const pathApi = usesWindowsPaths(projectCwd, filePath) ? path.win32 : path;
|
|
@@ -1826,7 +2005,7 @@ function toolPanelTitle(tool: ToolState): string {
|
|
|
1826
2005
|
const command = stripAnsi(tool.command ?? tool.title).trim();
|
|
1827
2006
|
const meaningfulCommand = tool.kind === "web_search"
|
|
1828
2007
|
? tool.title
|
|
1829
|
-
:
|
|
2008
|
+
: unwrapShellCommand(command) ?? tool.title;
|
|
1830
2009
|
const duration = toolDuration(tool);
|
|
1831
2010
|
const prefix = `${icon} `;
|
|
1832
2011
|
const suffix = duration ? ` · ${duration}` : "";
|
|
@@ -1875,6 +2054,10 @@ function formatTurnDuration(durationMs: number): string {
|
|
|
1875
2054
|
return `${seconds}s`;
|
|
1876
2055
|
}
|
|
1877
2056
|
|
|
2057
|
+
function unwrapShellCommand(command: string): string | undefined {
|
|
2058
|
+
return unwrapPowerShellCommand(command) ?? unwrapPosixShellCommand(command);
|
|
2059
|
+
}
|
|
2060
|
+
|
|
1878
2061
|
function unwrapPowerShellCommand(command: string): string | undefined {
|
|
1879
2062
|
const executable = command.match(
|
|
1880
2063
|
/^(?:"[^"]*(?:pwsh|powershell)\.exe"|(?:\S*[\\/])?(?:pwsh|powershell)(?:\.exe)?)(?=\s|$)/i,
|
|
@@ -1885,6 +2068,71 @@ function unwrapPowerShellCommand(command: string): string | undefined {
|
|
|
1885
2068
|
if (!commandFlag) return undefined;
|
|
1886
2069
|
const payload = args.slice(commandFlag.index + commandFlag[0].length).trim();
|
|
1887
2070
|
if (!payload) return undefined;
|
|
2071
|
+
return unwrapQuotedPayload(payload);
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
function unwrapPosixShellCommand(command: string): string | undefined {
|
|
2075
|
+
let executable = readCommandWord(command, 0);
|
|
2076
|
+
if (!executable) return undefined;
|
|
2077
|
+
|
|
2078
|
+
if (posixExecutableName(executable.value) === "env") {
|
|
2079
|
+
let cursor = executable.end;
|
|
2080
|
+
while (true) {
|
|
2081
|
+
const candidate = readCommandWord(command, cursor);
|
|
2082
|
+
if (!candidate) return undefined;
|
|
2083
|
+
const name = posixExecutableName(candidate.value);
|
|
2084
|
+
if (isPosixShell(name)) {
|
|
2085
|
+
executable = candidate;
|
|
2086
|
+
break;
|
|
2087
|
+
}
|
|
2088
|
+
if (!candidate.value.startsWith("-") && !/^[A-Za-z_][A-Za-z0-9_]*=/.test(candidate.value)) {
|
|
2089
|
+
return undefined;
|
|
2090
|
+
}
|
|
2091
|
+
cursor = candidate.end;
|
|
2092
|
+
}
|
|
2093
|
+
} else if (!isPosixShell(posixExecutableName(executable.value))) {
|
|
2094
|
+
return undefined;
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
const args = command.slice(executable.end);
|
|
2098
|
+
const commandFlag = /(?:^|\s)(?:-[A-Za-z]*c[A-Za-z]*|--command)(?:\s+|=)/.exec(args);
|
|
2099
|
+
if (!commandFlag) return undefined;
|
|
2100
|
+
const payload = args.slice(commandFlag.index + commandFlag[0].length).trim();
|
|
2101
|
+
if (!payload) return undefined;
|
|
2102
|
+
return unwrapQuotedPayload(payload);
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
function readCommandWord(command: string, start: number): { value: string; end: number } | undefined {
|
|
2106
|
+
let cursor = start;
|
|
2107
|
+
while (cursor < command.length && /\s/.test(command[cursor] ?? "")) cursor += 1;
|
|
2108
|
+
if (cursor >= command.length) return undefined;
|
|
2109
|
+
|
|
2110
|
+
const quote = command[cursor];
|
|
2111
|
+
if (quote === "\"" || quote === "'") {
|
|
2112
|
+
const endQuote = command.indexOf(quote, cursor + 1);
|
|
2113
|
+
if (endQuote < 0) return undefined;
|
|
2114
|
+
return { value: command.slice(cursor + 1, endQuote), end: endQuote + 1 };
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
const word = /^\S+/.exec(command.slice(cursor));
|
|
2118
|
+
if (!word) return undefined;
|
|
2119
|
+
return { value: word[0], end: cursor + word[0].length };
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
function posixExecutableName(executable: string): string {
|
|
2123
|
+
return executable.replaceAll("\\", "/").split("/").at(-1)?.toLowerCase() ?? "";
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
function isPosixShell(executable: string): boolean {
|
|
2127
|
+
return executable === "sh"
|
|
2128
|
+
|| executable === "bash"
|
|
2129
|
+
|| executable === "zsh"
|
|
2130
|
+
|| executable === "dash"
|
|
2131
|
+
|| executable === "ksh"
|
|
2132
|
+
|| executable === "fish";
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
function unwrapQuotedPayload(payload: string): string {
|
|
1888
2136
|
const quote = payload[0];
|
|
1889
2137
|
if ((quote === "\"" || quote === "'") && payload.at(-1) === quote) return payload.slice(1, -1).trim();
|
|
1890
2138
|
return payload;
|
|
@@ -2316,10 +2564,12 @@ function turnCard(
|
|
|
2316
2564
|
tag: "plain_text",
|
|
2317
2565
|
content: title,
|
|
2318
2566
|
},
|
|
2319
|
-
subtitle
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2567
|
+
...(subtitle ? {
|
|
2568
|
+
subtitle: {
|
|
2569
|
+
tag: "plain_text",
|
|
2570
|
+
content: subtitle,
|
|
2571
|
+
},
|
|
2572
|
+
} : {}),
|
|
2323
2573
|
padding: "12px 12px 12px 12px",
|
|
2324
2574
|
},
|
|
2325
2575
|
body: {
|
|
@@ -2360,6 +2610,34 @@ function sectionCard(
|
|
|
2360
2610
|
};
|
|
2361
2611
|
}
|
|
2362
2612
|
|
|
2613
|
+
function compactCard(
|
|
2614
|
+
title: string,
|
|
2615
|
+
template: string,
|
|
2616
|
+
elements: Record<string, unknown>[],
|
|
2617
|
+
): Record<string, unknown> {
|
|
2618
|
+
return {
|
|
2619
|
+
schema: "2.0",
|
|
2620
|
+
config: {
|
|
2621
|
+
update_multi: true,
|
|
2622
|
+
width_mode: "compact",
|
|
2623
|
+
},
|
|
2624
|
+
header: {
|
|
2625
|
+
template,
|
|
2626
|
+
title: {
|
|
2627
|
+
tag: "plain_text",
|
|
2628
|
+
content: title,
|
|
2629
|
+
},
|
|
2630
|
+
padding: "12px 12px 12px 12px",
|
|
2631
|
+
},
|
|
2632
|
+
body: {
|
|
2633
|
+
direction: "vertical",
|
|
2634
|
+
vertical_spacing: "12px",
|
|
2635
|
+
padding: "12px 12px 12px 12px",
|
|
2636
|
+
elements,
|
|
2637
|
+
},
|
|
2638
|
+
};
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2363
2641
|
function formatUpdate(update: Record<string, JsonValue>): string {
|
|
2364
2642
|
const updateType = update.sessionUpdate;
|
|
2365
2643
|
if (updateType === "agent_message_chunk" && isObject(update.content)) {
|
|
@@ -6,6 +6,8 @@ interface FenceState {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
const FENCE_OPENER = /^([ \t]*)((?:`{3,})|(?:~{3,}))(.*)$/;
|
|
9
|
+
const MARKDOWN_LINK = /(!?)\[([^\]\r\n]*)\]\((<?[^)\r\n]+>?)\)/g;
|
|
10
|
+
const LOCAL_FILE_LINE_REFERENCE = /(:\d+(?::\d+)?)$/;
|
|
9
11
|
|
|
10
12
|
export function normalizeFeishuMarkdown(markdown: string): string {
|
|
11
13
|
const lines = markdown.match(/[^\r\n]*(?:\r\n|\n)|[^\r\n]+$/g) ?? [];
|
|
@@ -17,7 +19,7 @@ export function normalizeFeishuMarkdown(markdown: string): string {
|
|
|
17
19
|
|
|
18
20
|
if (!fence) {
|
|
19
21
|
const opener = FENCE_OPENER.exec(body);
|
|
20
|
-
if (!opener?.[2]) return
|
|
22
|
+
if (!opener?.[2]) return `${includeLocalFileLineReferences(body)}${ending}`;
|
|
21
23
|
|
|
22
24
|
const indent = opener[1] ?? "";
|
|
23
25
|
const delimiter = opener[2];
|
|
@@ -38,6 +40,30 @@ export function normalizeFeishuMarkdown(markdown: string): string {
|
|
|
38
40
|
}).join("");
|
|
39
41
|
}
|
|
40
42
|
|
|
43
|
+
function includeLocalFileLineReferences(markdown: string): string {
|
|
44
|
+
return markdown.replace(MARKDOWN_LINK, (link, imageMarker: string, label: string, rawTarget: string) => {
|
|
45
|
+
if (imageMarker) return link;
|
|
46
|
+
|
|
47
|
+
const target = unwrapMarkdownTarget(rawTarget.trim());
|
|
48
|
+
if (!isLocalFileTarget(target)) return link;
|
|
49
|
+
|
|
50
|
+
const reference = LOCAL_FILE_LINE_REFERENCE.exec(target)?.[1];
|
|
51
|
+
if (!reference || label.endsWith(reference)) return link;
|
|
52
|
+
return `[${label}${reference}](${rawTarget})`;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function unwrapMarkdownTarget(target: string): string {
|
|
57
|
+
return target.startsWith("<") && target.endsWith(">") ? target.slice(1, -1) : target;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isLocalFileTarget(target: string): boolean {
|
|
61
|
+
return /^file:\/\//i.test(target)
|
|
62
|
+
|| /^\/?[a-z]:[\\/]/i.test(target)
|
|
63
|
+
|| /^\\\\/.test(target)
|
|
64
|
+
|| /^\/(?!\/)/.test(target);
|
|
65
|
+
}
|
|
66
|
+
|
|
41
67
|
function isFenceCloser(line: string, fence: FenceState): boolean {
|
|
42
68
|
const match = /^[ \t]*(`+|~+)[ \t]*$/.exec(line);
|
|
43
69
|
const delimiter = match?.[1];
|