@keyou007/agent-bot 0.1.24-alpha.1 → 0.1.24-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 +94 -0
- package/README.md +33 -7
- package/README.zh.md +32 -7
- package/dist/acp/AcpJsonRpcConnection.js +3 -1
- package/dist/acp/AcpJsonRpcConnection.js.map +1 -1
- package/dist/cli/SelfUpdateRunner.js +2 -0
- package/dist/cli/SelfUpdateRunner.js.map +1 -1
- package/dist/cli/SelfUpdater.js +2 -0
- package/dist/cli/SelfUpdater.js.map +1 -1
- package/dist/codex/AppServerConnection.js +3 -1
- package/dist/codex/AppServerConnection.js.map +1 -1
- package/dist/codex/CodexEventMapper.js +41 -3
- package/dist/codex/CodexEventMapper.js.map +1 -1
- package/dist/codex/CodexRuntime.js +95 -3
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/commands/CommandRouter.js +5 -0
- package/dist/commands/CommandRouter.js.map +1 -1
- package/dist/feishu/CardRenderer.js +49 -23
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +2 -0
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/feishu/FeishuMarkdown.js +70 -4
- package/dist/feishu/FeishuMarkdown.js.map +1 -1
- package/dist/feishu/FeishuMessageClient.js +44 -0
- package/dist/feishu/FeishuMessageClient.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +114 -37
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/feishu/RecentMessageContext.js +70 -0
- package/dist/feishu/RecentMessageContext.js.map +1 -0
- package/dist/index.js +62 -24
- package/dist/index.js.map +1 -1
- package/dist/local-files/DiagramPreview.js +171 -0
- package/dist/local-files/DiagramPreview.js.map +1 -0
- package/dist/local-files/LocalFileViewerServer.js +141 -3
- package/dist/local-files/LocalFileViewerServer.js.map +1 -1
- package/dist/local-files/PreviewSyntaxHighlight.js +57 -0
- package/dist/local-files/PreviewSyntaxHighlight.js.map +1 -0
- package/dist/local-files/TurnPreviewPage.js +499 -60
- package/dist/local-files/TurnPreviewPage.js.map +1 -1
- package/dist/presentation/OutboundRouter.js +18 -9
- package/dist/presentation/OutboundRouter.js.map +1 -1
- package/dist/presentation/TurnStateReducer.js +157 -8
- package/dist/presentation/TurnStateReducer.js.map +1 -1
- package/dist/presentation/turnReasoning.js +23 -0
- package/dist/presentation/turnReasoning.js.map +1 -0
- package/dist/proxy/ProxySessionController.js +461 -49
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/state/StateStore.js +43 -2
- package/dist/state/StateStore.js.map +1 -1
- package/dist/state/TurnPreviewJournal.js +469 -0
- package/dist/state/TurnPreviewJournal.js.map +1 -0
- package/dist/state/migrations.js +10 -0
- package/dist/state/migrations.js.map +1 -1
- package/dist/updates/AutomaticUpdatePreparer.js +16 -7
- package/dist/updates/AutomaticUpdatePreparer.js.map +1 -1
- package/dist/updates/ManualUpdateController.js +102 -0
- package/dist/updates/ManualUpdateController.js.map +1 -0
- package/dist/updates/PublishedRelease.js +42 -4
- package/dist/updates/PublishedRelease.js.map +1 -1
- package/dist/utils/ProtocolLineWarning.js +15 -0
- package/dist/utils/ProtocolLineWarning.js.map +1 -0
- package/docs/technical-reference.md +21 -1
- package/docs/technical-reference.zh.md +21 -1
- package/npm-shrinkwrap.json +1401 -10
- package/package.json +3 -1
- package/skills/agent-bot/SKILL.md +9 -2
- package/src/acp/AcpJsonRpcConnection.ts +3 -1
- package/src/cli/SelfUpdateRunner.ts +3 -0
- package/src/cli/SelfUpdater.ts +2 -0
- package/src/codex/AppServerConnection.ts +3 -1
- package/src/codex/CodexEventMapper.ts +40 -3
- package/src/codex/CodexRuntime.ts +91 -4
- package/src/commands/CommandRouter.ts +4 -0
- package/src/commands/commandTypes.ts +1 -0
- package/src/feishu/CardRenderer.ts +47 -21
- package/src/feishu/FeishuConnector.ts +2 -0
- package/src/feishu/FeishuMarkdown.ts +69 -3
- package/src/feishu/FeishuMessageClient.ts +44 -0
- package/src/feishu/FeishuTurnPresenter.ts +107 -31
- package/src/feishu/RecentMessageContext.ts +90 -0
- package/src/feishu/types.ts +5 -0
- package/src/index.ts +62 -22
- package/src/local-files/DiagramPreview.ts +173 -0
- package/src/local-files/LocalFileViewerServer.ts +129 -2
- package/src/local-files/PreviewSyntaxHighlight.ts +58 -0
- package/src/local-files/TurnPreviewPage.ts +539 -57
- package/src/presentation/OutboundRouter.ts +25 -7
- package/src/presentation/TurnStateReducer.ts +153 -9
- package/src/presentation/turnReasoning.ts +21 -0
- package/src/presentation/turnViewTypes.ts +19 -1
- package/src/proxy/ProxySessionController.ts +453 -40
- package/src/runtime/types.ts +21 -0
- package/src/state/StateStore.ts +45 -2
- package/src/state/TurnPreviewJournal.ts +398 -0
- package/src/state/migrations.ts +10 -0
- package/src/updates/AutomaticUpdatePreparer.ts +21 -7
- package/src/updates/ManualUpdateController.ts +116 -0
- package/src/updates/PublishedRelease.ts +49 -3
- package/src/utils/ProtocolLineWarning.ts +13 -0
|
@@ -1,13 +1,19 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import path from "node:path";
|
|
3
|
+
import { enableDiagramFences, DIAGRAM_PREVIEW_CLIENT_SCRIPT, DIAGRAM_PREVIEW_CSS } from "./DiagramPreview.js";
|
|
2
4
|
import MarkdownIt from "markdown-it";
|
|
5
|
+
import { highlightPreviewCode, PREVIEW_SYNTAX_CSS } from "./PreviewSyntaxHighlight.js";
|
|
3
6
|
import { displayFilePath, displayToolCommand, formatShellCommandForDisplay, toolStatusIcon } from "../feishu/CardRenderer.js";
|
|
7
|
+
import { turnReasoningItems } from "../presentation/turnReasoning.js";
|
|
4
8
|
import { isFileUrl, rewriteMarkdownFileLinks } from "./MarkdownFileLinks.js";
|
|
5
9
|
const MARKDOWN = new MarkdownIt({
|
|
6
10
|
breaks: true,
|
|
7
11
|
html: false,
|
|
8
12
|
linkify: true,
|
|
9
13
|
typographer: false,
|
|
14
|
+
highlight: highlightPreviewCode,
|
|
10
15
|
});
|
|
16
|
+
enableDiagramFences(MARKDOWN);
|
|
11
17
|
const validateLink = MARKDOWN.validateLink.bind(MARKDOWN);
|
|
12
18
|
MARKDOWN.validateLink = (url) => isFileUrl(url) || validateLink(url);
|
|
13
19
|
const defaultLinkOpen = MARKDOWN.renderer.rules.link_open
|
|
@@ -27,6 +33,7 @@ const PREVIEW_LABELS = {
|
|
|
27
33
|
unknown: "未知",
|
|
28
34
|
plan: "计划",
|
|
29
35
|
error: "错误",
|
|
36
|
+
reasoning: "思考",
|
|
30
37
|
finalAnswer: "最终回答",
|
|
31
38
|
generating: "回答生成中",
|
|
32
39
|
command: "命令",
|
|
@@ -41,7 +48,6 @@ const PREVIEW_LABELS = {
|
|
|
41
48
|
tools: "个工具",
|
|
42
49
|
turnTokens: "本轮",
|
|
43
50
|
totalTokens: "总计",
|
|
44
|
-
cachedTokens: "缓存命中",
|
|
45
51
|
nonCachedTokens: "非缓存",
|
|
46
52
|
compactionTokens: "压缩",
|
|
47
53
|
compactionAfterTokens: "压缩后",
|
|
@@ -68,6 +74,7 @@ const PREVIEW_LABELS = {
|
|
|
68
74
|
unknown: "Unknown",
|
|
69
75
|
plan: "Plan",
|
|
70
76
|
error: "Error",
|
|
77
|
+
reasoning: "Reasoning",
|
|
71
78
|
finalAnswer: "Final answer",
|
|
72
79
|
generating: "Generating answer",
|
|
73
80
|
command: "Command",
|
|
@@ -82,7 +89,6 @@ const PREVIEW_LABELS = {
|
|
|
82
89
|
tools: "tools",
|
|
83
90
|
turnTokens: "Turn",
|
|
84
91
|
totalTokens: "Total",
|
|
85
|
-
cachedTokens: "Cache hit",
|
|
86
92
|
nonCachedTokens: "Non-cached",
|
|
87
93
|
compactionTokens: "Compaction",
|
|
88
94
|
compactionAfterTokens: "After compaction",
|
|
@@ -118,13 +124,13 @@ export function detectTurnPreviewLanguage(acceptLanguage) {
|
|
|
118
124
|
.sort((left, right) => right.quality - left.quality || left.index - right.index);
|
|
119
125
|
return /^zh(?:[-_]|$)/iu.test(candidates[0]?.tag ?? "") ? "zh" : "en";
|
|
120
126
|
}
|
|
121
|
-
export const TURN_PREVIEW_CLIENT_SCRIPT = `(() => {
|
|
127
|
+
export const TURN_PREVIEW_CLIENT_SCRIPT = DIAGRAM_PREVIEW_CLIENT_SCRIPT + `(() => {
|
|
122
128
|
const eventsUrl = document.body.dataset.eventsUrl;
|
|
123
129
|
const content = document.getElementById("turn-content");
|
|
124
130
|
const metadata = document.getElementById("turn-metadata");
|
|
125
131
|
const status = document.getElementById("turn-status");
|
|
126
132
|
const live = document.getElementById("turn-live");
|
|
127
|
-
if (!eventsUrl || !content || !metadata || !status || !live
|
|
133
|
+
if (!eventsUrl || !content || !metadata || !status || !live) return;
|
|
128
134
|
|
|
129
135
|
const isChinese = /^zh/i.test(document.documentElement.lang);
|
|
130
136
|
const labels = {
|
|
@@ -133,9 +139,13 @@ export const TURN_PREVIEW_CLIENT_SCRIPT = `(() => {
|
|
|
133
139
|
waitingForUpdates: isChinese ? "等待更新" : "Waiting for updates",
|
|
134
140
|
unavailable: isChinese ? "暂时不可用" : "Temporarily unavailable",
|
|
135
141
|
reconnecting: isChinese ? "正在重连" : "Reconnecting",
|
|
142
|
+
loading: isChinese ? "正在加载…" : "Loading…",
|
|
143
|
+
loadFailed: isChinese ? "加载失败,请重试。" : "Could not load details.",
|
|
144
|
+
retry: isChinese ? "重试" : "Retry",
|
|
136
145
|
};
|
|
137
146
|
|
|
138
147
|
let source;
|
|
148
|
+
const terminalAtLoad = document.body.dataset.terminal === "true";
|
|
139
149
|
const updateElapsed = () => {
|
|
140
150
|
document.querySelectorAll("[data-live-elapsed]").forEach((element) => {
|
|
141
151
|
const startedAt = Number(element.dataset.startedAt);
|
|
@@ -169,41 +179,284 @@ export const TURN_PREVIEW_CLIENT_SCRIPT = `(() => {
|
|
|
169
179
|
});
|
|
170
180
|
};
|
|
171
181
|
updateElapsed();
|
|
172
|
-
setInterval(updateElapsed, 1000);
|
|
182
|
+
const clock = terminalAtLoad ? undefined : setInterval(updateElapsed, 1000);
|
|
173
183
|
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
const
|
|
184
|
+
const nodeKey = (node) => {
|
|
185
|
+
if (node.nodeType !== 1) return undefined;
|
|
186
|
+
for (const attribute of ["data-preview-key", "data-activity", "data-activity-id", "data-scroll-id", "data-reasoning-field", "data-reasoning-part"]) {
|
|
187
|
+
if (node.hasAttribute(attribute)) return attribute + ":" + node.getAttribute(attribute);
|
|
188
|
+
}
|
|
189
|
+
if (node.tagName === "IMG") return "image:" + node.getAttribute("src");
|
|
190
|
+
// Keep Markdown image paragraphs/links anchored when text is inserted before them.
|
|
191
|
+
if (["P", "A", "FIGURE", "PICTURE"].includes(node.tagName)) {
|
|
192
|
+
const image = node.querySelector("img");
|
|
193
|
+
if (image) return node.tagName + ":image:" + image.getAttribute("src");
|
|
194
|
+
}
|
|
195
|
+
for (const name of ["markdown", "tool-command", "tool-image", "files", "tool-header", "tool-body", "tool-footer"]) {
|
|
196
|
+
if (node.classList.contains(name)) return node.tagName + ":" + name;
|
|
197
|
+
}
|
|
198
|
+
return undefined;
|
|
199
|
+
};
|
|
200
|
+
const compatible = (current, next) => current.nodeType === next.nodeType && current.nodeName === next.nodeName;
|
|
201
|
+
const patchNode = (current, next) => {
|
|
202
|
+
// Summary snapshots never own a detail body that has already been fetched.
|
|
203
|
+
if (current.nodeType === 1 && current.hasAttribute("data-detail-body") && next.hasAttribute("data-detail-body")) return;
|
|
204
|
+
if (current.isEqualNode(next)) return;
|
|
205
|
+
if (current.nodeType !== 1) {
|
|
206
|
+
if (current.nodeValue !== next.nodeValue) current.nodeValue = next.nodeValue;
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (current.hasAttribute("data-diagram") && next.hasAttribute("data-diagram")) {
|
|
210
|
+
const code = current.querySelector(".diagram-source code");
|
|
211
|
+
const nextCode = next.querySelector(".diagram-source code");
|
|
212
|
+
if (code && nextCode && code.textContent !== nextCode.textContent) {
|
|
213
|
+
code.textContent = nextCode.textContent;
|
|
214
|
+
window.agentBotDiagrams.refresh(current);
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const preserveOpen = current.tagName === "DETAILS";
|
|
219
|
+
for (const attribute of Array.from(current.attributes)) {
|
|
220
|
+
if (preserveOpen && attribute.name === "open") continue;
|
|
221
|
+
if (!next.hasAttribute(attribute.name)) current.removeAttribute(attribute.name);
|
|
222
|
+
}
|
|
223
|
+
for (const attribute of Array.from(next.attributes)) {
|
|
224
|
+
if (preserveOpen && attribute.name === "open") continue;
|
|
225
|
+
if (current.getAttribute(attribute.name) !== attribute.value) current.setAttribute(attribute.name, attribute.value);
|
|
226
|
+
}
|
|
227
|
+
patchChildren(current, next);
|
|
228
|
+
};
|
|
229
|
+
const patchChildren = (parent, next) => {
|
|
230
|
+
const nextChildren = Array.from(next.childNodes);
|
|
231
|
+
const nextKeys = new Set(nextChildren.map(nodeKey).filter((key) => key !== undefined));
|
|
232
|
+
const remaining = new Set(Array.from(parent.childNodes));
|
|
233
|
+
const keyed = new Map();
|
|
234
|
+
for (const child of remaining) {
|
|
235
|
+
const key = nodeKey(child);
|
|
236
|
+
if (key === undefined) continue;
|
|
237
|
+
if (!nextKeys.has(key)) {
|
|
238
|
+
child.remove();
|
|
239
|
+
remaining.delete(child);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const matches = keyed.get(key) || [];
|
|
243
|
+
matches.push(child);
|
|
244
|
+
keyed.set(key, matches);
|
|
245
|
+
}
|
|
246
|
+
let cursor = parent.firstChild;
|
|
247
|
+
for (const child of nextChildren) {
|
|
248
|
+
const key = nodeKey(child);
|
|
249
|
+
const matches = key === undefined ? undefined : keyed.get(key);
|
|
250
|
+
const current = key === undefined
|
|
251
|
+
? cursor && nodeKey(cursor) === undefined && compatible(cursor, child) ? cursor : undefined
|
|
252
|
+
: matches?.find((candidate) => remaining.has(candidate) && compatible(candidate, child));
|
|
253
|
+
if (current) {
|
|
254
|
+
remaining.delete(current);
|
|
255
|
+
// Drop obsolete text/markup before a retained block without detaching that block.
|
|
256
|
+
while (cursor && cursor !== current && nodeKey(cursor) === undefined) {
|
|
257
|
+
const obsolete = cursor;
|
|
258
|
+
cursor = cursor.nextSibling;
|
|
259
|
+
remaining.delete(obsolete);
|
|
260
|
+
obsolete.remove();
|
|
261
|
+
}
|
|
262
|
+
if (current !== cursor) {
|
|
263
|
+
if (typeof parent.moveBefore === "function" && current.isConnected) parent.moveBefore(current, cursor);
|
|
264
|
+
else parent.insertBefore(current, cursor);
|
|
265
|
+
}
|
|
266
|
+
patchNode(current, child);
|
|
267
|
+
cursor = current.nextSibling;
|
|
268
|
+
} else {
|
|
269
|
+
const added = child.cloneNode(true);
|
|
270
|
+
parent.insertBefore(added, cursor);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
for (const child of remaining) child.remove();
|
|
274
|
+
};
|
|
275
|
+
const patchMarkup = (element, markup) => {
|
|
276
|
+
const template = document.createElement("template");
|
|
277
|
+
template.innerHTML = markup;
|
|
278
|
+
patchChildren(element, template.content);
|
|
279
|
+
};
|
|
280
|
+
const detailStates = new WeakMap();
|
|
281
|
+
const detailState = (details) => {
|
|
282
|
+
if (!detailStates.has(details)) detailStates.set(details, {});
|
|
283
|
+
return detailStates.get(details);
|
|
284
|
+
};
|
|
285
|
+
const loadDetails = async (details, retry = false) => {
|
|
286
|
+
if (!details.open || !details.isConnected) return;
|
|
287
|
+
const state = detailState(details);
|
|
288
|
+
const revision = details.dataset.detailRevision;
|
|
289
|
+
if (state.loadedRevision === revision) state.loadedFor = revision;
|
|
290
|
+
if (state.pending || state.loadedFor === revision || (!retry && state.failedRevision === revision)) return;
|
|
291
|
+
const body = details.querySelector("[data-detail-body]");
|
|
292
|
+
const target = details.querySelector("[data-detail-content]");
|
|
293
|
+
const message = details.querySelector("[data-detail-message]");
|
|
294
|
+
if (!body || !target || !message) return;
|
|
295
|
+
const controller = new AbortController();
|
|
296
|
+
const request = { controller, revision };
|
|
297
|
+
state.pending = request;
|
|
298
|
+
state.failedRevision = undefined;
|
|
299
|
+
body.setAttribute("aria-busy", "true");
|
|
300
|
+
message.textContent = state.loadedRevision ? "" : labels.loading;
|
|
301
|
+
const timeout = setTimeout(() => controller.abort(), 15000);
|
|
302
|
+
try {
|
|
303
|
+
const url = new URL(eventsUrl, window.location.href);
|
|
304
|
+
url.searchParams.delete("events");
|
|
305
|
+
url.searchParams.set("detail", details.dataset.detailKey);
|
|
306
|
+
if (state.cursor !== undefined) url.searchParams.set("detailAfter", String(state.cursor));
|
|
307
|
+
const response = await fetch(url, { signal: controller.signal, cache: "no-store" });
|
|
308
|
+
if (!response.ok) throw new Error("Detail HTTP " + response.status);
|
|
309
|
+
const result = await response.json();
|
|
310
|
+
if (result.key !== details.dataset.detailKey || typeof result.content !== "string" || typeof result.revision !== "string") throw new Error("Invalid detail response");
|
|
311
|
+
if (state.pending !== request || !details.open || !details.isConnected) return;
|
|
312
|
+
if (details.dataset.detailRevision !== revision && result.revision !== details.dataset.detailRevision) return;
|
|
313
|
+
const positions = Array.from(target.querySelectorAll("[data-scroll-id]"), (item) => [item, item.scrollLeft, item.scrollTop]);
|
|
314
|
+
if (typeof result.outputAppend === "string") {
|
|
315
|
+
const output = target.querySelector(".tool-output:not(.error-output)");
|
|
316
|
+
if (!output) throw new Error("Output delta without a loaded body");
|
|
317
|
+
output.append(document.createTextNode(result.outputAppend));
|
|
318
|
+
const footer = target.querySelector(".tool-footer");
|
|
319
|
+
if (footer && result.footer) patchMarkup(footer, result.footer);
|
|
320
|
+
} else patchMarkup(target, result.content);
|
|
321
|
+
state.cursor = result.cursor;
|
|
322
|
+
window.agentBotDiagrams?.scan();
|
|
323
|
+
for (const [item, left, top] of positions) { if (item.isConnected) { item.scrollLeft = left; item.scrollTop = top; } }
|
|
324
|
+
state.loadedRevision = result.revision;
|
|
325
|
+
state.loadedFor = details.dataset.detailRevision;
|
|
326
|
+
message.textContent = "";
|
|
327
|
+
updateElapsed();
|
|
328
|
+
} catch {
|
|
329
|
+
if (state.pending !== request || !details.open || !details.isConnected) return;
|
|
330
|
+
state.cursor = undefined;
|
|
331
|
+
state.failedRevision = revision;
|
|
332
|
+
message.textContent = labels.loadFailed;
|
|
333
|
+
const button = document.createElement("button");
|
|
334
|
+
button.type = "button";
|
|
335
|
+
button.textContent = labels.retry;
|
|
336
|
+
button.addEventListener("click", () => { void loadDetails(details, true); });
|
|
337
|
+
message.append(button);
|
|
338
|
+
} finally {
|
|
339
|
+
clearTimeout(timeout);
|
|
340
|
+
if (state.pending === request) {
|
|
341
|
+
state.pending = undefined;
|
|
342
|
+
body.removeAttribute("aria-busy");
|
|
343
|
+
if (details.dataset.detailRevision !== revision) void loadDetails(details);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
content.addEventListener("toggle", (event) => {
|
|
348
|
+
const details = event.target;
|
|
349
|
+
if (!details.matches?.("details[data-detail-key]")) return;
|
|
350
|
+
if (details.open) { void loadDetails(details, true); return; }
|
|
351
|
+
const state = detailState(details);
|
|
352
|
+
state.pending?.controller.abort();
|
|
353
|
+
state.pending = undefined;
|
|
354
|
+
details.querySelector("[data-detail-body]")?.removeAttribute("aria-busy");
|
|
355
|
+
const message = details.querySelector("[data-detail-message]");
|
|
356
|
+
if (message) message.textContent = "";
|
|
357
|
+
}, true);
|
|
358
|
+
const refreshDetails = () => {
|
|
359
|
+
for (const details of content.querySelectorAll("details[data-detail-key][open]")) void loadDetails(details);
|
|
360
|
+
};
|
|
361
|
+
window.addEventListener("pagehide", () => {
|
|
362
|
+
source?.close();
|
|
363
|
+
clearInterval(clock);
|
|
364
|
+
for (const details of content.querySelectorAll("details[data-detail-key]")) detailStates.get(details)?.pending?.controller.abort();
|
|
365
|
+
}, { once: true });
|
|
366
|
+
let previousContent;
|
|
367
|
+
let previousMetadata;
|
|
368
|
+
const applySnapshot = (update) => {
|
|
369
|
+
const contentChanged = update.content !== previousContent;
|
|
370
|
+
const scrollPositions = new Map(Array.from(contentChanged ? content.querySelectorAll("[data-scroll-id]") : [], (item) => [item.dataset.scrollId, [item.scrollLeft, item.scrollTop]]));
|
|
177
371
|
const top = window.scrollY;
|
|
178
372
|
const maxTop = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
|
|
179
373
|
const atBottom = maxTop - top <= 32;
|
|
180
|
-
|
|
181
|
-
|
|
374
|
+
if (contentChanged) {
|
|
375
|
+
patchMarkup(content, update.content);
|
|
376
|
+
window.agentBotDiagrams.scan();
|
|
377
|
+
previousContent = update.content;
|
|
378
|
+
}
|
|
379
|
+
if (update.metadata !== previousMetadata) {
|
|
380
|
+
patchMarkup(metadata, update.metadata);
|
|
381
|
+
previousMetadata = update.metadata;
|
|
382
|
+
}
|
|
182
383
|
status.textContent = update.statusLabel;
|
|
183
384
|
status.className = "status " + update.status;
|
|
184
|
-
for (const item of content.querySelectorAll("details[data-activity-id]")) {
|
|
185
|
-
if (disclosures.has(item.dataset.activityId)) item.open = disclosures.get(item.dataset.activityId);
|
|
186
|
-
}
|
|
187
385
|
for (const item of content.querySelectorAll("[data-scroll-id]")) {
|
|
188
386
|
const position = scrollPositions.get(item.dataset.scrollId);
|
|
189
387
|
if (position) { item.scrollLeft = position[0]; item.scrollTop = position[1]; }
|
|
190
388
|
}
|
|
191
389
|
updateElapsed();
|
|
390
|
+
refreshDetails();
|
|
391
|
+
if (!contentChanged) return;
|
|
192
392
|
requestAnimationFrame(() => {
|
|
193
393
|
const nextMaxTop = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
|
|
194
394
|
window.scrollTo(0, atBottom ? nextMaxTop : Math.min(top, nextMaxTop));
|
|
195
395
|
});
|
|
196
396
|
};
|
|
197
397
|
|
|
398
|
+
const applyPatch = (update) => {
|
|
399
|
+
const template = document.createElement("template");
|
|
400
|
+
template.innerHTML = update.content;
|
|
401
|
+
const incoming = template.content;
|
|
402
|
+
const timeline = content.querySelector('[data-preview-key="timeline"]');
|
|
403
|
+
const nextTimeline = incoming.querySelector('[data-preview-key="timeline"]');
|
|
404
|
+
for (const id of update.removed || []) {
|
|
405
|
+
for (const node of timeline.children) if (node.dataset.activity === id || node.dataset.activityId === id) node.remove();
|
|
406
|
+
}
|
|
407
|
+
for (const node of Array.from(nextTimeline.children)) {
|
|
408
|
+
const id = node.dataset.activity || node.dataset.activityId;
|
|
409
|
+
if (!id) continue;
|
|
410
|
+
timeline.querySelector(".empty")?.remove();
|
|
411
|
+
const current = Array.from(timeline.children).find((item) => (item.dataset.activity || item.dataset.activityId) === id);
|
|
412
|
+
if (current) patchNode(current, node);
|
|
413
|
+
else {
|
|
414
|
+
const nextId = update.before?.[id];
|
|
415
|
+
const anchor = Array.from(timeline.children).find((child) => (child.dataset.activity || child.dataset.activityId) === nextId);
|
|
416
|
+
timeline.insertBefore(node, anchor || null);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
timeline.hidden = !timeline.children.length;
|
|
420
|
+
const incomingKeys = new Set(Array.from(incoming.children, (node) => node.dataset.previewKey));
|
|
421
|
+
for (const node of Array.from(incoming.children)) {
|
|
422
|
+
const key = nodeKey(node);
|
|
423
|
+
if (node.dataset.previewKey === "timeline" || !key) continue;
|
|
424
|
+
const current = Array.from(content.children).find((item) => nodeKey(item) === key);
|
|
425
|
+
if (current) patchNode(current, node);
|
|
426
|
+
else {
|
|
427
|
+
const result = content.querySelector('[data-preview-key="result"]');
|
|
428
|
+
content.insertBefore(node, node.dataset.activityId === "turn:files-summary" ? result : null);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
for (const key of ["approval", "error", "plan", "result"]) {
|
|
432
|
+
if (!incomingKeys.has(key)) content.querySelector('[data-preview-key="' + key + '"]')?.remove();
|
|
433
|
+
}
|
|
434
|
+
patchMarkup(metadata, update.metadata);
|
|
435
|
+
status.textContent = update.statusLabel;
|
|
436
|
+
status.className = "status " + update.status;
|
|
437
|
+
window.agentBotDiagrams.scan();
|
|
438
|
+
refreshDetails();
|
|
439
|
+
updateElapsed();
|
|
440
|
+
};
|
|
441
|
+
if (terminalAtLoad || typeof EventSource !== "function") return;
|
|
198
442
|
source = new EventSource(eventsUrl);
|
|
443
|
+
source.addEventListener("patch", (event) => {
|
|
444
|
+
try {
|
|
445
|
+
const update = JSON.parse(event.data);
|
|
446
|
+
applyPatch(update);
|
|
447
|
+
live.textContent = update.terminal ? "" : labels.live;
|
|
448
|
+
live.className = update.terminal ? "live terminal" : "live connected";
|
|
449
|
+
if (update.terminal) { source.close(); clearInterval(clock); }
|
|
450
|
+
} catch { live.textContent = labels.waitingForUpdates; }
|
|
451
|
+
});
|
|
199
452
|
source.addEventListener("update", (event) => {
|
|
200
453
|
try {
|
|
201
454
|
const update = JSON.parse(event.data);
|
|
202
455
|
if (typeof update.content !== "string" || typeof update.metadata !== "string") return;
|
|
203
|
-
|
|
456
|
+
applySnapshot(update);
|
|
204
457
|
live.textContent = update.terminal ? "" : labels.live;
|
|
205
458
|
live.className = update.terminal ? "live terminal" : "live connected";
|
|
206
|
-
if (update.terminal) source.close();
|
|
459
|
+
if (update.terminal) { source.close(); clearInterval(clock); }
|
|
207
460
|
} catch {
|
|
208
461
|
live.textContent = labels.waitingForUpdates;
|
|
209
462
|
live.className = "live";
|
|
@@ -217,7 +470,6 @@ export const TURN_PREVIEW_CLIENT_SCRIPT = `(() => {
|
|
|
217
470
|
live.textContent = labels.reconnecting;
|
|
218
471
|
live.className = "live disconnected";
|
|
219
472
|
};
|
|
220
|
-
window.addEventListener("pagehide", () => source.close(), { once: true });
|
|
221
473
|
})();
|
|
222
474
|
`;
|
|
223
475
|
export function isTurnPreviewState(value) {
|
|
@@ -235,7 +487,7 @@ export function isTurnPreviewState(value) {
|
|
|
235
487
|
}
|
|
236
488
|
export function renderTurnPreviewPage(input) {
|
|
237
489
|
const language = input.language ?? "zh";
|
|
238
|
-
const snapshot = renderTurnPreviewSnapshot(input.state, input.localFileUrl, language);
|
|
490
|
+
const snapshot = renderTurnPreviewSnapshot(input.state, input.localFileUrl, language, { deferDetails: true });
|
|
239
491
|
const title = previewTitle(input.state);
|
|
240
492
|
return `<!doctype html>
|
|
241
493
|
<html lang="${language === "zh" ? "zh-CN" : "en"}">
|
|
@@ -243,9 +495,9 @@ export function renderTurnPreviewPage(input) {
|
|
|
243
495
|
<meta charset="utf-8">
|
|
244
496
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
245
497
|
<title>${escapeHtml(title)} · Agent Bot</title>
|
|
246
|
-
<style>${TURN_PREVIEW_CSS}</style>
|
|
498
|
+
<style>${TURN_PREVIEW_CSS}${DIAGRAM_PREVIEW_CSS}${PREVIEW_SYNTAX_CSS}</style>
|
|
247
499
|
</head>
|
|
248
|
-
<body data-events-url="${escapeAttribute(input.eventsUrl)}">
|
|
500
|
+
<body data-terminal="${snapshot.terminal}" data-events-url="${escapeAttribute(input.eventsUrl)}" data-diagram-script="${escapeAttribute(input.diagramScriptPath ?? "")}">
|
|
249
501
|
<header class="page-header">
|
|
250
502
|
<div class="header-inner">
|
|
251
503
|
<div class="header-top">
|
|
@@ -261,7 +513,19 @@ export function renderTurnPreviewPage(input) {
|
|
|
261
513
|
</body>
|
|
262
514
|
</html>`;
|
|
263
515
|
}
|
|
264
|
-
export function
|
|
516
|
+
export function renderTurnPreviewPatch(state, changed, removed, localFileUrl, language) {
|
|
517
|
+
const partial = { ...state,
|
|
518
|
+
activities: state.activities.filter((a) => changed.has(a.id)),
|
|
519
|
+
reasoningItems: state.reasoningItems?.filter((r) => changed.has(`reasoning:${r.itemId}`)),
|
|
520
|
+
};
|
|
521
|
+
const order = [
|
|
522
|
+
...(state.reasoningItems ?? []).filter((r) => !r.afterActivityId).map((r) => `reasoning:${r.itemId}`),
|
|
523
|
+
...state.activities.flatMap((a) => [a.id, ...(state.reasoningItems ?? []).filter((r) => r.afterActivityId === a.id).map((r) => `reasoning:${r.itemId}`)]),
|
|
524
|
+
];
|
|
525
|
+
const before = Object.fromEntries(order.flatMap((id, i) => changed.has(id) && order[i + 1] ? [[id, order[i + 1]]] : []));
|
|
526
|
+
return { ...renderTurnPreviewSnapshot(partial, localFileUrl, language, { deferDetails: true }), removed: [...removed], before };
|
|
527
|
+
}
|
|
528
|
+
export function renderTurnPreviewSnapshot(state, localFileUrl, language = "zh", options = {}) {
|
|
265
529
|
const labels = previewLabels(language);
|
|
266
530
|
const resolveFileUrl = localFileUrl ? (filePath) => {
|
|
267
531
|
if (path.isAbsolute(filePath) || path.win32.isAbsolute(filePath))
|
|
@@ -270,25 +534,35 @@ export function renderTurnPreviewSnapshot(state, localFileUrl, language = "zh")
|
|
|
270
534
|
return undefined;
|
|
271
535
|
return localFileUrl(path.resolve(state.projectCwd, filePath));
|
|
272
536
|
} : undefined;
|
|
273
|
-
const renderText = (value) => renderMarkdown(value, state.projectCwd, localFileUrl);
|
|
274
|
-
const timeline = renderTimeline(state
|
|
537
|
+
const renderText = (value) => renderMarkdown(value, state.projectCwd, localFileUrl, language);
|
|
538
|
+
const timeline = renderTimeline(state, renderText, resolveFileUrl, state.fullToolOutputs, state.fullToolErrors, language, state.projectCwd, options.deferDetails);
|
|
539
|
+
const terminal = isTerminal(state.status);
|
|
540
|
+
const waitingForProgress = !terminal && !state.finalResponse && !state.assistantText
|
|
541
|
+
&& !state.error && !state.approval && state.status !== "waiting_for_approval"
|
|
542
|
+
&& state.plan.length === 0 && state.fileSummary.length === 0;
|
|
543
|
+
const timelineContent = timeline || (waitingForProgress
|
|
544
|
+
? `<div class="empty">${escapeHtml(language === "zh" ? "正在等待 Agent 返回进度…" : "Waiting for Agent progress…")}</div>`
|
|
545
|
+
: "");
|
|
275
546
|
const sections = [
|
|
276
|
-
state.prompt ? renderMessageActivity("prompt", state.prompt, "user prompt", renderText) : "",
|
|
547
|
+
state.prompt || state.promptImagePaths?.length ? renderMessageActivity("prompt", state.prompt ?? "", "user prompt", renderText, renderMessageImages(state.promptImagePaths, resolveFileUrl, language)) : "",
|
|
548
|
+
state.historyDetailError
|
|
549
|
+
? `<section class="notice" data-preview-key="history-detail"><h2>${language === "zh" ? "历史执行详情读取失败" : "Failed to load historical execution details"}</h2><pre>${escapeHtml(state.historyDetailError)}</pre><p>${language === "zh" ? "刷新页面重试;本轮执行状态未改变。" : "Refresh to retry; the Turn's execution status is unchanged."}</p></section>`
|
|
550
|
+
: state.historyDetail === "summary"
|
|
551
|
+
? `<section class="notice" data-preview-key="history-detail">${language === "zh" ? "当前仅保存轮次摘要,尚未加载历史执行详情。" : "Only a Turn summary is saved; execution details have not been loaded."}</section>` : "",
|
|
277
552
|
state.plan.length > 0 ? renderPlan(state, language) : "",
|
|
278
553
|
state.activitiesTruncated
|
|
279
|
-
? `<div class="notice">${escapeHtml(labels.truncated)}</div>`
|
|
554
|
+
? `<div class="notice" data-preview-key="truncated">${escapeHtml(labels.truncated)}</div>`
|
|
280
555
|
: "",
|
|
281
|
-
|
|
282
|
-
state.fileSummary.length > 0 ? renderFileSummary(state, language, resolveFileUrl) : "",
|
|
283
|
-
state.approval ? `<section class="notice"><h2>${escapeHtml(state.approval.title)}</h2><div class="markdown">${renderText(state.approval.reason ?? "")}</div><p>${escapeHtml(language === "zh" ? "请在飞书任务卡片中确认或拒绝。" : "Approve or reject using the task card in Feishu.")}</p></section>` : "",
|
|
284
|
-
state.error ? `<section class="result error-result"><h2>${escapeHtml(labels.error)}</h2><pre>${escapeHtml(state.error)}</pre></section>` : "",
|
|
556
|
+
`<section class="timeline" data-preview-key="timeline"${timelineContent ? "" : " hidden"}>${timelineContent}</section>`,
|
|
557
|
+
state.fileSummary.length > 0 ? renderFileSummary(state, language, resolveFileUrl, options.deferDetails) : "",
|
|
558
|
+
state.approval ? `<section class="notice" data-preview-key="approval"><h2>${escapeHtml(state.approval.title)}</h2><div class="markdown">${renderText(state.approval.reason ?? "")}</div><p>${escapeHtml(language === "zh" ? "请在飞书任务卡片中确认或拒绝。" : "Approve or reject using the task card in Feishu.")}</p></section>` : "",
|
|
559
|
+
state.error ? `<section class="result error-result" data-preview-key="error"><h2>${escapeHtml(labels.error)}</h2><pre>${escapeHtml(state.error)}</pre></section>` : "",
|
|
285
560
|
state.finalResponse
|
|
286
|
-
? `<section class="result final-result"><h2>${escapeHtml(labels.finalAnswer)}</h2><div class="markdown">${renderText(state.finalResponse)}</div></section>`
|
|
561
|
+
? `<section class="result final-result" data-preview-key="result"><h2>${escapeHtml(labels.finalAnswer)}</h2><div class="markdown">${renderText(state.finalResponse)}</div></section>`
|
|
287
562
|
: state.assistantText
|
|
288
|
-
? `<section class="result"><h2>${escapeHtml(labels.generating)}</h2><div class="markdown">${renderText(state.assistantText)}</div></section>`
|
|
563
|
+
? `<section class="result" data-preview-key="result"><h2>${escapeHtml(labels.generating)}</h2><div class="markdown">${renderText(state.assistantText)}</div></section>`
|
|
289
564
|
: "",
|
|
290
565
|
].filter(Boolean).join("");
|
|
291
|
-
const terminal = isTerminal(state.status);
|
|
292
566
|
return {
|
|
293
567
|
content: sections,
|
|
294
568
|
metadata: renderMetadata(state, language),
|
|
@@ -304,28 +578,120 @@ function renderPlan(state, language) {
|
|
|
304
578
|
const marker = step.status === "completed" ? "✓" : step.status === "in_progress" ? "↻" : "○";
|
|
305
579
|
return `<li class="${step.status}"><span>${marker}</span><span>${escapeHtml(step.text)}</span></li>`;
|
|
306
580
|
}).join("");
|
|
307
|
-
return `<section class="plan"><h2>${escapeHtml(labels.plan)} <span>${completed}/${state.plan.length}</span></h2><ol>${items}</ol></section>`;
|
|
581
|
+
return `<section class="plan" data-preview-key="plan"><h2>${escapeHtml(labels.plan)} <span>${completed}/${state.plan.length}</span></h2><ol>${items}</ol></section>`;
|
|
308
582
|
}
|
|
309
|
-
function renderTimeline(
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
583
|
+
function renderTimeline(state, renderText, localFileUrl, fullToolOutputs, fullToolErrors, language = "zh", projectCwd, deferDetails = false) {
|
|
584
|
+
const activities = state.activities ?? [];
|
|
585
|
+
const positions = new Map(activities.map((activity, index) => [activity.id, index]));
|
|
586
|
+
const reasoning = new Map();
|
|
587
|
+
for (const item of turnReasoningItems(state)) {
|
|
588
|
+
const position = item.afterActivityId ? positions.get(item.afterActivityId) ?? -1 : -1;
|
|
589
|
+
const items = reasoning.get(position) ?? [];
|
|
590
|
+
items.push(item);
|
|
591
|
+
reasoning.set(position, items);
|
|
592
|
+
}
|
|
593
|
+
const renderReasoning = (position) => (reasoning.get(position) ?? [])
|
|
594
|
+
.map((item) => renderReasoningItem(item, state.projectCwd, localFileUrl, language, deferDetails)).join("");
|
|
595
|
+
return renderReasoning(-1) + activities.map((activity, index) => {
|
|
596
|
+
const visible = activity.kind === "reasoning"
|
|
597
|
+
? activity.id.startsWith("commentary:") ? { ...activity, kind: "assistant" } : undefined
|
|
598
|
+
: activity;
|
|
599
|
+
return (visible ? renderActivity(visible, renderText, localFileUrl, fullToolOutputs, fullToolErrors, language, projectCwd, deferDetails) : "")
|
|
600
|
+
+ renderReasoning(index);
|
|
601
|
+
}).join("");
|
|
602
|
+
}
|
|
603
|
+
function reasoningPresentation(item) {
|
|
604
|
+
let title;
|
|
605
|
+
const sections = ["summary", "content"].map((field) => ({
|
|
606
|
+
field,
|
|
607
|
+
parts: item[field].map((text, index) => {
|
|
608
|
+
const heading = !title && text?.trim() ? leadingReasoningHeading(text) : undefined;
|
|
609
|
+
if (heading)
|
|
610
|
+
title = heading.title;
|
|
611
|
+
return { index, text: heading?.body ?? text };
|
|
612
|
+
}).filter((part) => part.text?.trim()),
|
|
613
|
+
}));
|
|
614
|
+
return { title, sections };
|
|
314
615
|
}
|
|
315
|
-
function
|
|
616
|
+
function leadingReasoningHeading(text) {
|
|
617
|
+
const first = /^(?:[ \t]*\r?\n)*([^\r\n]*)(?:\r?\n|$)/u.exec(text);
|
|
618
|
+
if (!first || !/^ {0,3}(?:#{1,6}[ \t]+|\*\*|__)/u.test(first[1]))
|
|
619
|
+
return undefined;
|
|
620
|
+
// Parse only a possible title line; collapsed panels must not render the full Markdown.
|
|
621
|
+
const [block, inline] = MARKDOWN.parse(first[1], {});
|
|
622
|
+
const tokens = inline?.children?.filter((token) => token.type !== "text" || token.content !== "") ?? [];
|
|
623
|
+
const strongTitle = block?.type === "paragraph_open"
|
|
624
|
+
&& tokens[0]?.type === "strong_open" && tokens.at(-1)?.type === "strong_close"
|
|
625
|
+
&& tokens.slice(1, -1).every((token) => token.level > 0);
|
|
626
|
+
if (block?.type !== "heading_open" && !strongTitle)
|
|
627
|
+
return undefined;
|
|
628
|
+
const title = tokens.map((token) => token.content).join("").trim();
|
|
629
|
+
if (!title)
|
|
630
|
+
return undefined;
|
|
631
|
+
// Keep linked/image headings in the body so moving their text never discards an attachment or link.
|
|
632
|
+
const keepHeading = tokens.some((token) => token.type === "link_open" || token.type === "image");
|
|
633
|
+
return { title, body: keepHeading ? text : text.slice(first[0].length) };
|
|
634
|
+
}
|
|
635
|
+
function renderReasoningItem(item, cwd, localFileUrl, language, deferDetails) {
|
|
636
|
+
const presentation = reasoningPresentation(item);
|
|
637
|
+
if (!presentation.title && !presentation.sections.some((section) => section.parts.length > 0))
|
|
638
|
+
return "";
|
|
639
|
+
const title = presentation.title ?? previewLabels(language).reasoning;
|
|
640
|
+
const lazy = deferDetails ? lazyDetailAttributes(`reasoning:${item.itemId}`, reasoningRevision(item, cwd)) : "";
|
|
641
|
+
const body = deferDetails ? lazyDetailBody() : renderReasoningBody(item, cwd, localFileUrl, language, presentation);
|
|
642
|
+
return `<details class="reasoning-step" data-activity-id="reasoning:${escapeAttribute(item.itemId)}"${lazy}><summary class="reasoning-header" title="${escapeAttribute(title)}"><span class="reasoning-title">💭 ${escapeHtml(title)}</span></summary>${body}</details>`;
|
|
643
|
+
}
|
|
644
|
+
function renderReasoningBody(item, cwd, localFileUrl, language, presentation = reasoningPresentation(item)) {
|
|
645
|
+
return presentation.sections.map(({ field, parts }) => {
|
|
646
|
+
if (parts.length === 0)
|
|
647
|
+
return "";
|
|
648
|
+
const sections = parts.map(({ index, text }) => `<div class="markdown reasoning-part" data-reasoning-part="${index}">${renderMarkdown(text, cwd, localFileUrl, language)}</div>`).join("");
|
|
649
|
+
return `<section class="reasoning-section" data-reasoning-field="${field}">${sections}</section>`;
|
|
650
|
+
}).join("");
|
|
651
|
+
}
|
|
652
|
+
function reasoningRevision(item, cwd) {
|
|
653
|
+
if (item.previewRevision !== undefined)
|
|
654
|
+
return String(item.previewRevision);
|
|
655
|
+
return createHash("sha256").update(JSON.stringify([item.summary, item.content, cwd])).digest("hex").slice(0, 24);
|
|
656
|
+
}
|
|
657
|
+
function renderActivity(activity, renderText, localFileUrl, fullToolOutputs, fullToolErrors, language = "zh", projectCwd, deferDetails = false) {
|
|
316
658
|
switch (activity.kind) {
|
|
317
659
|
case "assistant":
|
|
660
|
+
if (activity.id.startsWith("commentary:runtime-error:")) {
|
|
661
|
+
return `<article class="activity message runtime-error" data-activity="${escapeAttribute(activity.id)}"><pre>${escapeHtml(activity.text)}</pre></article>`;
|
|
662
|
+
}
|
|
318
663
|
return renderMessageActivity(activity.id, activity.text, "commentary", renderText);
|
|
319
664
|
case "user":
|
|
320
|
-
return renderMessageActivity(activity.id, activity.text, "user", renderText);
|
|
665
|
+
return renderMessageActivity(activity.id, activity.text, "user", renderText, renderMessageImages(activity.localImagePaths, localFileUrl, language));
|
|
321
666
|
case "tool":
|
|
322
|
-
return renderToolActivity(activity.id, activity.tool, localFileUrl, fullToolOutputs, fullToolErrors, language, projectCwd);
|
|
667
|
+
return renderToolActivity(activity.id, activity.tool, localFileUrl, fullToolOutputs, fullToolErrors, language, projectCwd, deferDetails);
|
|
323
668
|
}
|
|
324
669
|
}
|
|
325
|
-
function renderMessageActivity(id, text, kind, renderText) {
|
|
326
|
-
return `<article class="activity message ${kind}" data-activity="${escapeAttribute(id)}"><div class="markdown">${renderText(text)}</div></article>`;
|
|
670
|
+
function renderMessageActivity(id, text, kind, renderText, images = "") {
|
|
671
|
+
return `<article class="activity message ${kind}" data-activity="${escapeAttribute(id)}"><div class="markdown">${renderText(text)}${images}</div></article>`;
|
|
327
672
|
}
|
|
328
|
-
function
|
|
673
|
+
function renderMessageImages(paths, localFileUrl, language) {
|
|
674
|
+
if (!paths?.length)
|
|
675
|
+
return "";
|
|
676
|
+
return [...new Set(paths)].map((filePath, index) => {
|
|
677
|
+
const label = language === "zh" ? `图片 ${index + 1}` : `Image ${index + 1}`;
|
|
678
|
+
const unavailable = `<p class="muted">${escapeHtml(label)}${language === "zh" ? "不可用" : " unavailable"}</p>`;
|
|
679
|
+
const url = localFileUrl?.(filePath);
|
|
680
|
+
if (!url)
|
|
681
|
+
return unavailable;
|
|
682
|
+
try {
|
|
683
|
+
const raw = new URL(url);
|
|
684
|
+
if (raw.protocol !== "http:" && raw.protocol !== "https:")
|
|
685
|
+
return unavailable;
|
|
686
|
+
raw.searchParams.set("raw", "1");
|
|
687
|
+
return `<p class="message-image"><img src="${escapeAttribute(raw.toString())}" alt="${escapeAttribute(label)}" loading="lazy"></p>`;
|
|
688
|
+
}
|
|
689
|
+
catch {
|
|
690
|
+
return unavailable;
|
|
691
|
+
}
|
|
692
|
+
}).join("");
|
|
693
|
+
}
|
|
694
|
+
function renderToolActivity(activityId, tool, localFileUrl, fullToolOutputs, fullToolErrors, language = "zh", projectCwd, deferDetails = false) {
|
|
329
695
|
const labels = previewLabels(language);
|
|
330
696
|
const command = tool.command ? (displayToolCommand(tool.command) || tool.command.trim()) : undefined;
|
|
331
697
|
const repl = isReplTool(tool);
|
|
@@ -335,6 +701,25 @@ function renderToolActivity(activityId, tool, localFileUrl, fullToolOutputs, ful
|
|
|
335
701
|
? formatToolCommand(command)
|
|
336
702
|
: undefined;
|
|
337
703
|
const title = displayCommand ? summarizeToolCommand(displayCommand) : toolTitle(tool, command, repl, language);
|
|
704
|
+
const titleMarkup = displayCommand
|
|
705
|
+
? `<code class="tool-command-title">${escapeHtml(title)}</code>`
|
|
706
|
+
: `<span class="tool-title">${escapeHtml(title)}</span>`;
|
|
707
|
+
const timing = renderToolTiming(tool, "header");
|
|
708
|
+
const statusLabel = toolStatusLabel(tool.status, language);
|
|
709
|
+
const icon = `<span class="tool-status-icon" role="img" aria-label="${escapeAttribute(statusLabel)}" title="${escapeAttribute(statusLabel)}">${toolStatusIcon(tool.status)}</span>`;
|
|
710
|
+
const header = `<summary class="tool-header">${icon}<span class="tool-summary-content">${titleMarkup}</span><span class="tool-meta">${timing}</span></summary>`;
|
|
711
|
+
const body = deferDetails
|
|
712
|
+
? lazyDetailBody()
|
|
713
|
+
: renderToolBody(activityId, tool, localFileUrl, fullToolOutputs, fullToolErrors, language, projectCwd);
|
|
714
|
+
const lazy = deferDetails ? lazyDetailAttributes(`tool:${activityId}`, toolDetailRevision(tool, fullToolOutputs, fullToolErrors, projectCwd)) : "";
|
|
715
|
+
return `<article class="activity tool" data-activity="${escapeAttribute(activityId)}"><details class="tool-step" data-tool-status="${tool.status}" data-activity-id="${escapeAttribute(activityId)}"${lazy}>${header}${body}</details></article>`;
|
|
716
|
+
}
|
|
717
|
+
function renderToolBody(activityId, tool, localFileUrl, fullToolOutputs, fullToolErrors, language = "zh", projectCwd) {
|
|
718
|
+
const labels = previewLabels(language);
|
|
719
|
+
const command = tool.command ? (displayToolCommand(tool.command) || tool.command.trim()) : undefined;
|
|
720
|
+
const repl = isReplTool(tool);
|
|
721
|
+
const displayCommand = repl ? formatReplCommand(command ?? "") : command ? formatToolCommand(command) : undefined;
|
|
722
|
+
const status = `<span class="tool-state ${tool.status}">${escapeHtml(toolStatusLabel(tool.status, language))}</span>`;
|
|
338
723
|
const output = fullToolOutputs?.[tool.id] ?? tool.output;
|
|
339
724
|
const error = fullToolErrors?.[tool.id] ?? tool.error;
|
|
340
725
|
const sameError = Boolean(output && error && normalizeOutput(output) === normalizeOutput(error));
|
|
@@ -353,21 +738,17 @@ function renderToolActivity(activityId, tool, localFileUrl, fullToolOutputs, ful
|
|
|
353
738
|
? `<ul class="files" aria-label="${escapeAttribute(labels.files)}">${tool.files.map((file) => renderFileEntry(file, localFileUrl, projectCwd)).join("")}</ul>`
|
|
354
739
|
: "",
|
|
355
740
|
].filter(Boolean).join("");
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
const
|
|
361
|
-
const
|
|
362
|
-
const status = `<span class="tool-state ${tool.status}">${escapeHtml(statusLabel)}</span>`;
|
|
363
|
-
const header = `<summary class="tool-header">${icon}<span class="tool-summary-content">${titleMarkup}</span><span class="tool-meta">${timing}</span></summary>`;
|
|
741
|
+
const footer = renderToolFooter(tool, (displayOutput?.length ?? 0) + (displayError?.length ?? 0), language);
|
|
742
|
+
return `<div class="tool-body"><div class="tool-content" data-scroll-id="${escapeAttribute(`${activityId}:content`)}" tabindex="0">${detailParts || `<div class="muted">${escapeHtml(labels.noToolDetails)}</div>`}</div><div class="tool-footer">${footer}</div></div>`;
|
|
743
|
+
}
|
|
744
|
+
export function renderToolFooter(tool, characterCount, language) {
|
|
745
|
+
const labels = previewLabels(language);
|
|
746
|
+
const status = `<span class="tool-state ${tool.status}">${escapeHtml(toolStatusLabel(tool.status, language))}</span>`;
|
|
364
747
|
const startedAt = tool.startedAt === undefined ? undefined : new Date(tool.startedAt);
|
|
365
748
|
const startTime = startedAt
|
|
366
749
|
? `<time datetime="${startedAt.toISOString()}" title="${escapeAttribute(labels.startingTime)}">${new Intl.DateTimeFormat(language === "zh" ? "zh-CN" : "en-GB", { hour: "2-digit", minute: "2-digit", second: "2-digit", hourCycle: "h23" }).format(startedAt)}</time>`
|
|
367
750
|
: `<span title="${escapeAttribute(labels.startingTime)}">${escapeHtml(labels.unknown)}</span>`;
|
|
368
|
-
|
|
369
|
-
const footer = `<div class="tool-footer">${status}${startTime}${renderToolTiming(tool, "footer")}<span>${formatNumber(characterCount)} ${escapeHtml(labels.characters)}</span></div>`;
|
|
370
|
-
return `<article class="activity tool"><details class="tool-step" data-tool-status="${tool.status}" data-activity-id="${escapeAttribute(activityId)}">${header}<div class="tool-body"><div class="tool-content" data-scroll-id="${escapeAttribute(`${activityId}:content`)}" tabindex="0">${detailParts || `<div class="muted">${escapeHtml(labels.noToolDetails)}</div>`}</div>${footer}</div></details></article>`;
|
|
751
|
+
return `${status}${startTime}${renderToolTiming(tool, "footer")}<span>${formatNumber(characterCount)} ${escapeHtml(labels.characters)}</span>`;
|
|
371
752
|
}
|
|
372
753
|
function toolTitle(tool, command, repl = false, language = "zh") {
|
|
373
754
|
if (repl)
|
|
@@ -542,10 +923,55 @@ function renderToolImage(tool, localFileUrl, language = "zh") {
|
|
|
542
923
|
return "";
|
|
543
924
|
}
|
|
544
925
|
}
|
|
545
|
-
function renderFileSummary(state, language, localFileUrl) {
|
|
926
|
+
function renderFileSummary(state, language, localFileUrl, deferDetails = false) {
|
|
546
927
|
const labels = previewLabels(language);
|
|
547
|
-
const
|
|
548
|
-
|
|
928
|
+
const body = deferDetails ? lazyDetailBody() : renderFileSummaryBody(state, localFileUrl);
|
|
929
|
+
const lazy = deferDetails ? lazyDetailAttributes("files", fileSummaryRevision(state)) : "";
|
|
930
|
+
return `<details class="files-summary" data-activity-id="turn:files-summary"${lazy}><summary><h2>${escapeHtml(labels.fileChanges)} <span>${state.fileSummary.length}</span></h2></summary>${body}</details>`;
|
|
931
|
+
}
|
|
932
|
+
function renderFileSummaryBody(state, localFileUrl) {
|
|
933
|
+
return `<ul class="files">${state.fileSummary.map((file) => renderFileEntry(file, localFileUrl, state.projectCwd)).join("")}</ul>`;
|
|
934
|
+
}
|
|
935
|
+
function lazyDetailAttributes(key, revision) {
|
|
936
|
+
return ` data-detail-key="${escapeAttribute(key)}" data-detail-revision="${revision}"`;
|
|
937
|
+
}
|
|
938
|
+
function lazyDetailBody() {
|
|
939
|
+
return '<div data-detail-body><div class="detail-message" data-detail-message role="status"></div><div data-detail-content></div></div>';
|
|
940
|
+
}
|
|
941
|
+
function toolDetailRevision(tool, outputs, errors, cwd) {
|
|
942
|
+
if (tool.previewRevision !== undefined)
|
|
943
|
+
return String(tool.previewRevision);
|
|
944
|
+
return createHash("sha256").update(JSON.stringify([tool, outputs?.[tool.id], errors?.[tool.id], cwd])).digest("hex").slice(0, 24);
|
|
945
|
+
}
|
|
946
|
+
function fileSummaryRevision(state) {
|
|
947
|
+
return createHash("sha256").update(JSON.stringify([state.fileSummary, state.projectCwd])).digest("hex").slice(0, 24);
|
|
948
|
+
}
|
|
949
|
+
export function renderTurnPreviewDetail(state, key, localFileUrl, language = "zh") {
|
|
950
|
+
const resolveFileUrl = localFileUrl ? (filePath) => {
|
|
951
|
+
if (path.isAbsolute(filePath) || path.win32.isAbsolute(filePath))
|
|
952
|
+
return localFileUrl(filePath);
|
|
953
|
+
if (!state.projectCwd || !path.isAbsolute(state.projectCwd))
|
|
954
|
+
return undefined;
|
|
955
|
+
return localFileUrl(path.resolve(state.projectCwd, filePath));
|
|
956
|
+
} : undefined;
|
|
957
|
+
if (key === "files" && state.fileSummary.length > 0) {
|
|
958
|
+
return { key, revision: fileSummaryRevision(state), content: renderFileSummaryBody(state, resolveFileUrl) };
|
|
959
|
+
}
|
|
960
|
+
if (key.startsWith("reasoning:")) {
|
|
961
|
+
const item = turnReasoningItems(state).find((item) => `reasoning:${item.itemId}` === key);
|
|
962
|
+
if (!item || ![...item.summary, ...item.content].some((text) => text?.trim()))
|
|
963
|
+
return undefined;
|
|
964
|
+
return { key, revision: reasoningRevision(item, state.projectCwd),
|
|
965
|
+
content: renderReasoningBody(item, state.projectCwd, localFileUrl, language) };
|
|
966
|
+
}
|
|
967
|
+
const activity = state.activities?.find((item) => item.kind === "tool" && `tool:${item.id}` === key);
|
|
968
|
+
if (activity?.kind !== "tool")
|
|
969
|
+
return undefined;
|
|
970
|
+
return {
|
|
971
|
+
key,
|
|
972
|
+
revision: toolDetailRevision(activity.tool, state.fullToolOutputs, state.fullToolErrors, state.projectCwd),
|
|
973
|
+
content: renderToolBody(activity.id, activity.tool, resolveFileUrl, state.fullToolOutputs, state.fullToolErrors, language, state.projectCwd),
|
|
974
|
+
};
|
|
549
975
|
}
|
|
550
976
|
function renderFileEntry(file, localFileUrl, projectCwd) {
|
|
551
977
|
const label = `<code>${escapeHtml(displayFilePath(file.path, projectCwd))}</code>`;
|
|
@@ -569,11 +995,11 @@ function renderMetadata(state, language) {
|
|
|
569
995
|
return [
|
|
570
996
|
elapsed,
|
|
571
997
|
state.model?.trim() ? `<span title="${escapeAttribute(labels.model)}">${escapeHtml(state.model.trim())}</span>` : "",
|
|
998
|
+
state.modelProvider?.trim() ? `<span title="Provider">Provider: ${escapeHtml(state.modelProvider.trim())}</span>` : "",
|
|
572
999
|
state.totalTokens === undefined ? "" : state.cachedInputTokens === undefined
|
|
573
1000
|
? `<span title="${escapeAttribute(labels.turnTokens)}">${formatTokenCount(state.totalTokens)} tokens</span>`
|
|
574
1001
|
: `<span title="${escapeAttribute(labels.nonCachedTokens)}: ${formatNumber(state.totalTokens)} tokens">${escapeHtml(labels.nonCachedTokens)} ${formatTokenCount(state.totalTokens)} tokens</span>`,
|
|
575
1002
|
state.totalTokensIncludingCache === undefined ? "" : `<span title="${escapeAttribute(labels.totalTokens)}: ${formatNumber(state.totalTokensIncludingCache)} tokens">${escapeHtml(labels.totalTokens)} ${formatTokenCount(state.totalTokensIncludingCache)} tokens</span>`,
|
|
576
|
-
state.cachedInputTokens === undefined ? "" : `<span title="${escapeAttribute(labels.cachedTokens)}: ${formatNumber(state.cachedInputTokens)} tokens">${escapeHtml(labels.cachedTokens)} ${formatTokenCount(state.cachedInputTokens)} tokens</span>`,
|
|
577
1003
|
state.contextCompactionAfterTokens === undefined ? "" : state.contextCompactionBeforeTokens === undefined
|
|
578
1004
|
? `<span title="${escapeAttribute(labels.compactionAfterTokens)}">${formatTokenCount(state.contextCompactionAfterTokens)} tokens</span>`
|
|
579
1005
|
: `<span title="${escapeAttribute(labels.compactionTokens)}">${formatTokenCount(state.contextCompactionBeforeTokens)} → ${formatTokenCount(state.contextCompactionAfterTokens)} tokens</span>`,
|
|
@@ -584,8 +1010,8 @@ function previewTitle(state) {
|
|
|
584
1010
|
const value = (state.prompt ?? state.taskTitle ?? "Turn Preview").replace(/\s+/gu, " ").trim();
|
|
585
1011
|
return value.length > 80 ? `${value.slice(0, 79)}…` : value;
|
|
586
1012
|
}
|
|
587
|
-
function renderMarkdown(value, projectCwd, localFileUrl) {
|
|
588
|
-
const environment = {};
|
|
1013
|
+
function renderMarkdown(value, projectCwd, localFileUrl, language = "zh") {
|
|
1014
|
+
const environment = { language };
|
|
589
1015
|
const tokens = MARKDOWN.parse(value, environment);
|
|
590
1016
|
// Only the directory is used to resolve relative links; no Markdown file is created.
|
|
591
1017
|
const markdownPath = projectCwd && path.isAbsolute(projectCwd) ? path.join(projectCwd, "turn.md") : undefined;
|
|
@@ -641,6 +1067,10 @@ function escapeAttribute(value) {
|
|
|
641
1067
|
const TURN_PREVIEW_CSS = `
|
|
642
1068
|
:root { color-scheme: light dark; font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; --page: #fff; --surface: #f5f6f7; --code: #fafafa; --text: #25272b; --muted: #62666d; --line: #e0e2e5; --accent: #245bc0; --success: #257449; --danger: #b52c34; --scrollbar-thumb: #d4d7dc; --scrollbar-thumb-hover: #afb4bc; }
|
|
643
1069
|
* { box-sizing: border-box; letter-spacing: 0; }
|
|
1070
|
+
html { scrollbar-gutter: stable; }
|
|
1071
|
+
@supports not (scrollbar-gutter: stable) {
|
|
1072
|
+
html { overflow-y: scroll; }
|
|
1073
|
+
}
|
|
644
1074
|
body { margin: 0; background: var(--page); color: var(--text); font-size: 13px; line-height: 1.55; }
|
|
645
1075
|
.page-header { position: sticky; top: 0; z-index: 3; background: var(--page); border-bottom: 1px solid var(--line); }
|
|
646
1076
|
.header-inner { display: flex; flex-wrap: wrap; align-items: center; gap: 3px 12px; max-width: 1320px; margin: auto; padding: 6px 20px; }
|
|
@@ -685,6 +1115,11 @@ main { max-width: 1320px; margin: 0 auto; padding: 0 20px 40px; }
|
|
|
685
1115
|
.message > .markdown, .result > .markdown { font-size: 14px; font-weight: 400; }
|
|
686
1116
|
.markdown > :first-child { margin-top: 0; }
|
|
687
1117
|
.markdown > :last-child { margin-bottom: 0; }
|
|
1118
|
+
.reasoning-step { margin:8px 0; border:1px solid var(--line); border-radius:7px; min-width:0; }
|
|
1119
|
+
.reasoning-header { cursor:pointer; padding:8px 10px; color:var(--muted); }
|
|
1120
|
+
.reasoning-title { min-width:0; flex:1; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
1121
|
+
.reasoning-section { margin:0 10px 10px; max-height:30em; overflow:auto; overflow-wrap:anywhere; }
|
|
1122
|
+
.reasoning-part + .reasoning-part { border-top:1px solid var(--line); padding-top:8px; }
|
|
688
1123
|
.markdown p, .markdown ul, .markdown ol, .markdown pre, .markdown blockquote { margin: 0 0 6px; }
|
|
689
1124
|
.markdown ul, .markdown ol { padding-left: 1.5em; }
|
|
690
1125
|
.markdown h1, .markdown h2, .markdown h3 { margin: 12px 0 6px; font-size: 14px; }
|
|
@@ -745,6 +1180,7 @@ summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px;
|
|
|
745
1180
|
.tool-meta span { white-space: nowrap; }
|
|
746
1181
|
.tool-header-timing { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
747
1182
|
.tool-header-timing:empty, .tool-footer-timing:empty { display: none; }
|
|
1183
|
+
.runtime-error pre { white-space: pre-wrap; overflow-wrap: anywhere; border-left: 2px solid var(--danger); }
|
|
748
1184
|
.error-output, .error-result h2 { color: var(--danger); }
|
|
749
1185
|
.muted, .empty { color: var(--muted); }
|
|
750
1186
|
.notice, .empty { padding: 10px 0; }
|
|
@@ -758,6 +1194,9 @@ summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px;
|
|
|
758
1194
|
.additions { color: var(--success); }
|
|
759
1195
|
.deletions { color: var(--danger); }
|
|
760
1196
|
.file-delta { display: inline-flex; flex: none; gap: 6px; font: 12px "Cascadia Mono", Consolas, monospace; }
|
|
1197
|
+
.detail-message { padding: 8px 12px; color: var(--muted); }
|
|
1198
|
+
.detail-message:empty { display: none; }
|
|
1199
|
+
.detail-message button { margin-left: 8px; cursor: pointer; }
|
|
761
1200
|
.tool-image img { display: block; max-width: 100%; max-height: 720px; }
|
|
762
1201
|
.final-result h2 { color: var(--success); }
|
|
763
1202
|
@media (max-width: 700px) {
|