@keyou007/agent-bot 0.1.24-alpha.4 → 0.1.24-alpha.6
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 +59 -0
- package/README.md +42 -6
- package/README.zh.md +42 -6
- package/SECURITY.md +12 -0
- package/config.example.yaml +5 -0
- package/dist/cli/controlProtocol.js.map +1 -1
- package/dist/cli/serverStatus.js +1 -0
- package/dist/cli/serverStatus.js.map +1 -1
- package/dist/cli.js +6 -1
- package/dist/cli.js.map +1 -1
- package/dist/codex/AppServerPermissions.js +34 -0
- package/dist/codex/AppServerPermissions.js.map +1 -0
- package/dist/codex/CodexLocalActivityDetector.js +37 -5
- package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
- package/dist/codex/CodexRuntime.js +238 -127
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/codex/ProviderSwitch.js +12 -13
- package/dist/codex/ProviderSwitch.js.map +1 -1
- package/dist/codex/ThreadWriterProcess.js +19 -5
- package/dist/codex/ThreadWriterProcess.js.map +1 -1
- package/dist/config/AgentExecutionDefaultsStore.js +4 -0
- package/dist/config/AgentExecutionDefaultsStore.js.map +1 -1
- package/dist/config/loadConfig.js +4 -0
- package/dist/config/loadConfig.js.map +1 -1
- package/dist/config/schema.js +3 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/feishu/CardRenderer.js +146 -72
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +1 -0
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +121 -17
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/index.js +43 -6
- package/dist/index.js.map +1 -1
- package/dist/local-files/CsvPreview.js +148 -0
- package/dist/local-files/CsvPreview.js.map +1 -0
- package/dist/local-files/LocalFileViewerServer.js +104 -14
- package/dist/local-files/LocalFileViewerServer.js.map +1 -1
- package/dist/local-files/TurnPreviewPage.js +128 -12
- package/dist/local-files/TurnPreviewPage.js.map +1 -1
- package/dist/presentation/TurnStateReducer.js +80 -6
- package/dist/presentation/TurnStateReducer.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +767 -260
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/proxy/SessionOperationQueue.js +14 -0
- package/dist/proxy/SessionOperationQueue.js.map +1 -0
- package/dist/runtime/AcpRuntimeAdapter.js +4 -2
- package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
- package/dist/state/StateStore.js +22 -0
- package/dist/state/StateStore.js.map +1 -1
- package/dist/state/TurnPreviewJournal.js +19 -7
- package/dist/state/TurnPreviewJournal.js.map +1 -1
- package/dist/state/migrations.js +6 -0
- package/dist/state/migrations.js.map +1 -1
- package/dist/supervision/SafeRestartNotifier.js +1 -0
- package/dist/supervision/SafeRestartNotifier.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +19 -6
- package/dist/supervision/SafeRestartScheduler.js.map +1 -1
- package/dist/terminal/TerminalCards.js +13 -0
- package/dist/terminal/TerminalCards.js.map +1 -0
- package/dist/terminal/TerminalLinks.js +76 -0
- package/dist/terminal/TerminalLinks.js.map +1 -0
- package/dist/terminal/TerminalPage.js +212 -0
- package/dist/terminal/TerminalPage.js.map +1 -0
- package/dist/terminal/TerminalProcess.js +71 -0
- package/dist/terminal/TerminalProcess.js.map +1 -0
- package/dist/terminal/TerminalServer.js +639 -0
- package/dist/terminal/TerminalServer.js.map +1 -0
- package/docs/technical-reference.md +53 -3
- package/docs/technical-reference.zh.md +52 -2
- package/npm-shrinkwrap.json +55 -5
- package/package.json +11 -4
- package/skills/agent-bot/SKILL.md +15 -2
- package/src/cli/controlProtocol.ts +2 -1
- package/src/cli/serverStatus.ts +1 -0
- package/src/cli.ts +6 -1
- package/src/codex/AppServerPermissions.ts +39 -0
- package/src/codex/CodexLocalActivityDetector.ts +34 -4
- package/src/codex/CodexRuntime.ts +243 -139
- package/src/codex/ProviderSwitch.ts +11 -12
- package/src/codex/ThreadWriterProcess.ts +17 -9
- package/src/config/AgentExecutionDefaultsStore.ts +2 -0
- package/src/config/loadConfig.ts +4 -0
- package/src/config/schema.ts +5 -1
- package/src/feishu/CardRenderer.ts +159 -72
- package/src/feishu/FeishuConnector.ts +1 -0
- package/src/feishu/FeishuTurnPresenter.ts +108 -16
- package/src/feishu/types.ts +1 -0
- package/src/index.ts +44 -7
- package/src/local-files/CsvPreview.ts +132 -0
- package/src/local-files/LocalFileViewerServer.ts +102 -15
- package/src/local-files/TurnPreviewPage.ts +131 -13
- package/src/presentation/TurnStateReducer.ts +87 -7
- package/src/presentation/turnViewTypes.ts +9 -0
- package/src/proxy/ProxySessionController.ts +762 -259
- package/src/proxy/SessionOperationQueue.ts +13 -0
- package/src/runtime/AcpRuntimeAdapter.ts +5 -3
- package/src/runtime/types.ts +10 -3
- package/src/state/StateStore.ts +38 -1
- package/src/state/TurnPreviewJournal.ts +19 -7
- package/src/state/migrations.ts +6 -0
- package/src/supervision/SafeRestartNotifier.ts +1 -0
- package/src/supervision/SafeRestartScheduler.ts +18 -6
- package/src/terminal/TerminalCards.ts +18 -0
- package/src/terminal/TerminalLinks.ts +61 -0
- package/src/terminal/TerminalPage.ts +213 -0
- package/src/terminal/TerminalProcess.ts +71 -0
- package/src/terminal/TerminalServer.ts +513 -0
|
@@ -3,12 +3,15 @@ import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import http, { type IncomingMessage, type ServerResponse } from "node:http";
|
|
6
|
+
import type { Duplex } from "node:stream";
|
|
6
7
|
import path from "node:path";
|
|
7
8
|
import hljs from "highlight.js/lib/common";
|
|
8
9
|
import dos from "highlight.js/lib/languages/dos";
|
|
9
10
|
import dockerfile from "highlight.js/lib/languages/dockerfile";
|
|
10
11
|
import powershell from "highlight.js/lib/languages/powershell";
|
|
11
12
|
import MarkdownIt from "markdown-it";
|
|
13
|
+
import { CSV_PREVIEW_CSS, renderCsvPreview, csvSourcePreview } from "./CsvPreview.js";
|
|
14
|
+
import { enableDiagramFences, DIAGRAM_PREVIEW_CLIENT_SCRIPT, DIAGRAM_PREVIEW_CSS } from "./DiagramPreview.js";
|
|
12
15
|
import { isFileUrl, rewriteMarkdownFileLinks } from "./MarkdownFileLinks.js";
|
|
13
16
|
import {
|
|
14
17
|
selectPreferredNetworkAddress,
|
|
@@ -56,6 +59,7 @@ const MARKDOWN_RENDERER = new MarkdownIt({
|
|
|
56
59
|
return `<pre class="markdown-code-block hljs"><code${languageAttribute}>${highlighted}</code></pre>`;
|
|
57
60
|
},
|
|
58
61
|
});
|
|
62
|
+
enableDiagramFences(MARKDOWN_RENDERER);
|
|
59
63
|
MARKDOWN_RENDERER.renderer.rules.table_open = () =>
|
|
60
64
|
'<div class="markdown-table-scroll" role="region" aria-label="Markdown 表格" tabindex="0"><table>\n';
|
|
61
65
|
MARKDOWN_RENDERER.renderer.rules.table_close = () => '</table></div>\n';
|
|
@@ -79,7 +83,7 @@ const HIGHLIGHT_LANGUAGE_BY_FILENAME: Readonly<Record<string, string>> = {
|
|
|
79
83
|
"dockerfile": "dockerfile",
|
|
80
84
|
"makefile": "makefile",
|
|
81
85
|
};
|
|
82
|
-
const VIEWER_CLIENT_SCRIPT = `(() => {
|
|
86
|
+
const VIEWER_CLIENT_SCRIPT = DIAGRAM_PREVIEW_CLIENT_SCRIPT + `(() => {
|
|
83
87
|
const eventsUrl = document.body.dataset.eventsUrl;
|
|
84
88
|
const content = document.getElementById("viewer-content");
|
|
85
89
|
const metadata = document.getElementById("viewer-metadata");
|
|
@@ -157,14 +161,15 @@ const VIEWER_CLIENT_SCRIPT = `(() => {
|
|
|
157
161
|
requestAnimationFrame(positionHashTarget);
|
|
158
162
|
if (!eventsUrl || typeof EventSource !== "function") return;
|
|
159
163
|
|
|
160
|
-
const restoreScroll = (top, left, atBottom, codeScrollLeft,
|
|
164
|
+
const restoreScroll = (top, left, atBottom, codeScrollLeft, tablePositions) => {
|
|
161
165
|
requestAnimationFrame(() => {
|
|
162
166
|
const maxTop = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
|
|
163
167
|
window.scrollTo(left, atBottom ? maxTop : Math.min(top, maxTop));
|
|
164
168
|
const code = content.querySelector(".code");
|
|
165
169
|
if (code) code.scrollLeft = codeScrollLeft;
|
|
166
|
-
content.querySelectorAll(".markdown-table-scroll").forEach((table, index) => {
|
|
167
|
-
table.scrollLeft =
|
|
170
|
+
content.querySelectorAll(".markdown-table-scroll, .csv-table-scroll").forEach((table, index) => {
|
|
171
|
+
table.scrollLeft = tablePositions[index]?.left ?? 0;
|
|
172
|
+
table.scrollTop = tablePositions[index]?.top ?? 0;
|
|
168
173
|
});
|
|
169
174
|
});
|
|
170
175
|
};
|
|
@@ -174,12 +179,40 @@ const VIEWER_CLIENT_SCRIPT = `(() => {
|
|
|
174
179
|
const maxTop = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
|
|
175
180
|
const atBottom = maxTop - top <= 24;
|
|
176
181
|
const codeScrollLeft = content.querySelector(".code")?.scrollLeft ?? 0;
|
|
177
|
-
const
|
|
178
|
-
|
|
182
|
+
const tablePositions = Array.from(content.querySelectorAll(".markdown-table-scroll, .csv-table-scroll"), (table) => ({ left: table.scrollLeft, top: table.scrollTop }));
|
|
183
|
+
const template = document.createElement("template");
|
|
184
|
+
template.innerHTML = nextContent;
|
|
185
|
+
const previousDiagrams = new Set(content.querySelectorAll("[data-diagram]"));
|
|
186
|
+
const nextDiagrams = Array.from(template.content.querySelectorAll("[data-diagram]"));
|
|
187
|
+
const retainedDiagrams = new Map();
|
|
188
|
+
const diagramSource = (block) => block.querySelector(".diagram-source code").textContent;
|
|
189
|
+
// Match unchanged diagrams first so inserted text or diagrams do not reset their state.
|
|
190
|
+
for (const next of nextDiagrams) {
|
|
191
|
+
const current = Array.from(previousDiagrams).find((block) => diagramSource(block) === diagramSource(next));
|
|
192
|
+
if (current) { retainedDiagrams.set(next, current); previousDiagrams.delete(current); }
|
|
193
|
+
}
|
|
194
|
+
for (const next of nextDiagrams) {
|
|
195
|
+
if (retainedDiagrams.has(next)) continue;
|
|
196
|
+
const current = Array.from(previousDiagrams).find((block) => block.dataset.previewKey === next.dataset.previewKey);
|
|
197
|
+
if (current) { retainedDiagrams.set(next, current); previousDiagrams.delete(current); }
|
|
198
|
+
}
|
|
199
|
+
const diagramScroll = [];
|
|
200
|
+
for (const [next, current] of retainedDiagrams) {
|
|
201
|
+
for (const panel of current.querySelectorAll(".diagram-preview, .diagram-source")) {
|
|
202
|
+
diagramScroll.push([panel, panel.scrollLeft, panel.scrollTop]);
|
|
203
|
+
}
|
|
204
|
+
const source = current.querySelector(".diagram-source code");
|
|
205
|
+
if (source.textContent !== diagramSource(next)) source.textContent = diagramSource(next);
|
|
206
|
+
current.dataset.previewKey = next.dataset.previewKey;
|
|
207
|
+
next.replaceWith(current);
|
|
208
|
+
}
|
|
209
|
+
content.replaceChildren(template.content);
|
|
179
210
|
metadata.innerHTML = nextMetadata;
|
|
180
211
|
syncViewMode();
|
|
212
|
+
window.agentBotDiagrams.scan();
|
|
213
|
+
for (const [panel, left, top] of diagramScroll) { panel.scrollLeft = left; panel.scrollTop = top; }
|
|
181
214
|
highlightHashTarget();
|
|
182
|
-
restoreScroll(top, left, atBottom, codeScrollLeft,
|
|
215
|
+
restoreScroll(top, left, atBottom, codeScrollLeft, tablePositions);
|
|
183
216
|
};
|
|
184
217
|
|
|
185
218
|
const source = new EventSource(eventsUrl);
|
|
@@ -187,7 +220,7 @@ const VIEWER_CLIENT_SCRIPT = `(() => {
|
|
|
187
220
|
try {
|
|
188
221
|
const update = JSON.parse(event.data);
|
|
189
222
|
if (typeof update.content === "string" && typeof update.metadata === "string") {
|
|
190
|
-
if (viewSwitch) viewSwitch.hidden = update.viewMode !== "markdown" && update.viewMode !== "html";
|
|
223
|
+
if (viewSwitch) viewSwitch.hidden = update.viewMode !== "markdown" && update.viewMode !== "html" && update.viewMode !== "csv";
|
|
191
224
|
replaceContent(update.content, update.metadata);
|
|
192
225
|
}
|
|
193
226
|
} catch {
|
|
@@ -229,7 +262,7 @@ interface DirectoryViewerEntry {
|
|
|
229
262
|
interface RenderedFileSnapshot {
|
|
230
263
|
content: string;
|
|
231
264
|
metadata: string[];
|
|
232
|
-
viewMode?: "markdown" | "html";
|
|
265
|
+
viewMode?: "markdown" | "html" | "csv";
|
|
233
266
|
}
|
|
234
267
|
|
|
235
268
|
export interface LocalFileViewerServerOptions {
|
|
@@ -251,11 +284,18 @@ export interface LocalFileViewerAddress {
|
|
|
251
284
|
publicConnectionKind?: NetworkConnectionKind;
|
|
252
285
|
}
|
|
253
286
|
|
|
287
|
+
export interface LocalFileViewerService {
|
|
288
|
+
handleRequest(request: IncomingMessage, response: ServerResponse): void;
|
|
289
|
+
handleUpgrade(request: IncomingMessage, socket: Duplex, head: Buffer): void;
|
|
290
|
+
close(): Promise<void>;
|
|
291
|
+
}
|
|
292
|
+
|
|
254
293
|
export class LocalFileViewerServer {
|
|
255
294
|
private server?: http.Server;
|
|
256
295
|
private secret?: Buffer;
|
|
257
296
|
private address?: LocalFileViewerAddress;
|
|
258
297
|
private basePath = "";
|
|
298
|
+
private readonly services = new Map<string, LocalFileViewerService>();
|
|
259
299
|
|
|
260
300
|
constructor(private readonly options: LocalFileViewerServerOptions) {}
|
|
261
301
|
|
|
@@ -283,6 +323,11 @@ export class LocalFileViewerServer {
|
|
|
283
323
|
});
|
|
284
324
|
});
|
|
285
325
|
server.on("clientError", (_error, socket) => socket.end("HTTP/1.1 400 Bad Request\r\n\r\n"));
|
|
326
|
+
server.on("upgrade", (request, socket, head) => {
|
|
327
|
+
const service = this.serviceFor(request);
|
|
328
|
+
if (service) service.handleUpgrade(request, socket, head);
|
|
329
|
+
else socket.end("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
|
|
330
|
+
});
|
|
286
331
|
|
|
287
332
|
let selectedPort = preferredPort;
|
|
288
333
|
try {
|
|
@@ -329,12 +374,36 @@ export class LocalFileViewerServer {
|
|
|
329
374
|
}
|
|
330
375
|
|
|
331
376
|
async close(): Promise<void> {
|
|
377
|
+
await Promise.all([...this.services.values()].map((service) => service.close()));
|
|
378
|
+
this.services.clear();
|
|
332
379
|
const server = this.server;
|
|
333
380
|
this.server = undefined;
|
|
334
381
|
this.address = undefined;
|
|
335
382
|
if (server) await closeServer(server);
|
|
336
383
|
}
|
|
337
384
|
|
|
385
|
+
mountService(name: string, service: LocalFileViewerService): { publicBaseUrl: string; unmount(): void } {
|
|
386
|
+
if (!this.address || !this.server) throw new Error("Preview server is not running.");
|
|
387
|
+
if (!/^[a-z][a-z0-9-]*$/u.test(name) || ["preview", "turn-preview", "assets", "view", "raw"].includes(name)) {
|
|
388
|
+
throw new Error("Invalid Preview service route.");
|
|
389
|
+
}
|
|
390
|
+
const basePath = `${this.basePath}/${name}`;
|
|
391
|
+
if (this.services.has(basePath)) throw new Error("Preview service route is already mounted.");
|
|
392
|
+
this.services.set(basePath, service);
|
|
393
|
+
const url = new URL(this.address.publicBaseUrl);
|
|
394
|
+
url.pathname = basePath;
|
|
395
|
+
return { publicBaseUrl: url.toString(), unmount: () => {
|
|
396
|
+
if (this.services.get(basePath) === service) this.services.delete(basePath);
|
|
397
|
+
} };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
private serviceFor(request: IncomingMessage): LocalFileViewerService | undefined {
|
|
401
|
+
try {
|
|
402
|
+
const pathname = new URL(request.url ?? "/", "http://127.0.0.1").pathname;
|
|
403
|
+
return [...this.services].find(([base]) => pathname === base || pathname.startsWith(`${base}/`))?.[1];
|
|
404
|
+
} catch { return undefined; }
|
|
405
|
+
}
|
|
406
|
+
|
|
338
407
|
createFileUrl(filePath: string, reference?: string): string | undefined {
|
|
339
408
|
if (!this.address || !this.secret) return undefined;
|
|
340
409
|
const absolutePath = normalizeAbsolutePath(filePath);
|
|
@@ -365,6 +434,8 @@ export class LocalFileViewerServer {
|
|
|
365
434
|
}
|
|
366
435
|
|
|
367
436
|
private async handleRequest(request: IncomingMessage, response: ServerResponse): Promise<void> {
|
|
437
|
+
const service = this.serviceFor(request);
|
|
438
|
+
if (service) { service.handleRequest(request, response); return; }
|
|
368
439
|
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
369
440
|
response.setHeader("Allow", "GET, HEAD");
|
|
370
441
|
this.sendHtml(response, 405, errorPage("不支持的请求", "该服务只接受只读请求。"), request.method === "HEAD");
|
|
@@ -401,14 +472,16 @@ export class LocalFileViewerServer {
|
|
|
401
472
|
return;
|
|
402
473
|
}
|
|
403
474
|
const journal = this.options.previewJournal;
|
|
404
|
-
const
|
|
475
|
+
const stored = journal?.has(turnId) ? undefined : this.options.getTurnSnapshot?.(turnId);
|
|
476
|
+
const journalId = isTurnPreviewState(stored) ? stored.previewTurnId ?? turnId : turnId;
|
|
477
|
+
const recorded = journal?.has(journalId);
|
|
405
478
|
if (recorded && requestUrl.searchParams.get("events") === "1") {
|
|
406
|
-
this.serveJournalEvents(request, response,
|
|
479
|
+
this.serveJournalEvents(request, response, journalId, journal!);
|
|
407
480
|
return;
|
|
408
481
|
}
|
|
409
482
|
const detailKey = requestUrl.searchParams.get("detail");
|
|
410
483
|
let snapshot = recorded
|
|
411
|
-
? journal!.load(
|
|
484
|
+
? journal!.load(journalId, detailKey ?? false)
|
|
412
485
|
: this.options.getTurnSnapshot?.(turnId);
|
|
413
486
|
if (!recorded && this.options.loadTurnSnapshot && request.method !== "HEAD"
|
|
414
487
|
&& requestUrl.searchParams.get("events") !== "1" && !requestUrl.searchParams.has("detail")) {
|
|
@@ -435,7 +508,7 @@ export class LocalFileViewerServer {
|
|
|
435
508
|
const after = requestUrl.searchParams.get("detailAfter");
|
|
436
509
|
const outputAppend = recorded && after && tool?.kind === "tool" && tool.tool.kind === "command" && !tool.tool.error
|
|
437
510
|
&& !/repl/i.test(tool.tool.title) && snapshot.previewCursor !== undefined
|
|
438
|
-
? journal!.outputAppend(
|
|
511
|
+
? journal!.outputAppend(journalId, tool.id, Number(after), snapshot.previewCursor) : undefined;
|
|
439
512
|
const detail = outputAppend !== undefined && tool?.kind === "tool"
|
|
440
513
|
&& (snapshot.fullToolOutputs?.[tool.id]?.length ?? 0) > outputAppend.length
|
|
441
514
|
? { key, revision: String(tool.tool.previewRevision), content: "", outputAppend,
|
|
@@ -611,6 +684,7 @@ export class LocalFileViewerServer {
|
|
|
611
684
|
],
|
|
612
685
|
content: snapshot.content,
|
|
613
686
|
viewMode: snapshot.viewMode,
|
|
687
|
+
diagramScriptPath: isMarkdownFile(filePath) ? `${this.basePath}/assets/mermaid.js`.replace(/\/{2,}/gu, "/") : undefined,
|
|
614
688
|
liveUpdates: {
|
|
615
689
|
eventsUrl: eventsUrl.toString(),
|
|
616
690
|
scriptPath: `${this.basePath}/assets/viewer.js`.replace(/\/{2,}/gu, "/"),
|
|
@@ -640,6 +714,11 @@ export class LocalFileViewerServer {
|
|
|
640
714
|
appendUrlQueryParameter(renderUrl, "render", "html");
|
|
641
715
|
appendUrlQueryParameter(renderUrl, "version", `${stat.mtimeMs}-${stat.ctimeMs}-${stat.size}`);
|
|
642
716
|
content = `<iframe class="html-preview" data-view-panel="rendered" title="HTML 预览" sandbox="allow-scripts" referrerpolicy="no-referrer" src="${escapeAttribute(renderUrl.toString())}"></iframe><section data-view-panel="code">${notice}${renderText(preview.text, filePath)}</section>`;
|
|
717
|
+
} else if (extension === ".csv") {
|
|
718
|
+
viewMode = "csv";
|
|
719
|
+
const source = csvSourcePreview(preview.text);
|
|
720
|
+
const sourceNotice = source.truncated ? '<div class="notice">代码仅显示前 2000 个物理行,可打开原始文件或下载完整文件。</div>' : "";
|
|
721
|
+
content = `${renderCsvPreview(preview.text, preview.truncated)}<section data-view-panel="code">${notice}${sourceNotice}${renderText(source.text, filePath)}</section>`;
|
|
643
722
|
} else {
|
|
644
723
|
viewMode = isMarkdownFile(filePath) ? "markdown" : undefined;
|
|
645
724
|
const previewContent = viewMode === "markdown"
|
|
@@ -927,6 +1006,9 @@ export class LocalFileViewerServer {
|
|
|
927
1006
|
if (renderHtml) {
|
|
928
1007
|
// A response-level sandbox also isolates this document when opened outside the viewer iframe.
|
|
929
1008
|
response.setHeader("Content-Security-Policy", "sandbox allow-scripts; default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:; media-src data: blob:; font-src data:; base-uri 'none'; form-action 'none'; frame-ancestors 'self'");
|
|
1009
|
+
} else {
|
|
1010
|
+
// Raw HTML/SVG/XML must not gain the shared origin's storage or execution privileges.
|
|
1011
|
+
response.setHeader("Content-Security-Policy", "sandbox; default-src 'none'; style-src 'unsafe-inline'; img-src data: blob:; media-src data: blob:; base-uri 'none'; form-action 'none'; frame-ancestors 'self'");
|
|
930
1012
|
}
|
|
931
1013
|
response.setHeader("Accept-Ranges", "bytes");
|
|
932
1014
|
response.setHeader("Cache-Control", "no-store");
|
|
@@ -984,6 +1066,7 @@ function renderViewerPage(input: {
|
|
|
984
1066
|
actions: Array<{ href: string; label: string }>;
|
|
985
1067
|
content: string;
|
|
986
1068
|
viewMode?: RenderedFileSnapshot["viewMode"];
|
|
1069
|
+
diagramScriptPath?: string;
|
|
987
1070
|
liveUpdates?: { eventsUrl: string; scriptPath: string };
|
|
988
1071
|
}): string {
|
|
989
1072
|
const metadata = renderMetadata(input.metadata);
|
|
@@ -1008,7 +1091,7 @@ function renderViewerPage(input: {
|
|
|
1008
1091
|
<meta charset="utf-8">
|
|
1009
1092
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1010
1093
|
<title>${escapeHtml(input.title)} · Agent Bot</title>
|
|
1011
|
-
<style
|
|
1094
|
+
<style>${input.diagramScriptPath ? DIAGRAM_PREVIEW_CSS : ""}
|
|
1012
1095
|
:root { --viewer-header-offset: 56px; --viewer-code-font: "Cascadia Mono", "JetBrains Mono", "SFMono-Regular", Consolas, "Liberation Mono", "Sarasa Mono SC", "Noto Sans Mono CJK SC", "Microsoft YaHei Mono", NSimSun, monospace; color-scheme: light dark; font-family: Inter, "Segoe UI", system-ui, sans-serif; }
|
|
1013
1096
|
* { box-sizing: border-box; }
|
|
1014
1097
|
body { margin: 0; background: #f5f7fa; color: #1f2329; }
|
|
@@ -1056,6 +1139,8 @@ function renderViewerPage(input: {
|
|
|
1056
1139
|
.markdown-body code { padding: .14em .35em; border-radius: 4px; background: #f2f3f5; font-family: var(--viewer-code-font); font-size: .9em; }
|
|
1057
1140
|
.markdown-body .markdown-code-block { margin: 0 0 1em; padding: 14px 16px; overflow: auto; border-radius: 6px; background: #f6f8fa; line-height: 1.55; }
|
|
1058
1141
|
.markdown-body .markdown-code-block code { padding: 0; background: transparent; font-size: 13px; }
|
|
1142
|
+
.markdown-body .diagram-source { padding: 14px 16px; }
|
|
1143
|
+
.markdown-body .diagram-source code { padding: 0; background: transparent; font-size: 13px; }
|
|
1059
1144
|
.markdown-table-scroll { max-width: 100%; margin: 0 0 1em; overflow-x: auto; }
|
|
1060
1145
|
.markdown-table-scroll:focus-visible { outline: 2px solid #1456f0; outline-offset: 2px; }
|
|
1061
1146
|
.markdown-body table { width: max-content; margin: 0; border-collapse: collapse; white-space: nowrap; overflow-wrap: normal; word-break: normal; }
|
|
@@ -1063,6 +1148,7 @@ function renderViewerPage(input: {
|
|
|
1063
1148
|
.markdown-body th { background: #f5f7fa; }
|
|
1064
1149
|
.markdown-body img { max-width: 100%; height: auto; }
|
|
1065
1150
|
.markdown-body hr { height: 1px; margin: 1.5em 0; border: 0; background: #dfe3e8; }
|
|
1151
|
+
${CSV_PREVIEW_CSS}
|
|
1066
1152
|
.media { display: grid; place-items: center; min-height: 220px; }
|
|
1067
1153
|
.media img, .media video { max-width: 100%; max-height: calc(100vh - 70px); }
|
|
1068
1154
|
.media audio { width: min(720px, 100%); }
|
|
@@ -1092,6 +1178,7 @@ function renderViewerPage(input: {
|
|
|
1092
1178
|
.view-switch button.is-active { background: #3a3b3d; color: #8ab4ff; box-shadow: none; }
|
|
1093
1179
|
.code, .directory-list { background: #202124; color: #e8eaed; border-color: #3a3b3d; }
|
|
1094
1180
|
.markdown-body { background: #202124; color: #e8eaed; }
|
|
1181
|
+
.markdown-body .diagram { --border: #55585c; }
|
|
1095
1182
|
.markdown-body h1, .markdown-body h2 { border-color: #3a3b3d; }
|
|
1096
1183
|
.markdown-body blockquote { color: #a6a9ad; border-color: #55585c; }
|
|
1097
1184
|
.markdown-body a { color: #8ab4ff; }
|
|
@@ -1115,7 +1202,7 @@ function renderViewerPage(input: {
|
|
|
1115
1202
|
}
|
|
1116
1203
|
</style>
|
|
1117
1204
|
</head>
|
|
1118
|
-
<body${input.viewMode ? ' data-view-mode="rendered"' : ""}${liveAttributes}>
|
|
1205
|
+
<body${input.viewMode ? ' data-view-mode="rendered"' : ""}${liveAttributes}${input.diagramScriptPath ? ` data-diagram-script="${escapeAttribute(input.diagramScriptPath)}"` : ""}>
|
|
1119
1206
|
<header id="viewer-header">
|
|
1120
1207
|
<h1 id="viewer-title" data-file-path="${escapeAttribute(input.filePath)}">${escapeHtml(input.filePath)}</h1>
|
|
1121
1208
|
<div class="meta"><span class="metadata-values" id="viewer-metadata">${metadata}</span>${toolbar}</div>
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { approvalActivityId } from "../presentation/TurnStateReducer.js";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { enableDiagramFences, DIAGRAM_PREVIEW_CLIENT_SCRIPT, DIAGRAM_PREVIEW_CSS } from "./DiagramPreview.js";
|
|
4
5
|
import MarkdownIt from "markdown-it";
|
|
5
6
|
import { highlightPreviewCode, PREVIEW_SYNTAX_CSS } from "./PreviewSyntaxHighlight.js";
|
|
6
|
-
import type { ToolState } from "../runtime/types.js";
|
|
7
|
+
import type { ApprovalRequest, ToolState } from "../runtime/types.js";
|
|
7
8
|
import type { FileSummary, TurnActivity, TurnReasoningItem, TurnViewState, TurnViewStatus } from "../presentation/turnViewTypes.js";
|
|
8
9
|
import { displayFilePath, displayToolCommand, formatShellCommandForDisplay, toolStatusIcon } from "../feishu/CardRenderer.js";
|
|
9
10
|
import { turnReasoningItems } from "../presentation/turnReasoning.js";
|
|
@@ -41,6 +42,8 @@ const PREVIEW_LABELS = {
|
|
|
41
42
|
error: "错误",
|
|
42
43
|
reasoning: "思考",
|
|
43
44
|
finalAnswer: "最终回答",
|
|
45
|
+
downloadMarkdown: "下载 Markdown",
|
|
46
|
+
copyMarkdown: "复制 Markdown",
|
|
44
47
|
generating: "回答生成中",
|
|
45
48
|
command: "命令",
|
|
46
49
|
result: "结果",
|
|
@@ -51,12 +54,13 @@ const PREVIEW_LABELS = {
|
|
|
51
54
|
noToolDetails: "没有可显示的工具详情。",
|
|
52
55
|
fileChanges: "文件变更",
|
|
53
56
|
model: "模型",
|
|
57
|
+
modelCalls: "模型调用",
|
|
58
|
+
modelCallsDescription: "本轮有效 Token 用量更新次数",
|
|
59
|
+
contextTokens: "上下文",
|
|
54
60
|
tools: "个工具",
|
|
55
61
|
turnTokens: "本轮",
|
|
56
62
|
totalTokens: "总计",
|
|
57
63
|
nonCachedTokens: "非缓存",
|
|
58
|
-
compactionTokens: "压缩",
|
|
59
|
-
compactionAfterTokens: "压缩后",
|
|
60
64
|
characters: "字符",
|
|
61
65
|
truncated: "较早的活动已被运行时截断;本页展示 Agent Bot 当前保存的完整快照。",
|
|
62
66
|
statuses: {
|
|
@@ -82,6 +86,8 @@ const PREVIEW_LABELS = {
|
|
|
82
86
|
error: "Error",
|
|
83
87
|
reasoning: "Reasoning",
|
|
84
88
|
finalAnswer: "Final answer",
|
|
89
|
+
downloadMarkdown: "Download Markdown",
|
|
90
|
+
copyMarkdown: "Copy Markdown",
|
|
85
91
|
generating: "Generating answer",
|
|
86
92
|
command: "Command",
|
|
87
93
|
result: "Result",
|
|
@@ -92,12 +98,13 @@ const PREVIEW_LABELS = {
|
|
|
92
98
|
noToolDetails: "No tool details available.",
|
|
93
99
|
fileChanges: "File changes",
|
|
94
100
|
model: "Model",
|
|
101
|
+
modelCalls: "Model calls",
|
|
102
|
+
modelCallsDescription: "Effective token usage updates in this turn",
|
|
103
|
+
contextTokens: "Context",
|
|
95
104
|
tools: "tools",
|
|
96
105
|
turnTokens: "Turn",
|
|
97
106
|
totalTokens: "Total",
|
|
98
107
|
nonCachedTokens: "Non-cached",
|
|
99
|
-
compactionTokens: "Compaction",
|
|
100
|
-
compactionAfterTokens: "After compaction",
|
|
101
108
|
characters: "characters",
|
|
102
109
|
truncated: "Earlier activities were truncated by the runtime; this page shows the complete snapshot currently saved by Agent Bot.",
|
|
103
110
|
statuses: {
|
|
@@ -152,7 +159,76 @@ export const TURN_PREVIEW_CLIENT_SCRIPT = DIAGRAM_PREVIEW_CLIENT_SCRIPT + `(() =
|
|
|
152
159
|
loading: isChinese ? "正在加载…" : "Loading…",
|
|
153
160
|
loadFailed: isChinese ? "加载失败,请重试。" : "Could not load details.",
|
|
154
161
|
retry: isChinese ? "重试" : "Retry",
|
|
162
|
+
copied: isChinese ? "已复制 Markdown" : "Markdown copied",
|
|
163
|
+
downloaded: isChinese ? "已开始下载" : "Download started",
|
|
164
|
+
copyFailed: isChinese ? "复制失败,请检查浏览器剪贴板权限后重试。" : "Copy failed. Check clipboard permissions and retry.",
|
|
165
|
+
downloadFailed: isChinese ? "下载失败,请重试。" : "Download failed. Please retry.",
|
|
155
166
|
};
|
|
167
|
+
const copyMarkdownFallback = (text) => {
|
|
168
|
+
const active = document.activeElement;
|
|
169
|
+
const selection = window.getSelection?.();
|
|
170
|
+
const ranges = selection ? Array.from({ length: selection.rangeCount }, (_, i) => selection.getRangeAt(i).cloneRange()) : [];
|
|
171
|
+
const textarea = document.createElement("textarea");
|
|
172
|
+
textarea.value = text;
|
|
173
|
+
textarea.readOnly = true;
|
|
174
|
+
textarea.style.cssText = "position:fixed;top:0;left:0;width:1px;height:1px;opacity:0;pointer-events:none";
|
|
175
|
+
document.body.append(textarea);
|
|
176
|
+
try {
|
|
177
|
+
textarea.focus({ preventScroll: true });
|
|
178
|
+
textarea.select();
|
|
179
|
+
textarea.setSelectionRange(0, text.length);
|
|
180
|
+
if (!document.execCommand?.("copy")) throw new Error("Clipboard unavailable");
|
|
181
|
+
} finally {
|
|
182
|
+
textarea.remove();
|
|
183
|
+
active?.focus?.({ preventScroll: true });
|
|
184
|
+
if (selection) {
|
|
185
|
+
selection.removeAllRanges();
|
|
186
|
+
for (const range of ranges) selection.addRange(range);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
const copyMarkdown = async (text) => {
|
|
191
|
+
if (window.navigator?.clipboard?.writeText) {
|
|
192
|
+
try { await window.navigator.clipboard.writeText(text); return; } catch {}
|
|
193
|
+
}
|
|
194
|
+
copyMarkdownFallback(text);
|
|
195
|
+
};
|
|
196
|
+
const answerActionsInFlight = new WeakSet();
|
|
197
|
+
content.addEventListener("click", async (event) => {
|
|
198
|
+
const button = event.target.closest?.("button[data-answer-action]");
|
|
199
|
+
const section = button?.closest(".final-result[data-answer-markdown]");
|
|
200
|
+
if (!section || !content.contains(section) || answerActionsInFlight.has(button)) return;
|
|
201
|
+
const action = button.dataset.answerAction;
|
|
202
|
+
if (action !== "copy" && action !== "download") return;
|
|
203
|
+
event.preventDefault();
|
|
204
|
+
const feedback = section.querySelector("[data-answer-feedback]");
|
|
205
|
+
answerActionsInFlight.add(button);
|
|
206
|
+
button.disabled = true;
|
|
207
|
+
try {
|
|
208
|
+
const markdown = new TextDecoder("utf-8", { ignoreBOM: true }).decode(Uint8Array.from(atob(section.dataset.answerMarkdown), (char) => char.charCodeAt(0)));
|
|
209
|
+
if (action === "copy") await copyMarkdown(markdown);
|
|
210
|
+
else {
|
|
211
|
+
const url = URL.createObjectURL(new Blob([markdown], { type: "text/markdown;charset=utf-8" }));
|
|
212
|
+
const link = document.createElement("a");
|
|
213
|
+
try {
|
|
214
|
+
link.href = url;
|
|
215
|
+
link.download = section.dataset.answerFilename;
|
|
216
|
+
link.hidden = true;
|
|
217
|
+
document.body.append(link);
|
|
218
|
+
link.click();
|
|
219
|
+
} finally {
|
|
220
|
+
link.remove();
|
|
221
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (feedback) feedback.textContent = action === "copy" ? labels.copied : labels.downloaded;
|
|
225
|
+
} catch {
|
|
226
|
+
if (feedback) feedback.textContent = action === "copy" ? labels.copyFailed : labels.downloadFailed;
|
|
227
|
+
} finally {
|
|
228
|
+
button.disabled = false;
|
|
229
|
+
answerActionsInFlight.delete(button);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
156
232
|
|
|
157
233
|
let source;
|
|
158
234
|
const terminalAtLoad = document.body.dataset.terminal === "true";
|
|
@@ -589,10 +665,10 @@ export function renderTurnPreviewSnapshot(
|
|
|
589
665
|
: "",
|
|
590
666
|
`<section class="timeline" data-preview-key="timeline"${timelineContent ? "" : " hidden"}>${timelineContent}</section>`,
|
|
591
667
|
state.fileSummary.length > 0 ? renderFileSummary(state, language, resolveFileUrl, options.deferDetails) : "",
|
|
592
|
-
state.approval ?
|
|
668
|
+
state.approval ? renderApprovalNotice(state.approval, renderText, language) : "",
|
|
593
669
|
state.error ? `<section class="result error-result" data-preview-key="error"><h2>${escapeHtml(labels.error)}</h2><pre>${escapeHtml(state.error)}</pre></section>` : "",
|
|
594
670
|
state.finalResponse
|
|
595
|
-
?
|
|
671
|
+
? renderFinalAnswer(state.finalResponse, state.turnId, renderText, language)
|
|
596
672
|
: state.assistantText
|
|
597
673
|
? `<section class="result" data-preview-key="result"><h2>${escapeHtml(labels.generating)}</h2><div class="markdown">${renderText(state.assistantText)}</div></section>`
|
|
598
674
|
: "",
|
|
@@ -606,6 +682,34 @@ export function renderTurnPreviewSnapshot(
|
|
|
606
682
|
};
|
|
607
683
|
}
|
|
608
684
|
|
|
685
|
+
function renderApprovalNotice(request: ApprovalRequest, renderText: (value: string) => string, language: TurnPreviewLanguage): string {
|
|
686
|
+
const command = request.command?.trim();
|
|
687
|
+
const commandApproval = command && request.kind !== "mode_change";
|
|
688
|
+
const title = commandApproval ? language === "zh" ? "命令执行确认" : "Command approval" : request.title;
|
|
689
|
+
const reason = commandApproval
|
|
690
|
+
? request.reason?.trim() || (request.title.trim() !== command ? request.title : "")
|
|
691
|
+
: request.reason ?? "";
|
|
692
|
+
const description = commandApproval
|
|
693
|
+
? `<p class="approval-reason">${escapeHtml(reason === command ? "" : reason)}</p>`
|
|
694
|
+
: `<div class="markdown">${renderText(reason)}</div>`;
|
|
695
|
+
const details = commandApproval
|
|
696
|
+
? `<details class="approval-details" data-activity-id="approval:${escapeAttribute(request.id)}"><summary>${language === "zh" ? "查看完整命令" : "View full command"}</summary><pre data-scroll-id="approval:${escapeAttribute(request.id)}:command">${escapeHtml(request.command!)}</pre></details>` : "";
|
|
697
|
+
const instruction = request.automatic
|
|
698
|
+
? language === "zh" ? "自动执行模式:正在自动同意。" : "Auto mode: approving automatically."
|
|
699
|
+
: language === "zh" ? "请在飞书任务卡片中确认或拒绝。" : "Approve or reject using the task card in Feishu.";
|
|
700
|
+
return `<section class="notice" data-preview-key="approval"><h2>${escapeHtml(title)}</h2>${description}${details}<p>${instruction}</p></section>`;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function renderFinalAnswer(markdown: string, turnId: string, renderText: (value: string) => string, language: TurnPreviewLanguage): string {
|
|
704
|
+
const labels = previewLabels(language);
|
|
705
|
+
const filename = `turn-${turnId.replace(/[^A-Za-z0-9_-]/gu, "_").slice(0, 80) || "answer"}.md`;
|
|
706
|
+
const icon = (paths: string): string => `<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">${paths}</svg>`;
|
|
707
|
+
const button = (action: string, label: string, paths: string): string => `<button type="button" class="answer-action" data-answer-action="${action}" title="${escapeAttribute(label)}" aria-label="${escapeAttribute(label)}">${icon(paths)}</button>`;
|
|
708
|
+
const actions = button("download", labels.downloadMarkdown, '<path d="M12 3v12m-5-5 5 5 5-5M5 16v5h14v-5"/>')
|
|
709
|
+
+ button("copy", labels.copyMarkdown, '<rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h3"/>');
|
|
710
|
+
return `<section class="result final-result" data-preview-key="result" data-answer-markdown="${Buffer.from(markdown, "utf8").toString("base64")}" data-answer-filename="${filename}"><div class="result-heading"><h2>${escapeHtml(labels.finalAnswer)}</h2>${actions}<span class="answer-feedback" data-answer-feedback role="status" aria-live="polite"></span></div><div class="markdown">${renderText(markdown)}</div></section>`;
|
|
711
|
+
}
|
|
712
|
+
|
|
609
713
|
function renderPlan(state: TurnViewState, language: TurnPreviewLanguage): string {
|
|
610
714
|
const labels = previewLabels(language);
|
|
611
715
|
const completed = state.plan.filter((step) => step.status === "completed").length;
|
|
@@ -626,7 +730,8 @@ function renderTimeline(
|
|
|
626
730
|
projectCwd?: string,
|
|
627
731
|
deferDetails = false,
|
|
628
732
|
): string {
|
|
629
|
-
const activities = state.
|
|
733
|
+
const activities = state.approval
|
|
734
|
+
? state.activities.filter((a) => a.id !== approvalActivityId(state.turnId, state.approval!.id)) : state.activities ?? [];
|
|
630
735
|
const positions = new Map(activities.map((activity, index) => [activity.id, index]));
|
|
631
736
|
const reasoning = new Map<number, TurnReasoningItem[]>();
|
|
632
737
|
for (const item of turnReasoningItems(state)) {
|
|
@@ -1092,7 +1197,7 @@ function renderMetadata(state: TurnViewState, language: TurnPreviewLanguage): st
|
|
|
1092
1197
|
? state.startedAt + state.durationMs
|
|
1093
1198
|
: undefined);
|
|
1094
1199
|
const elapsed = `<span>${escapeHtml(labels.elapsed)}<strong${isTerminal(state.status) ? "" : ` data-live-elapsed data-started-at="${state.startedAt}"`}>${completedAt === undefined && isTerminal(state.status) ? escapeHtml(labels.unknown) : formatDuration((completedAt ?? Date.now()) - state.startedAt)}</strong></span>`;
|
|
1095
|
-
|
|
1200
|
+
const fields = [
|
|
1096
1201
|
elapsed,
|
|
1097
1202
|
state.modelProvider?.trim() ? `<span title="Provider">${escapeHtml(state.modelProvider.trim())}</span>` : "",
|
|
1098
1203
|
state.model?.trim() ? `<span title="${escapeAttribute(labels.model)}">${escapeHtml(state.model.trim())}</span>` : "",
|
|
@@ -1100,11 +1205,13 @@ function renderMetadata(state: TurnViewState, language: TurnPreviewLanguage): st
|
|
|
1100
1205
|
? `<span title="${escapeAttribute(labels.turnTokens)}">${formatTokenCount(state.totalTokens)} tokens</span>`
|
|
1101
1206
|
: `<span title="${escapeAttribute(labels.nonCachedTokens)}: ${formatNumber(state.totalTokens)} tokens">${escapeHtml(labels.nonCachedTokens)} ${formatTokenCount(state.totalTokens)} tokens</span>`,
|
|
1102
1207
|
state.totalTokensIncludingCache === undefined ? "" : `<span title="${escapeAttribute(labels.totalTokens)}: ${formatNumber(state.totalTokensIncludingCache)} tokens">${escapeHtml(labels.totalTokens)} ${formatTokenCount(state.totalTokensIncludingCache)} tokens</span>`,
|
|
1103
|
-
state.
|
|
1104
|
-
? `<span title="${escapeAttribute(labels.
|
|
1105
|
-
|
|
1208
|
+
state.latestContextTokens !== undefined && Number.isFinite(state.latestContextTokens) && state.latestContextTokens >= 0
|
|
1209
|
+
? `<span title="${escapeAttribute(labels.contextTokens)}: ${formatNumber(state.latestContextTokens)} tokens">${escapeHtml(labels.contextTokens)} ${formatTokenCount(state.latestContextTokens)} tokens</span>` : "",
|
|
1210
|
+
state.modelCallCount !== undefined && Number.isSafeInteger(state.modelCallCount) && state.modelCallCount >= 0
|
|
1211
|
+
? `<span title="${escapeAttribute(labels.modelCallsDescription)}">${escapeHtml(labels.modelCalls)} ${formatNumber(state.modelCallCount)}${language === "zh" ? " 次" : ""}</span>` : "",
|
|
1106
1212
|
(state.totalToolCount ?? 0) > 0 ? `<span>${formatNumber(state.totalToolCount ?? 0)} ${escapeHtml(labels.tools)}</span>` : "",
|
|
1107
|
-
].filter(Boolean)
|
|
1213
|
+
].filter(Boolean);
|
|
1214
|
+
return fields.join("");
|
|
1108
1215
|
}
|
|
1109
1216
|
|
|
1110
1217
|
function previewTitle(state: TurnViewState): string {
|
|
@@ -1310,6 +1417,17 @@ summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px;
|
|
|
1310
1417
|
.detail-message button { margin-left: 8px; cursor: pointer; }
|
|
1311
1418
|
.tool-image img { display: block; max-width: 100%; max-height: 720px; }
|
|
1312
1419
|
.final-result h2 { color: var(--success); }
|
|
1420
|
+
.approval-reason { white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
1421
|
+
.approval-reason:empty { display: none; }
|
|
1422
|
+
.approval-details { margin: 8px 0; }
|
|
1423
|
+
.approval-details pre { max-height: 30em; }
|
|
1424
|
+
.result-heading { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; margin-bottom: 8px; }
|
|
1425
|
+
.result-heading h2 { margin: 0 4px 0 0; }
|
|
1426
|
+
.answer-action { display: inline-flex; align-items: center; justify-content: center; flex: none; width: 30px; height: 30px; padding: 0; border: 1px solid transparent; border-radius: 5px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
1427
|
+
.answer-action:hover { color: var(--accent); background: var(--surface); }
|
|
1428
|
+
.answer-action:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
1429
|
+
.answer-action:disabled { opacity: 0.5; cursor: wait; }
|
|
1430
|
+
.answer-feedback { min-width: 0; color: var(--muted); font-size: 12px; overflow-wrap: anywhere; }
|
|
1313
1431
|
@media (max-width: 700px) {
|
|
1314
1432
|
.header-inner { padding: 5px 12px; }
|
|
1315
1433
|
.header-top { flex-basis: 100%; }
|