pi-agent-browser-native 0.1.6 → 0.2.1
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 +24 -0
- package/README.md +99 -5
- package/docs/ARCHITECTURE.md +16 -8
- package/docs/TOOL_CONTRACT.md +27 -17
- package/extensions/agent-browser/index.ts +196 -59
- package/extensions/agent-browser/lib/results/envelope.ts +7 -0
- package/extensions/agent-browser/lib/results/presentation.ts +263 -22
- package/extensions/agent-browser/lib/results/shared.ts +25 -0
- package/extensions/agent-browser/lib/results/snapshot.ts +32 -16
- package/extensions/agent-browser/lib/runtime.ts +158 -32
- package/package.json +1 -1
|
@@ -9,9 +9,18 @@
|
|
|
9
9
|
import { readFile, stat } from "node:fs/promises";
|
|
10
10
|
import { resolve } from "node:path";
|
|
11
11
|
|
|
12
|
-
import type
|
|
12
|
+
import { parseCommandInfo, type CommandInfo } from "../runtime.js";
|
|
13
13
|
import { buildSnapshotPresentation, formatRawSnapshotText, formatSnapshotSummary } from "./snapshot.js";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
type AgentBrowserBatchResult,
|
|
16
|
+
type AgentBrowserEnvelope,
|
|
17
|
+
type BatchFailurePresentationDetails,
|
|
18
|
+
type BatchStepPresentationDetails,
|
|
19
|
+
type ToolPresentation,
|
|
20
|
+
isRecord,
|
|
21
|
+
parsePositiveInteger,
|
|
22
|
+
stringifyUnknown,
|
|
23
|
+
} from "./shared.js";
|
|
15
24
|
|
|
16
25
|
const IMAGE_EXTENSION_TO_MIME_TYPE: Record<string, string> = {
|
|
17
26
|
".gif": "image/gif",
|
|
@@ -23,6 +32,13 @@ const IMAGE_EXTENSION_TO_MIME_TYPE: Record<string, string> = {
|
|
|
23
32
|
|
|
24
33
|
const INLINE_IMAGE_MAX_BYTES_ENV = "PI_AGENT_BROWSER_INLINE_IMAGE_MAX_BYTES";
|
|
25
34
|
const DEFAULT_INLINE_IMAGE_MAX_BYTES = 5 * 1_024 * 1_024;
|
|
35
|
+
const NAVIGATION_SUMMARY_COMMANDS = new Set(["back", "click", "dblclick", "forward", "reload"]);
|
|
36
|
+
const NAVIGATION_SUMMARY_FIELD = "navigationSummary";
|
|
37
|
+
|
|
38
|
+
interface NavigationSummary {
|
|
39
|
+
title?: string;
|
|
40
|
+
url?: string;
|
|
41
|
+
}
|
|
26
42
|
|
|
27
43
|
function getImageMimeType(filePath: string): string | undefined {
|
|
28
44
|
const extension = filePath.toLowerCase().slice(filePath.lastIndexOf("."));
|
|
@@ -90,24 +106,241 @@ function getScreenshotSummary(data: Record<string, unknown>): string | undefined
|
|
|
90
106
|
return typeof data.path === "string" ? `Saved image: ${data.path}` : undefined;
|
|
91
107
|
}
|
|
92
108
|
|
|
93
|
-
function
|
|
94
|
-
return
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
109
|
+
function isNavigationObservableCommand(command: string | undefined): boolean {
|
|
110
|
+
return command !== undefined && NAVIGATION_SUMMARY_COMMANDS.has(command);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isNavigationSummary(value: unknown): value is NavigationSummary {
|
|
114
|
+
return isRecord(value) && (typeof value.title === "string" || typeof value.url === "string");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getNavigationSummary(data: Record<string, unknown>): NavigationSummary | undefined {
|
|
118
|
+
const candidate = data[NAVIGATION_SUMMARY_FIELD];
|
|
119
|
+
return isNavigationSummary(candidate) ? candidate : undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function formatNavigationSummary(summary: NavigationSummary): string | undefined {
|
|
123
|
+
const title = typeof summary.title === "string" && summary.title.trim().length > 0 ? summary.title.trim() : undefined;
|
|
124
|
+
const url = typeof summary.url === "string" && summary.url.trim().length > 0 ? summary.url.trim() : undefined;
|
|
125
|
+
if (!title && !url) return undefined;
|
|
126
|
+
if (title && url) return `${title}\n${url}`;
|
|
127
|
+
return title ?? url;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function stripNavigationSummary(data: Record<string, unknown>): Record<string, unknown> {
|
|
131
|
+
const { [NAVIGATION_SUMMARY_FIELD]: _navigationSummary, ...rest } = data;
|
|
132
|
+
return rest;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function formatNavigationActionResult(data: Record<string, unknown>): string | undefined {
|
|
136
|
+
const actionData = stripNavigationSummary(data);
|
|
137
|
+
const lines: string[] = [];
|
|
138
|
+
if (typeof actionData.clicked === "string" || typeof actionData.clicked === "boolean") {
|
|
139
|
+
lines.push(`Clicked: ${String(actionData.clicked)}`);
|
|
140
|
+
}
|
|
141
|
+
if (typeof actionData.href === "string") {
|
|
142
|
+
lines.push(`Href: ${actionData.href}`);
|
|
143
|
+
}
|
|
144
|
+
if (typeof actionData.navigated === "boolean") {
|
|
145
|
+
lines.push(`Navigated: ${actionData.navigated}`);
|
|
146
|
+
}
|
|
147
|
+
if (lines.length > 0) {
|
|
148
|
+
return lines.join("\n");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const actionText = stringifyUnknown(actionData).trim();
|
|
152
|
+
if (actionText.length === 0 || actionText === "{}") {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
return actionText;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isStringArray(value: unknown): value is string[] {
|
|
159
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function getPresentationText(presentation: ToolPresentation): string {
|
|
163
|
+
return presentation.content
|
|
164
|
+
.filter((part): part is Extract<ToolPresentation["content"][number], { type: "text" }> => part.type === "text")
|
|
165
|
+
.map((part) => part.text.trim())
|
|
166
|
+
.filter((text) => text.length > 0)
|
|
102
167
|
.join("\n\n");
|
|
103
168
|
}
|
|
104
169
|
|
|
170
|
+
function getPresentationImages(presentation: ToolPresentation): Array<Extract<ToolPresentation["content"][number], { type: "image" }>> {
|
|
171
|
+
return presentation.content.filter(
|
|
172
|
+
(part): part is Extract<ToolPresentation["content"][number], { type: "image" }> => part.type === "image",
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function getPresentationPaths(options: {
|
|
177
|
+
primaryPath?: string;
|
|
178
|
+
secondaryPaths?: string[];
|
|
179
|
+
}): string[] {
|
|
180
|
+
return options.secondaryPaths ?? (options.primaryPath ? [options.primaryPath] : []);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function formatBatchStepCommand(command: string[] | undefined, index: number): string {
|
|
184
|
+
return command && command.length > 0 ? command.join(" ") : `step-${index + 1}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function formatBatchStepError(error: unknown): string {
|
|
188
|
+
const errorText = stringifyUnknown(error).trim();
|
|
189
|
+
return errorText.length > 0 ? `Error: ${errorText}` : "Error: batch step failed.";
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function getBatchFailureDetails(steps: Array<{ details: BatchStepPresentationDetails }>): BatchFailurePresentationDetails | undefined {
|
|
193
|
+
const failedSteps = steps.filter((step) => step.details.success === false);
|
|
194
|
+
if (failedSteps.length === 0) {
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
const successCount = steps.length - failedSteps.length;
|
|
198
|
+
return {
|
|
199
|
+
failedStep: failedSteps[0].details,
|
|
200
|
+
failureCount: failedSteps.length,
|
|
201
|
+
successCount,
|
|
202
|
+
totalCount: steps.length,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function buildBatchStepPresentation(options: {
|
|
207
|
+
cwd: string;
|
|
208
|
+
index: number;
|
|
209
|
+
item: AgentBrowserBatchResult;
|
|
210
|
+
}): Promise<{ details: BatchStepPresentationDetails; presentation: ToolPresentation }> {
|
|
211
|
+
const { cwd, index, item } = options;
|
|
212
|
+
const command = isStringArray(item.command) ? item.command : undefined;
|
|
213
|
+
const commandText = formatBatchStepCommand(command, index);
|
|
214
|
+
|
|
215
|
+
if (item.success === false) {
|
|
216
|
+
const errorText = formatBatchStepError(item.error);
|
|
217
|
+
const presentation: ToolPresentation = {
|
|
218
|
+
content: [{ type: "text", text: errorText }],
|
|
219
|
+
summary: errorText,
|
|
220
|
+
};
|
|
221
|
+
return {
|
|
222
|
+
details: {
|
|
223
|
+
command,
|
|
224
|
+
commandText,
|
|
225
|
+
data: item.error,
|
|
226
|
+
index,
|
|
227
|
+
success: false,
|
|
228
|
+
summary: errorText,
|
|
229
|
+
text: errorText,
|
|
230
|
+
},
|
|
231
|
+
presentation,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const presentation = await buildToolPresentation({
|
|
236
|
+
commandInfo: parseCommandInfo(command ?? []),
|
|
237
|
+
cwd,
|
|
238
|
+
envelope: { data: item.result, success: true },
|
|
239
|
+
});
|
|
240
|
+
const fullOutputPaths = getPresentationPaths({
|
|
241
|
+
primaryPath: presentation.fullOutputPath,
|
|
242
|
+
secondaryPaths: presentation.fullOutputPaths,
|
|
243
|
+
});
|
|
244
|
+
const imagePaths = getPresentationPaths({
|
|
245
|
+
primaryPath: presentation.imagePath,
|
|
246
|
+
secondaryPaths: presentation.imagePaths,
|
|
247
|
+
});
|
|
248
|
+
const text = getPresentationText(presentation) || presentation.summary;
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
details: {
|
|
252
|
+
command,
|
|
253
|
+
commandText,
|
|
254
|
+
data: presentation.data,
|
|
255
|
+
fullOutputPath: fullOutputPaths[0],
|
|
256
|
+
fullOutputPaths: fullOutputPaths.length > 0 ? fullOutputPaths : undefined,
|
|
257
|
+
imagePath: imagePaths[0],
|
|
258
|
+
imagePaths: imagePaths.length > 0 ? imagePaths : undefined,
|
|
259
|
+
index,
|
|
260
|
+
success: true,
|
|
261
|
+
summary: presentation.summary,
|
|
262
|
+
text,
|
|
263
|
+
},
|
|
264
|
+
presentation,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function buildBatchPresentation(options: {
|
|
269
|
+
cwd: string;
|
|
270
|
+
data: AgentBrowserBatchResult[];
|
|
271
|
+
summary: string;
|
|
272
|
+
}): Promise<ToolPresentation> {
|
|
273
|
+
const { cwd, data, summary } = options;
|
|
274
|
+
const steps: Array<{ details: BatchStepPresentationDetails; presentation: ToolPresentation }> = [];
|
|
275
|
+
for (const [index, item] of data.entries()) {
|
|
276
|
+
steps.push(await buildBatchStepPresentation({ cwd, index, item }));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const batchFailure = getBatchFailureDetails(steps);
|
|
280
|
+
const images = steps.flatMap((step) => getPresentationImages(step.presentation));
|
|
281
|
+
const fullOutputPaths = steps.flatMap((step) => getPresentationPaths({
|
|
282
|
+
primaryPath: step.presentation.fullOutputPath,
|
|
283
|
+
secondaryPaths: step.presentation.fullOutputPaths,
|
|
284
|
+
}));
|
|
285
|
+
const imagePaths = steps.flatMap((step) => getPresentationPaths({
|
|
286
|
+
primaryPath: step.presentation.imagePath,
|
|
287
|
+
secondaryPaths: step.presentation.imagePaths,
|
|
288
|
+
}));
|
|
289
|
+
const stepText =
|
|
290
|
+
steps.length === 0
|
|
291
|
+
? "(no batch steps)"
|
|
292
|
+
: steps
|
|
293
|
+
.map(({ details, presentation }) => {
|
|
294
|
+
const inlineImageCount = getPresentationImages(presentation).length;
|
|
295
|
+
const status = details.success ? "succeeded" : "failed";
|
|
296
|
+
const lines = [`Step ${details.index + 1} — ${details.commandText} (${status})`];
|
|
297
|
+
if (details.text.length > 0) {
|
|
298
|
+
lines.push(details.text);
|
|
299
|
+
}
|
|
300
|
+
if (inlineImageCount > 0) {
|
|
301
|
+
lines.push(`(${inlineImageCount} inline image attachment${inlineImageCount === 1 ? "" : "s"} below)`);
|
|
302
|
+
}
|
|
303
|
+
return lines.join("\n");
|
|
304
|
+
})
|
|
305
|
+
.join("\n\n");
|
|
306
|
+
const failureHeader =
|
|
307
|
+
batchFailure === undefined
|
|
308
|
+
? undefined
|
|
309
|
+
: [
|
|
310
|
+
summary,
|
|
311
|
+
`First failing step: ${batchFailure.failedStep.index + 1} — ${batchFailure.failedStep.commandText}`,
|
|
312
|
+
batchFailure.failureCount > 1
|
|
313
|
+
? `${batchFailure.failureCount} steps failed. See the per-step results below.`
|
|
314
|
+
: "See the per-step results below.",
|
|
315
|
+
].join("\n");
|
|
316
|
+
const text = failureHeader ? `${failureHeader}\n\n${stepText}` : stepText;
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
batchFailure,
|
|
320
|
+
batchSteps: steps.map((step) => step.details),
|
|
321
|
+
content: [{ type: "text", text }, ...images],
|
|
322
|
+
data,
|
|
323
|
+
fullOutputPath: fullOutputPaths[0],
|
|
324
|
+
fullOutputPaths: fullOutputPaths.length > 0 ? fullOutputPaths : undefined,
|
|
325
|
+
imagePath: imagePaths[0],
|
|
326
|
+
imagePaths: imagePaths.length > 0 ? imagePaths : undefined,
|
|
327
|
+
summary,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
105
331
|
function formatSummary(commandInfo: CommandInfo, data: unknown): string {
|
|
106
332
|
if (Array.isArray(data) && commandInfo.command === "batch") {
|
|
107
333
|
const successCount = data.filter((item) => isRecord(item) && item.success !== false).length;
|
|
108
|
-
return `Batch: ${successCount}/${data.length} succeeded`;
|
|
334
|
+
return successCount === data.length ? `Batch: ${successCount}/${data.length} succeeded` : `Batch failed: ${successCount}/${data.length} succeeded`;
|
|
109
335
|
}
|
|
110
336
|
if (isRecord(data)) {
|
|
337
|
+
const navigationSummary = getNavigationSummary(data);
|
|
338
|
+
if (navigationSummary && isNavigationObservableCommand(commandInfo.command)) {
|
|
339
|
+
const navigationText = formatNavigationSummary(navigationSummary);
|
|
340
|
+
if (navigationText) {
|
|
341
|
+
return `${commandInfo.command ?? "navigation"} → ${navigationText.split("\n", 1)[0] ?? navigationText}`;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
111
344
|
if (commandInfo.command === "snapshot") {
|
|
112
345
|
return formatSnapshotSummary(data);
|
|
113
346
|
}
|
|
@@ -136,9 +369,6 @@ function formatSummary(commandInfo: CommandInfo, data: unknown): string {
|
|
|
136
369
|
}
|
|
137
370
|
|
|
138
371
|
function formatContentText(commandInfo: CommandInfo, data: unknown): string {
|
|
139
|
-
if (Array.isArray(data) && commandInfo.command === "batch") {
|
|
140
|
-
return formatBatchContent(data as AgentBrowserBatchResult[]);
|
|
141
|
-
}
|
|
142
372
|
if (typeof data === "string") {
|
|
143
373
|
return data;
|
|
144
374
|
}
|
|
@@ -149,6 +379,15 @@ function formatContentText(commandInfo: CommandInfo, data: unknown): string {
|
|
|
149
379
|
return stringifyUnknown(data);
|
|
150
380
|
}
|
|
151
381
|
|
|
382
|
+
const navigationSummary = getNavigationSummary(data);
|
|
383
|
+
if (navigationSummary && isNavigationObservableCommand(commandInfo.command)) {
|
|
384
|
+
const navigationText = formatNavigationSummary(navigationSummary);
|
|
385
|
+
if (navigationText) {
|
|
386
|
+
const actionText = formatNavigationActionResult(data);
|
|
387
|
+
return actionText ? `${actionText}\n\nCurrent page:\n${navigationText}` : `Current page:\n${navigationText}`;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
152
391
|
if (commandInfo.command === "snapshot") {
|
|
153
392
|
return formatRawSnapshotText(data);
|
|
154
393
|
}
|
|
@@ -232,13 +471,15 @@ export async function buildToolPresentation(options: {
|
|
|
232
471
|
const data = envelope?.data;
|
|
233
472
|
const summary = formatSummary(commandInfo, data);
|
|
234
473
|
const presentation =
|
|
235
|
-
commandInfo.command === "
|
|
236
|
-
? await
|
|
237
|
-
:
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
474
|
+
commandInfo.command === "batch" && Array.isArray(data)
|
|
475
|
+
? await buildBatchPresentation({ cwd, data: data as AgentBrowserBatchResult[], summary })
|
|
476
|
+
: commandInfo.command === "snapshot" && isRecord(data)
|
|
477
|
+
? await buildSnapshotPresentation(data)
|
|
478
|
+
: {
|
|
479
|
+
content: [{ type: "text" as const, text: formatContentText(commandInfo, data) }],
|
|
480
|
+
data,
|
|
481
|
+
summary,
|
|
482
|
+
};
|
|
242
483
|
|
|
243
484
|
const imagePath = extractImagePath(cwd, data);
|
|
244
485
|
if (!imagePath) {
|
|
@@ -19,11 +19,36 @@ export interface AgentBrowserBatchResult {
|
|
|
19
19
|
success?: boolean;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
export interface BatchStepPresentationDetails {
|
|
23
|
+
command?: string[];
|
|
24
|
+
commandText: string;
|
|
25
|
+
data?: unknown;
|
|
26
|
+
fullOutputPath?: string;
|
|
27
|
+
fullOutputPaths?: string[];
|
|
28
|
+
imagePath?: string;
|
|
29
|
+
imagePaths?: string[];
|
|
30
|
+
index: number;
|
|
31
|
+
success: boolean;
|
|
32
|
+
summary: string;
|
|
33
|
+
text: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface BatchFailurePresentationDetails {
|
|
37
|
+
failedStep: BatchStepPresentationDetails;
|
|
38
|
+
failureCount: number;
|
|
39
|
+
successCount: number;
|
|
40
|
+
totalCount: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
22
43
|
export interface ToolPresentation {
|
|
44
|
+
batchFailure?: BatchFailurePresentationDetails;
|
|
45
|
+
batchSteps?: BatchStepPresentationDetails[];
|
|
23
46
|
content: Array<{ text: string; type: "text" } | { data: string; mimeType: string; type: "image" }>;
|
|
24
47
|
data?: unknown;
|
|
25
48
|
fullOutputPath?: string;
|
|
49
|
+
fullOutputPaths?: string[];
|
|
26
50
|
imagePath?: string;
|
|
51
|
+
imagePaths?: string[];
|
|
27
52
|
summary: string;
|
|
28
53
|
}
|
|
29
54
|
|
|
@@ -12,11 +12,13 @@ import { type ToolPresentation, compareRefIds, countLines, isRecord, normalizeWh
|
|
|
12
12
|
const SNAPSHOT_INLINE_MAX_CHARS = 6_000;
|
|
13
13
|
const SNAPSHOT_INLINE_MAX_LINES = 80;
|
|
14
14
|
const SNAPSHOT_INLINE_MAX_REFS = 60;
|
|
15
|
-
const SNAPSHOT_PRIMARY_PREVIEW_LINES =
|
|
16
|
-
const SNAPSHOT_SECTION_PREVIEW_LINES =
|
|
17
|
-
const SNAPSHOT_MAX_ADDITIONAL_SECTIONS =
|
|
18
|
-
const SNAPSHOT_KEY_REF_MAX_LINES =
|
|
19
|
-
const SNAPSHOT_OTHER_REF_MAX_LINES =
|
|
15
|
+
const SNAPSHOT_PRIMARY_PREVIEW_LINES = 8;
|
|
16
|
+
const SNAPSHOT_SECTION_PREVIEW_LINES = 2;
|
|
17
|
+
const SNAPSHOT_MAX_ADDITIONAL_SECTIONS = 2;
|
|
18
|
+
const SNAPSHOT_KEY_REF_MAX_LINES = 8;
|
|
19
|
+
const SNAPSHOT_OTHER_REF_MAX_LINES = 4;
|
|
20
|
+
const SNAPSHOT_ROLE_COUNT_MAX_ENTRIES = 4;
|
|
21
|
+
const SNAPSHOT_FALLBACK_PREVIEW_MAX_LINES = 12;
|
|
20
22
|
const SNAPSHOT_NAME_MAX_CHARS = 96;
|
|
21
23
|
const SNAPSHOT_LINE_MAX_CHARS = 140;
|
|
22
24
|
const SNAPSHOT_SPILL_FILE_PREFIX = "pi-agent-browser-snapshot";
|
|
@@ -165,7 +167,12 @@ function formatRoleCounts(roleCounts: Record<string, number>): string | undefine
|
|
|
165
167
|
if (right[1] !== left[1]) return right[1] - left[1];
|
|
166
168
|
return getRolePriority(left[0]) - getRolePriority(right[0]);
|
|
167
169
|
});
|
|
168
|
-
|
|
170
|
+
const visibleEntries = ordered.slice(0, SNAPSHOT_ROLE_COUNT_MAX_ENTRIES).map(([role, count]) => `${role} ${count}`);
|
|
171
|
+
const omittedEntries = Math.max(0, ordered.length - visibleEntries.length);
|
|
172
|
+
if (omittedEntries > 0) {
|
|
173
|
+
visibleEntries.push(`+${omittedEntries} more`);
|
|
174
|
+
}
|
|
175
|
+
return visibleEntries.join(", ");
|
|
169
176
|
}
|
|
170
177
|
|
|
171
178
|
function parseSnapshotLines(snapshot: string): SnapshotLine[] {
|
|
@@ -373,10 +380,10 @@ function buildSegmentPreview(segment: SnapshotSegment, maxLines: number): Snapsh
|
|
|
373
380
|
|
|
374
381
|
function buildFallbackSnapshotOutline(snapshotLines: SnapshotLine[]): SnapshotPreview {
|
|
375
382
|
const selected = new Set<number>();
|
|
376
|
-
for (let index = 0; index < snapshotLines.length && selected.size <
|
|
383
|
+
for (let index = 0; index < snapshotLines.length && selected.size < 4; index += 1) {
|
|
377
384
|
if (!isNoiseSnapshotLine(snapshotLines[index])) selected.add(index);
|
|
378
385
|
}
|
|
379
|
-
for (let index = 0; index < snapshotLines.length && selected.size <
|
|
386
|
+
for (let index = 0; index < snapshotLines.length && selected.size < SNAPSHOT_FALLBACK_PREVIEW_MAX_LINES; index += 1) {
|
|
380
387
|
const line = snapshotLines[index];
|
|
381
388
|
if (isNoiseSnapshotLine(line)) continue;
|
|
382
389
|
if (SNAPSHOT_SIGNAL_ROLES.has(line.role) || line.ref || line.name.length > 0) {
|
|
@@ -385,7 +392,7 @@ function buildFallbackSnapshotOutline(snapshotLines: SnapshotLine[]): SnapshotPr
|
|
|
385
392
|
}
|
|
386
393
|
const chosenLines = [...selected]
|
|
387
394
|
.sort((left, right) => left - right)
|
|
388
|
-
.slice(0,
|
|
395
|
+
.slice(0, SNAPSHOT_FALLBACK_PREVIEW_MAX_LINES)
|
|
389
396
|
.map((index) => snapshotLines[index]);
|
|
390
397
|
return {
|
|
391
398
|
omittedCount: Math.max(0, snapshotLines.length - chosenLines.length),
|
|
@@ -508,6 +515,8 @@ export async function buildSnapshotPresentation(data: Record<string, unknown>):
|
|
|
508
515
|
const snapshotSegments = useStructuredPreview ? buildSnapshotSegments(snapshotLines) : [];
|
|
509
516
|
const primarySegment = useStructuredPreview ? choosePrimarySegment(snapshotSegments) : undefined;
|
|
510
517
|
const additionalSegments = useStructuredPreview ? chooseAdditionalSegments(snapshotSegments, primarySegment) : [];
|
|
518
|
+
const additionalSegmentCount = useStructuredPreview && primarySegment ? Math.max(0, snapshotSegments.length - 1) : 0;
|
|
519
|
+
const omittedAdditionalSectionCount = Math.max(0, additionalSegmentCount - additionalSegments.length);
|
|
511
520
|
const primaryPreview = primarySegment ? buildSegmentPreview(primarySegment, SNAPSHOT_PRIMARY_PREVIEW_LINES) : undefined;
|
|
512
521
|
const additionalPreviews = additionalSegments
|
|
513
522
|
.map((segment) => ({
|
|
@@ -548,11 +557,9 @@ export async function buildSnapshotPresentation(data: Record<string, unknown>):
|
|
|
548
557
|
const lines: string[] = [
|
|
549
558
|
`Origin: ${origin}`,
|
|
550
559
|
`Refs: ${refEntries.length}`,
|
|
551
|
-
...(roleCountsText ? [`
|
|
560
|
+
...(roleCountsText ? [`Top roles: ${roleCountsText}`] : []),
|
|
552
561
|
"",
|
|
553
|
-
|
|
554
|
-
? `Compact snapshot view. Full raw snapshot: ${fullOutputPath}`
|
|
555
|
-
: `Compact snapshot view. Full raw snapshot unavailable: ${spillErrorText ?? "temp spill file could not be created."}`,
|
|
562
|
+
"Compact snapshot view.",
|
|
556
563
|
];
|
|
557
564
|
|
|
558
565
|
if (fallbackPreview) {
|
|
@@ -581,6 +588,9 @@ export async function buildSnapshotPresentation(data: Record<string, unknown>):
|
|
|
581
588
|
lines.push(`- ... (${preview.omittedCount} more lines in this section)`);
|
|
582
589
|
}
|
|
583
590
|
});
|
|
591
|
+
if (omittedAdditionalSectionCount > 0) {
|
|
592
|
+
lines.push(`- ... (${omittedAdditionalSectionCount} more sections omitted)`);
|
|
593
|
+
}
|
|
584
594
|
}
|
|
585
595
|
}
|
|
586
596
|
|
|
@@ -589,11 +599,16 @@ export async function buildSnapshotPresentation(data: Record<string, unknown>):
|
|
|
589
599
|
lines.push("", "Other refs:", ...otherRefEntries.map(formatCompactRef));
|
|
590
600
|
}
|
|
591
601
|
if (omittedOtherRefs > 0) {
|
|
592
|
-
lines.push(
|
|
593
|
-
`- ... (${omittedOtherRefs} additional refs ${fullOutputPath ? "in the full snapshot file" : "were omitted with the full raw snapshot"})`,
|
|
594
|
-
);
|
|
602
|
+
lines.push(`- ... (${omittedOtherRefs} additional refs omitted)`);
|
|
595
603
|
}
|
|
596
604
|
|
|
605
|
+
lines.push(
|
|
606
|
+
"",
|
|
607
|
+
fullOutputPath
|
|
608
|
+
? "Full raw snapshot path is available in details.fullOutputPath."
|
|
609
|
+
: `Full raw snapshot unavailable: ${spillErrorText ?? "temp spill file could not be created."}`,
|
|
610
|
+
);
|
|
611
|
+
|
|
597
612
|
return {
|
|
598
613
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
599
614
|
data: {
|
|
@@ -603,6 +618,7 @@ export async function buildSnapshotPresentation(data: Record<string, unknown>):
|
|
|
603
618
|
previewMode: fallbackPreview ? "outline" : "structured",
|
|
604
619
|
spillError: spillErrorText,
|
|
605
620
|
previewRefIds: [...previewRefIds],
|
|
621
|
+
additionalSectionsOmitted: omittedAdditionalSectionCount,
|
|
606
622
|
previewSections: [
|
|
607
623
|
...(primarySegment
|
|
608
624
|
? [
|