@vizzly-testing/cli 0.35.3-beta.0 → 0.35.3-beta.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/README.md +7 -4
- package/dist/api/client.js +20 -5
- package/dist/api/core.js +17 -3
- package/dist/cli.js +11 -51
- package/dist/commands/baselines.js +0 -7
- package/dist/commands/config-cmd.js +0 -7
- package/dist/commands/context.js +248 -306
- package/dist/commands/doctor.js +0 -8
- package/dist/commands/init.js +7 -12
- package/dist/commands/login.js +0 -13
- package/dist/commands/logout.js +0 -12
- package/dist/commands/orgs.js +0 -9
- package/dist/commands/whoami.js +0 -12
- package/dist/context/local-workspace-provider.js +276 -30
- package/dist/plugin-loader.js +1 -1
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +17 -17
- package/dist/server/routers/events.js +4 -4
- package/dist/utils/visual-context-normalizers.js +12 -3
- package/package.json +11 -10
- package/skills/vizzly/references/cli-context.md +5 -1
- package/dist/config/index.js +0 -13
- package/dist/project/index.js +0 -13
- package/dist/tdd/index.js +0 -19
- package/dist/tdd/services/baseline-downloader.js +0 -151
- package/dist/tdd/services/hotspot-service.js +0 -58
- package/dist/utils/environment.js +0 -109
package/dist/commands/context.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { createApiClient as defaultCreateApiClient, getBuildContext as defaultGetBuildContext, getComparisonContext as defaultGetComparisonContext, getReviewQueueContext as defaultGetReviewQueueContext, getScreenshotContext as defaultGetScreenshotContext, getSimilarFingerprintContext as defaultGetSimilarFingerprintContext } from '../api/index.js';
|
|
6
6
|
import { createLocalWorkspaceContextProvider as defaultCreateLocalWorkspaceContextProvider } from '../context/local-workspace-provider.js';
|
|
7
7
|
import { resolveContextSource as defaultResolveContextSource } from '../context/provider-resolver.js';
|
|
8
|
+
import { VizzlyError } from '../errors/vizzly-error.js';
|
|
8
9
|
import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
|
|
9
10
|
import * as defaultOutput from '../utils/output.js';
|
|
10
11
|
import { readSession as defaultReadSession } from '../utils/session.js';
|
|
@@ -16,7 +17,7 @@ function buildSourceErrorMessage() {
|
|
|
16
17
|
return '--source must be one of: auto, cloud, local';
|
|
17
18
|
}
|
|
18
19
|
function buildIncludeErrorMessage() {
|
|
19
|
-
return '--include must contain only:
|
|
20
|
+
return '--include must contain only: diffs';
|
|
20
21
|
}
|
|
21
22
|
function validateLimitRange(value, flagName, {
|
|
22
23
|
min = 1,
|
|
@@ -30,15 +31,81 @@ function validateLimitRange(value, flagName, {
|
|
|
30
31
|
}
|
|
31
32
|
return [];
|
|
32
33
|
}
|
|
33
|
-
function
|
|
34
|
+
function validateCursor(value) {
|
|
34
35
|
if (value == null) {
|
|
35
36
|
return [];
|
|
36
37
|
}
|
|
38
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
39
|
+
return ['--cursor must be a non-empty opaque cursor'];
|
|
40
|
+
}
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
function validateOffset(value) {
|
|
44
|
+
if (value == null) return [];
|
|
37
45
|
if (!Number.isInteger(value) || value < 0) {
|
|
38
46
|
return ['--offset must be a non-negative integer'];
|
|
39
47
|
}
|
|
40
48
|
return [];
|
|
41
49
|
}
|
|
50
|
+
let COMPACT_CONTEXT_LIMIT = 10;
|
|
51
|
+
let COMPACT_SUMMARY_MAX_BYTES = 64 * 1024;
|
|
52
|
+
let COMPACT_DIFFS_MAX_BYTES = 512 * 1024;
|
|
53
|
+
function getCompactPage(collection, label) {
|
|
54
|
+
if (!collection || !Array.isArray(collection.items) || !collection.page) {
|
|
55
|
+
throw new VizzlyError(`Vizzly returned an invalid compact ${label} collection`, 'COMPACT_CONTEXT_INVALID');
|
|
56
|
+
}
|
|
57
|
+
if (collection.items.length > COMPACT_CONTEXT_LIMIT) {
|
|
58
|
+
throw new VizzlyError(`Vizzly returned more than ${COMPACT_CONTEXT_LIMIT} compact ${label} items`, 'COMPACT_CONTEXT_INVALID');
|
|
59
|
+
}
|
|
60
|
+
if (collection.items.some(item => !item || typeof item !== 'object')) {
|
|
61
|
+
throw new VizzlyError(`Vizzly returned an invalid item in compact ${label}`, 'COMPACT_CONTEXT_INVALID');
|
|
62
|
+
}
|
|
63
|
+
let {
|
|
64
|
+
page
|
|
65
|
+
} = collection;
|
|
66
|
+
let validLimit = Number.isInteger(page.limit) && page.limit >= 1 && page.limit <= COMPACT_CONTEXT_LIMIT;
|
|
67
|
+
let validReturned = Number.isInteger(page.returned) && page.returned === collection.items.length;
|
|
68
|
+
let validTotal = page.total == null || Number.isInteger(page.total) && page.total >= 0 && page.total >= page.returned;
|
|
69
|
+
let validHasMore = typeof page.has_more === 'boolean';
|
|
70
|
+
let validCursor = page.next_cursor == null || typeof page.next_cursor === 'string' && page.next_cursor.length > 0;
|
|
71
|
+
if (!validLimit || !validReturned || !validTotal || !validHasMore || !validCursor) {
|
|
72
|
+
throw new VizzlyError(`Vizzly returned invalid pagination facts for compact ${label}`, 'COMPACT_CONTEXT_INVALID');
|
|
73
|
+
}
|
|
74
|
+
if (page.included === false && collection.items.length > 0) {
|
|
75
|
+
throw new VizzlyError(`Vizzly returned items for an omitted compact ${label} collection`, 'COMPACT_CONTEXT_INVALID');
|
|
76
|
+
}
|
|
77
|
+
if (page.has_more !== Boolean(page.next_cursor)) {
|
|
78
|
+
throw new VizzlyError(`Vizzly returned contradictory pagination facts for compact ${label}`, 'COMPACT_CONTEXT_INVALID');
|
|
79
|
+
}
|
|
80
|
+
return collection;
|
|
81
|
+
}
|
|
82
|
+
function validateCompactContext(context, resource) {
|
|
83
|
+
if (!context || typeof context !== 'object') {
|
|
84
|
+
throw new VizzlyError(`Vizzly returned an invalid compact ${resource} context`, 'COMPACT_CONTEXT_INVALID');
|
|
85
|
+
}
|
|
86
|
+
if (context.resource !== `${resource}_context` || !context[resource]?.id) {
|
|
87
|
+
throw new VizzlyError(`Vizzly returned an invalid compact ${resource} context`, 'COMPACT_CONTEXT_INVALID');
|
|
88
|
+
}
|
|
89
|
+
if (resource === 'build') {
|
|
90
|
+
getCompactPage(context.evidence, 'build evidence');
|
|
91
|
+
} else {
|
|
92
|
+
let history = context.history || {};
|
|
93
|
+
getCompactPage(history.similar_by_fingerprint, 'similar history');
|
|
94
|
+
getCompactPage(history.recent_by_name, 'recent history');
|
|
95
|
+
}
|
|
96
|
+
return context;
|
|
97
|
+
}
|
|
98
|
+
function validateCompactOutputSize(payload, resource, details) {
|
|
99
|
+
let bytes = Buffer.byteLength(JSON.stringify(payload));
|
|
100
|
+
let maxBytes = details === 'diffs' ? COMPACT_DIFFS_MAX_BYTES : COMPACT_SUMMARY_MAX_BYTES;
|
|
101
|
+
if (bytes > maxBytes) {
|
|
102
|
+
throw new VizzlyError(`Vizzly produced an oversized compact ${resource} context`, 'COMPACT_CONTEXT_OVERSIZED', {
|
|
103
|
+
bytes,
|
|
104
|
+
max_bytes: maxBytes
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return payload;
|
|
108
|
+
}
|
|
42
109
|
function validateSourceOption(value) {
|
|
43
110
|
if (value == null) {
|
|
44
111
|
return [];
|
|
@@ -55,8 +122,23 @@ function parseIncludeOption(value) {
|
|
|
55
122
|
let rawItems = Array.isArray(value) ? value : String(value).split(',');
|
|
56
123
|
return rawItems.map(item => item.trim()).filter(Boolean);
|
|
57
124
|
}
|
|
125
|
+
function buildCompactContextRequest(options = {}, globalOptions = {}) {
|
|
126
|
+
let include = parseIncludeOption(options.include);
|
|
127
|
+
let compact = !options.full && (!globalOptions.json || options.agent);
|
|
128
|
+
let details = include.includes('diffs') ? 'diffs' : 'summary';
|
|
129
|
+
return {
|
|
130
|
+
compact,
|
|
131
|
+
details,
|
|
132
|
+
include,
|
|
133
|
+
query: compact ? {
|
|
134
|
+
details,
|
|
135
|
+
limit: COMPACT_CONTEXT_LIMIT,
|
|
136
|
+
cursor: options.cursor
|
|
137
|
+
} : undefined
|
|
138
|
+
};
|
|
139
|
+
}
|
|
58
140
|
function validateIncludeOption(value) {
|
|
59
|
-
let allowed = new Set(['
|
|
141
|
+
let allowed = new Set(['diffs']);
|
|
60
142
|
let invalid = parseIncludeOption(value).filter(item => !allowed.has(item));
|
|
61
143
|
return invalid.length > 0 ? [buildIncludeErrorMessage()] : [];
|
|
62
144
|
}
|
|
@@ -92,7 +174,7 @@ function createClient(config, createApiClient) {
|
|
|
92
174
|
function attachContextSource(context, source) {
|
|
93
175
|
return {
|
|
94
176
|
...context,
|
|
95
|
-
source
|
|
177
|
+
source
|
|
96
178
|
};
|
|
97
179
|
}
|
|
98
180
|
async function loadContextConfig(globalOptions, options, deps) {
|
|
@@ -239,12 +321,6 @@ function getStatusTone(colors, status) {
|
|
|
239
321
|
function getComparisonDisplayState(comparison = {}) {
|
|
240
322
|
return comparison.result || comparison.status || 'unknown';
|
|
241
323
|
}
|
|
242
|
-
function isChangedComparison(comparison = {}) {
|
|
243
|
-
return ['changed', 'failed', 'pending'].includes(getComparisonDisplayState(comparison));
|
|
244
|
-
}
|
|
245
|
-
function isNewComparison(comparison = {}) {
|
|
246
|
-
return getComparisonDisplayState(comparison) === 'new';
|
|
247
|
-
}
|
|
248
324
|
function getComparisonName(comparison = {}) {
|
|
249
325
|
return comparison.screenshot_name || comparison.screenshot?.name || comparison.name || comparison.id || 'unknown screenshot';
|
|
250
326
|
}
|
|
@@ -255,139 +331,6 @@ function getComparisonFingerprint(comparison = {}) {
|
|
|
255
331
|
return comparison.diff?.fingerprint_hash || comparison.analysis?.fingerprint_hash || null;
|
|
256
332
|
}
|
|
257
333
|
|
|
258
|
-
/**
|
|
259
|
-
* Decide whether a comparison belongs in the agent handoff.
|
|
260
|
-
*
|
|
261
|
-
* Explicit API review state wins because an already-reviewed visual change is
|
|
262
|
-
* not actionable. Legacy result fallback keeps older flat responses useful
|
|
263
|
-
* only when the server did not supply that review fact.
|
|
264
|
-
*
|
|
265
|
-
* @param {Object} comparison - Normalized comparison record.
|
|
266
|
-
* @returns {boolean} Whether the record is actionable evidence.
|
|
267
|
-
*/
|
|
268
|
-
function isEvidenceCandidate(comparison = {}) {
|
|
269
|
-
if (comparison.needs_review != null) {
|
|
270
|
-
return comparison.needs_review === true;
|
|
271
|
-
}
|
|
272
|
-
return ['changed', 'new', 'failed', 'error'].includes(comparison.result);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Keep the group facts needed to understand one comparison in isolation.
|
|
277
|
-
*
|
|
278
|
-
* Repeating this small server-owned summary on each record avoids returning
|
|
279
|
-
* the full, potentially unbounded group tree in compact agent output.
|
|
280
|
-
*
|
|
281
|
-
* @param {Object} group - Normalized screenshot group.
|
|
282
|
-
* @returns {Object} Compact API-backed aggregate facts for the group.
|
|
283
|
-
*/
|
|
284
|
-
function buildEvidenceGroup(group = {}) {
|
|
285
|
-
let aggregate = group.aggregate_status || {};
|
|
286
|
-
return {
|
|
287
|
-
name: group.name || null,
|
|
288
|
-
variant_count: group.variant_count ?? null,
|
|
289
|
-
needs_review_count: aggregate.needs_review_count ?? null,
|
|
290
|
-
failed_count: aggregate.failed_count ?? null,
|
|
291
|
-
max_diff_percentage: aggregate.max_diff_percentage ?? null
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Shape one normalized comparison for the bounded evidence queue.
|
|
297
|
-
*
|
|
298
|
-
* The projection keeps visual result, review state, render assets, and
|
|
299
|
-
* Honeydiff facts together so an agent can reason about a diff without
|
|
300
|
-
* joining separate collections client-side.
|
|
301
|
-
*
|
|
302
|
-
* @param {Object} comparison - Normalized comparison record.
|
|
303
|
-
* @param {Object} group - Normalized group containing the comparison.
|
|
304
|
-
* @returns {Object} One self-contained comparison evidence record.
|
|
305
|
-
*/
|
|
306
|
-
function buildComparisonEvidence(comparison = {}, group = {}) {
|
|
307
|
-
return {
|
|
308
|
-
kind: 'comparison',
|
|
309
|
-
id: comparison.id,
|
|
310
|
-
name: comparison.name,
|
|
311
|
-
result: comparison.result,
|
|
312
|
-
status: comparison.status,
|
|
313
|
-
review_state: comparison.review_state,
|
|
314
|
-
visual_review: comparison.visual_review,
|
|
315
|
-
approval_status: comparison.approval_status,
|
|
316
|
-
needs_review: comparison.needs_review,
|
|
317
|
-
is_flaky: comparison.is_flaky,
|
|
318
|
-
group: buildEvidenceGroup(group),
|
|
319
|
-
screenshot: comparison.screenshot,
|
|
320
|
-
baseline: comparison.baseline,
|
|
321
|
-
diff: comparison.diff
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* Represent a capture failure without pretending it is a comparison.
|
|
327
|
-
*
|
|
328
|
-
* Failed screenshots have useful render evidence but no comparison ID. The
|
|
329
|
-
* explicit kind and null ID prevent suggested commands from sending a
|
|
330
|
-
* screenshot identifier to the comparison endpoint.
|
|
331
|
-
*
|
|
332
|
-
* @param {Object} capture - Normalized failed screenshot capture.
|
|
333
|
-
* @returns {Object} One failed-capture evidence record.
|
|
334
|
-
*/
|
|
335
|
-
function buildFailedCaptureEvidence(capture = {}) {
|
|
336
|
-
return {
|
|
337
|
-
...buildComparisonEvidence(capture, {
|
|
338
|
-
name: capture.name
|
|
339
|
-
}),
|
|
340
|
-
kind: 'failed_capture',
|
|
341
|
-
id: null,
|
|
342
|
-
error_message: capture.error_message
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* Read actionable variants without overriding a server-reviewed group.
|
|
348
|
-
*
|
|
349
|
-
* A false aggregate is authoritative even when a partial variant payload
|
|
350
|
-
* appears pending, which prevents the client from reopening completed work.
|
|
351
|
-
*
|
|
352
|
-
* @param {Object} group - Normalized screenshot group.
|
|
353
|
-
* @returns {Object[]} Actionable variants in API order.
|
|
354
|
-
*/
|
|
355
|
-
function getGroupEvidence(group = {}) {
|
|
356
|
-
if (group.aggregate_status?.needs_review === false) {
|
|
357
|
-
return [];
|
|
358
|
-
}
|
|
359
|
-
return (group.variants || []).filter(isEvidenceCandidate);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Interleave actionable variants across groups in their original API order.
|
|
364
|
-
*
|
|
365
|
-
* Taking one variant per group before taking second variants preserves useful
|
|
366
|
-
* breadth when the final handoff is capped and one screenshot has many device
|
|
367
|
-
* or browser variants.
|
|
368
|
-
*
|
|
369
|
-
* @param {Object[]} groups - Normalized screenshot groups.
|
|
370
|
-
* @returns {Object[]} Self-contained evidence records in breadth-first order.
|
|
371
|
-
*/
|
|
372
|
-
function selectBreadthFirstEvidence(groups = []) {
|
|
373
|
-
let candidatesByGroup = groups.map(getGroupEvidence);
|
|
374
|
-
let evidence = [];
|
|
375
|
-
let variantIndex = 0;
|
|
376
|
-
let remaining = candidatesByGroup.some(candidates => candidates.length > 0);
|
|
377
|
-
while (remaining) {
|
|
378
|
-
remaining = false;
|
|
379
|
-
for (let groupIndex = 0; groupIndex < groups.length; groupIndex += 1) {
|
|
380
|
-
let comparison = candidatesByGroup[groupIndex][variantIndex];
|
|
381
|
-
if (comparison) {
|
|
382
|
-
evidence.push(buildComparisonEvidence(comparison, groups[groupIndex]));
|
|
383
|
-
remaining = true;
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
variantIndex += 1;
|
|
387
|
-
}
|
|
388
|
-
return evidence;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
334
|
/**
|
|
392
335
|
* Quote a value only when a suggested command needs shell protection.
|
|
393
336
|
*
|
|
@@ -430,43 +373,41 @@ function appendContextSource(command, context = {}) {
|
|
|
430
373
|
let source = isLocalContext(context) ? 'local' : 'cloud';
|
|
431
374
|
return `${command} --source ${source}`;
|
|
432
375
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
*
|
|
437
|
-
* Executable commands replace generic client-authored advice. They let an
|
|
438
|
-
* agent ask the API for deeper comparison, history, or raw diff context, and
|
|
439
|
-
* only suggest the full build when the bounded queue omitted records.
|
|
440
|
-
*
|
|
441
|
-
* @param {Object} context - Normalized build context.
|
|
442
|
-
* @param {Object[]} evidence - Evidence included in the compact handoff.
|
|
443
|
-
* @param {boolean} truncated - Whether additional evidence was omitted.
|
|
444
|
-
* @returns {{label: string, command: string}[]} Suggested CLI commands.
|
|
445
|
-
*/
|
|
446
|
-
function buildSuggestedCommands(context = {}, evidence = [], truncated = false) {
|
|
376
|
+
function buildCompactBuildCommands(context = {}, include = [], cursor = null) {
|
|
377
|
+
let evidence = context.evidence?.items || [];
|
|
378
|
+
let page = context.evidence?.page || {};
|
|
447
379
|
let commands = [];
|
|
448
|
-
let firstComparison = evidence.find(item => item.kind === 'comparison' && item.id);
|
|
449
|
-
let firstNamedEvidence = evidence.find(item => item.name);
|
|
450
380
|
let buildTarget = isLocalContext(context) ? 'current' : context.build?.id || null;
|
|
451
|
-
|
|
381
|
+
let firstComparison = evidence.find(item => item.type === 'comparison' && item.id);
|
|
382
|
+
let firstNamedEvidence = evidence.find(item => item.screenshot_name);
|
|
383
|
+
if (firstComparison) {
|
|
452
384
|
commands.push({
|
|
453
385
|
label: 'Inspect comparison context',
|
|
454
|
-
command: appendContextSource(`vizzly --json context comparison ${quoteCommandArgument(firstComparison.id)}`, context)
|
|
386
|
+
command: appendContextSource(`vizzly --json context comparison ${quoteCommandArgument(firstComparison.id)} --agent`, context)
|
|
455
387
|
});
|
|
456
388
|
}
|
|
457
|
-
if (firstNamedEvidence
|
|
389
|
+
if (firstNamedEvidence) {
|
|
390
|
+
let screenshotName = firstNamedEvidence.screenshot_name;
|
|
458
391
|
commands.push({
|
|
459
392
|
label: 'Inspect screenshot history',
|
|
460
|
-
command: appendContextSource(`vizzly --json context screenshot ${quoteCommandArgument(
|
|
393
|
+
command: appendContextSource(`vizzly --json context screenshot ${quoteCommandArgument(screenshotName)}`, context)
|
|
461
394
|
});
|
|
462
395
|
}
|
|
463
396
|
if (buildTarget && evidence.length > 0) {
|
|
397
|
+
let cursorFlag = cursor ? ` --cursor ${quoteCommandArgument(cursor)}` : '';
|
|
464
398
|
commands.push({
|
|
465
399
|
label: 'Load raw diff diagnostics',
|
|
466
|
-
command: appendContextSource(`vizzly --json context build ${quoteCommandArgument(buildTarget)} --agent --include diffs`, context)
|
|
400
|
+
command: appendContextSource(`vizzly --json context build ${quoteCommandArgument(buildTarget)} --agent --include diffs${cursorFlag}`, context)
|
|
467
401
|
});
|
|
468
402
|
}
|
|
469
|
-
if (buildTarget &&
|
|
403
|
+
if (buildTarget && page.has_more && page.next_cursor) {
|
|
404
|
+
let includeFlag = include.length > 0 ? ` --include ${include.join(',')}` : '';
|
|
405
|
+
commands.push({
|
|
406
|
+
label: 'Load next evidence page',
|
|
407
|
+
command: appendContextSource(`vizzly --json context build ${quoteCommandArgument(buildTarget)} --agent --cursor ${quoteCommandArgument(page.next_cursor)}${includeFlag}`, context)
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
if (buildTarget) {
|
|
470
411
|
commands.push({
|
|
471
412
|
label: 'Load full build context',
|
|
472
413
|
command: appendContextSource(`vizzly --json context build ${quoteCommandArgument(buildTarget)} --agent --full`, context)
|
|
@@ -474,68 +415,118 @@ function buildSuggestedCommands(context = {}, evidence = [], truncated = false)
|
|
|
474
415
|
}
|
|
475
416
|
return commands;
|
|
476
417
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
*
|
|
481
|
-
* Status, summaries, review facts, assets, and Honeydiff values pass through
|
|
482
|
-
* normalization from the server. The client owns only the bounded selection,
|
|
483
|
-
* truthful truncation facts, explicit includes, and follow-up commands.
|
|
484
|
-
*
|
|
485
|
-
* @param {Object} context - Raw build context returned by the provider.
|
|
486
|
-
* @param {Object} options - Compact presentation options.
|
|
487
|
-
* @param {string|null} [options.source] - Resolved source fallback.
|
|
488
|
-
* @param {string[]} [options.include] - Explicit detail collections.
|
|
489
|
-
* @param {number} [options.evidenceLimit] - Maximum evidence record count.
|
|
490
|
-
* @returns {Object} Bounded agent build context.
|
|
491
|
-
*/
|
|
492
|
-
function buildAgentBuildPayload(context, {
|
|
493
|
-
source = null,
|
|
494
|
-
include = [],
|
|
495
|
-
evidenceLimit = 10
|
|
496
|
-
} = {}) {
|
|
497
|
-
let includeSet = new Set(include);
|
|
498
|
-
let includeDiffs = includeSet.has('diffs');
|
|
499
|
-
let normalized = normalizeBuildContext(context, {
|
|
500
|
-
includeDiffs
|
|
501
|
-
});
|
|
502
|
-
let candidates = [...normalized.failed_captures.map(buildFailedCaptureEvidence), ...selectBreadthFirstEvidence(normalized.groups)];
|
|
503
|
-
let evidence = candidates.slice(0, evidenceLimit);
|
|
504
|
-
let evidenceTruncated = candidates.length > evidence.length;
|
|
505
|
-
let payload = {
|
|
418
|
+
function buildCompactBuildPayload(context, include = [], cursor = null) {
|
|
419
|
+
return {
|
|
420
|
+
...context,
|
|
506
421
|
resource: 'build_agent_context',
|
|
507
|
-
|
|
508
|
-
review_flow: normalized.review_flow,
|
|
509
|
-
scope: normalized.scope || null,
|
|
510
|
-
project: {
|
|
511
|
-
organization: normalized.scope?.organization?.slug || null,
|
|
512
|
-
slug: normalized.scope?.project?.slug || null,
|
|
513
|
-
name: normalized.scope?.project?.name || null,
|
|
514
|
-
visibility: normalized.scope?.project?.visibility || null
|
|
515
|
-
},
|
|
516
|
-
build: normalized.build || null,
|
|
517
|
-
baseline: {
|
|
518
|
-
selected: normalized.baseline?.selected || null,
|
|
519
|
-
selection_reason: normalized.baseline?.selection_reason || null
|
|
520
|
-
},
|
|
521
|
-
status: normalized.status || null,
|
|
522
|
-
summary: normalized.summary || null,
|
|
523
|
-
signature_properties: normalized.signature_properties ?? null,
|
|
524
|
-
evidence_limit: evidenceLimit,
|
|
525
|
-
evidence_returned: evidence.length,
|
|
526
|
-
evidence_truncated: evidenceTruncated,
|
|
527
|
-
evidence,
|
|
528
|
-
links: normalized.links || {},
|
|
529
|
-
preview: normalized.preview || null,
|
|
530
|
-
suggested_commands: buildSuggestedCommands(normalized, evidence, evidenceTruncated)
|
|
422
|
+
suggested_commands: buildCompactBuildCommands(context, include, cursor)
|
|
531
423
|
};
|
|
532
|
-
|
|
533
|
-
|
|
424
|
+
}
|
|
425
|
+
function buildCompactComparisonCommands(context = {}, include = []) {
|
|
426
|
+
let commands = [];
|
|
427
|
+
let comparisonId = context.comparison?.id;
|
|
428
|
+
let streams = [['similar_by_fingerprint', 'similar history'], ['recent_by_name', 'recent history']];
|
|
429
|
+
let includeFlag = include.length > 0 ? ` --include ${include.join(',')}` : '';
|
|
430
|
+
for (let [stream, label] of streams) {
|
|
431
|
+
let page = context.history?.[stream]?.page;
|
|
432
|
+
if (!comparisonId || !page?.has_more || !page.next_cursor) {
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
commands.push({
|
|
436
|
+
label: `Load next ${label} page`,
|
|
437
|
+
command: appendContextSource(`vizzly --json context comparison ${quoteCommandArgument(comparisonId)} --agent --cursor ${quoteCommandArgument(page.next_cursor)}${includeFlag}`, context)
|
|
438
|
+
});
|
|
534
439
|
}
|
|
535
|
-
if (
|
|
536
|
-
|
|
440
|
+
if (comparisonId) {
|
|
441
|
+
commands.push({
|
|
442
|
+
label: 'Load full comparison context',
|
|
443
|
+
command: appendContextSource(`vizzly --json context comparison ${quoteCommandArgument(comparisonId)} --agent --full`, context)
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
return commands;
|
|
447
|
+
}
|
|
448
|
+
function buildCompactComparisonPayload(context, include = []) {
|
|
449
|
+
return {
|
|
450
|
+
...context,
|
|
451
|
+
resource: 'comparison_agent_context',
|
|
452
|
+
suggested_commands: buildCompactComparisonCommands(context, include)
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
function formatKnownBoolean(value) {
|
|
456
|
+
if (value === true) return 'yes';
|
|
457
|
+
if (value === false) return 'no';
|
|
458
|
+
return 'unknown';
|
|
459
|
+
}
|
|
460
|
+
function formatCompactPage(collection) {
|
|
461
|
+
let page = collection?.page || {};
|
|
462
|
+
let returned = page.returned ?? collection?.items?.length;
|
|
463
|
+
let total = page.total;
|
|
464
|
+
if (returned == null) return 'unknown';
|
|
465
|
+
if (total == null) return String(returned);
|
|
466
|
+
return `${returned} of ${total}${page.has_more ? ' · more available' : ''}`;
|
|
467
|
+
}
|
|
468
|
+
function displayCompactHeading(output, context, title, status) {
|
|
469
|
+
let colors = output.getColors();
|
|
470
|
+
let tone = getStatusTone(colors, status);
|
|
471
|
+
let organization = context.scope?.organization?.slug || 'unknown';
|
|
472
|
+
let project = context.scope?.project?.slug || 'unknown';
|
|
473
|
+
output.print(` ${colors.bold(title)} ${tone(status.toUpperCase())}`);
|
|
474
|
+
output.print(` ${colors.dim(`@${organization}/${project}`)}`);
|
|
475
|
+
output.blank();
|
|
476
|
+
}
|
|
477
|
+
function displayCompactBuildContext(output, context) {
|
|
478
|
+
output.header('context', 'build');
|
|
479
|
+
let colors = output.getColors();
|
|
480
|
+
let build = context.build || {};
|
|
481
|
+
let displayStatus = build.status || context.status?.state || 'unknown';
|
|
482
|
+
displayCompactHeading(output, context, build.name || build.id || 'unknown build', displayStatus);
|
|
483
|
+
output.labelValue('Attention', formatKnownBoolean(context.status?.needs_review));
|
|
484
|
+
output.labelValue('Evidence', formatCompactPage(context.evidence));
|
|
485
|
+
let items = context.evidence?.items || [];
|
|
486
|
+
if (items.length > 0) {
|
|
487
|
+
output.blank();
|
|
488
|
+
output.print(' Evidence');
|
|
489
|
+
for (let item of items) {
|
|
490
|
+
let result = item.result || item.status || 'unknown';
|
|
491
|
+
let percentage = getComparisonDiffPercentage(item);
|
|
492
|
+
let detail = percentage == null ? '' : ` · ${percentage}% diff`;
|
|
493
|
+
output.print(` ${colors.dim('•')} ${getComparisonName(item)}: ${result}${detail}`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (context.evidence?.page?.has_more) {
|
|
497
|
+
let next = context.suggested_commands?.find(command => command.label === 'Load next evidence page');
|
|
498
|
+
if (next) {
|
|
499
|
+
output.blank();
|
|
500
|
+
output.labelValue('Next', next.command);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (context.links?.build_url) {
|
|
504
|
+
output.labelValue('Build URL', context.links.build_url);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
function displayCompactComparisonContext(output, context) {
|
|
508
|
+
output.header('context', 'comparison');
|
|
509
|
+
let comparison = context.comparison || {};
|
|
510
|
+
let displayState = getComparisonDisplayState(comparison);
|
|
511
|
+
displayCompactHeading(output, context, getComparisonName(comparison), displayState);
|
|
512
|
+
output.labelValue('Images', comparison.diff?.image_url || comparison.analysis?.diff_image_url ? 'baseline/current/diff available' : 'unavailable');
|
|
513
|
+
output.labelValue('Similar history', formatCompactPage(context.history?.similar_by_fingerprint));
|
|
514
|
+
output.labelValue('Recent history', formatCompactPage(context.history?.recent_by_name));
|
|
515
|
+
let similar = context.history?.similar_by_fingerprint?.items || [];
|
|
516
|
+
if (similar.length > 0) {
|
|
517
|
+
output.blank();
|
|
518
|
+
output.print(' Similar Diffs');
|
|
519
|
+
printComparisonList(output, similar);
|
|
520
|
+
}
|
|
521
|
+
let recent = context.history?.recent_by_name?.items || [];
|
|
522
|
+
if (recent.length > 0) {
|
|
523
|
+
output.blank();
|
|
524
|
+
output.print(' Recent Diffs');
|
|
525
|
+
printComparisonList(output, recent);
|
|
526
|
+
}
|
|
527
|
+
if (context.links?.comparison_url) {
|
|
528
|
+
output.labelValue('Comparison URL', context.links.comparison_url);
|
|
537
529
|
}
|
|
538
|
-
return payload;
|
|
539
530
|
}
|
|
540
531
|
function getBuildCommentsCount(context = {}) {
|
|
541
532
|
if (Array.isArray(context.comments?.build)) {
|
|
@@ -734,61 +725,6 @@ function displayBuildContext(output, context) {
|
|
|
734
725
|
printFailedCaptures(output, failedCaptures);
|
|
735
726
|
}
|
|
736
727
|
}
|
|
737
|
-
function formatAgentBuildContext(context) {
|
|
738
|
-
let comparisons = context.comparisons || [];
|
|
739
|
-
let changed = comparisons.filter(isChangedComparison);
|
|
740
|
-
let fresh = comparisons.filter(isNewComparison);
|
|
741
|
-
let needsReview = comparisons.filter(comparison => comparison.needs_review);
|
|
742
|
-
let baseline = context.baseline?.selected;
|
|
743
|
-
let lines = [`# Vizzly Visual Context: ${context.build?.name || context.build?.id || 'Build'}`, '', `Project: ${context.scope?.organization?.slug || 'unknown'}/${context.scope?.project?.slug || 'unknown'}`, `Build: ${context.build?.id || 'unknown'} (${context.build?.status || 'unknown'})`];
|
|
744
|
-
if (baseline) {
|
|
745
|
-
lines.push(`Approved baseline: ${baseline.name || baseline.id || 'selected'} (${baseline.approval_status || 'unknown'})`);
|
|
746
|
-
}
|
|
747
|
-
if (context.status) {
|
|
748
|
-
lines.push(`Needs review: ${context.status.needs_review ? 'yes' : 'no'} (${context.status.pending_comparisons || 0} pending comparisons)`);
|
|
749
|
-
}
|
|
750
|
-
if (context.preview?.url || context.preview?.preview_url) {
|
|
751
|
-
lines.push(`Preview: ${context.preview.url || context.preview.preview_url}`);
|
|
752
|
-
}
|
|
753
|
-
if (context.links?.build_url) {
|
|
754
|
-
lines.push(`Build URL: ${context.links.build_url}`);
|
|
755
|
-
}
|
|
756
|
-
if (context.links?.report_url) {
|
|
757
|
-
lines.push(`Report: ${context.links.report_url}`);
|
|
758
|
-
}
|
|
759
|
-
lines.push('');
|
|
760
|
-
lines.push('## Diff Summary');
|
|
761
|
-
lines.push(`- Total comparisons: ${comparisons.length}`);
|
|
762
|
-
lines.push(`- Changed: ${changed.length}`);
|
|
763
|
-
lines.push(`- New: ${fresh.length}`);
|
|
764
|
-
lines.push(`- Needs review: ${needsReview.length}`);
|
|
765
|
-
if (changed.length > 0 || fresh.length > 0) {
|
|
766
|
-
lines.push('');
|
|
767
|
-
lines.push('## Evidence To Inspect');
|
|
768
|
-
for (let comparison of [...changed, ...fresh].slice(0, 10)) {
|
|
769
|
-
let diffPercentage = getComparisonDiffPercentage(comparison);
|
|
770
|
-
let detail = diffPercentage == null ? '' : ` · ${diffPercentage}% diff`;
|
|
771
|
-
let diffUrl = comparison.diff?.image_url || comparison.analysis?.diff_image_url;
|
|
772
|
-
lines.push(`- ${getComparisonName(comparison)}: ${getComparisonDisplayState(comparison)}${detail}`);
|
|
773
|
-
if (diffUrl) {
|
|
774
|
-
lines.push(` Diff: ${diffUrl}`);
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
if (comparisons.length > 0 && changed.length === 0 && fresh.length === 0) {
|
|
779
|
-
lines.push('');
|
|
780
|
-
lines.push('## Reviewed Screenshots');
|
|
781
|
-
for (let comparison of comparisons.slice(0, 10)) {
|
|
782
|
-
lines.push(`- ${getComparisonName(comparison)}: ${getComparisonDisplayState(comparison)}`);
|
|
783
|
-
}
|
|
784
|
-
if (comparisons.length > 10) {
|
|
785
|
-
lines.push(`- ...${comparisons.length - 10} more`);
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
lines.push('');
|
|
789
|
-
lines.push('Use this as reviewed UI context. Treat approved baselines as visual truth, inspect meaningful diffs, and leave approval decisions to humans.');
|
|
790
|
-
return lines.join('\n');
|
|
791
|
-
}
|
|
792
728
|
function countScreenshotCommentEntries(groups = []) {
|
|
793
729
|
return groups.reduce((total, group) => total + (group.comments?.length || 0), 0);
|
|
794
730
|
}
|
|
@@ -891,18 +827,23 @@ export async function contextBuildCommand(buildId, options = {}, globalOptions =
|
|
|
891
827
|
return;
|
|
892
828
|
}
|
|
893
829
|
let resolvedBuildId = resolveBuildContextId(buildId, runtime, deps);
|
|
894
|
-
let
|
|
895
|
-
|
|
896
|
-
details
|
|
897
|
-
|
|
830
|
+
let {
|
|
831
|
+
compact,
|
|
832
|
+
details,
|
|
833
|
+
include,
|
|
834
|
+
query
|
|
835
|
+
} = buildCompactContextRequest(options, globalOptions);
|
|
898
836
|
output.startSpinner('Fetching build context...');
|
|
899
837
|
let context = await runtime.provider.getBuildContext(resolvedBuildId, query);
|
|
900
838
|
output.stopSpinner();
|
|
901
|
-
if (
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
839
|
+
if (compact) {
|
|
840
|
+
let compactContext = validateCompactContext(context, 'build');
|
|
841
|
+
let payload = validateCompactOutputSize(buildCompactBuildPayload(compactContext, include, options.cursor), 'build', details);
|
|
842
|
+
if (globalOptions.json) {
|
|
843
|
+
output.data(payload);
|
|
844
|
+
} else {
|
|
845
|
+
displayCompactBuildContext(output, payload);
|
|
846
|
+
}
|
|
906
847
|
output.cleanup();
|
|
907
848
|
return;
|
|
908
849
|
}
|
|
@@ -911,11 +852,6 @@ export async function contextBuildCommand(buildId, options = {}, globalOptions =
|
|
|
911
852
|
output.cleanup();
|
|
912
853
|
return;
|
|
913
854
|
}
|
|
914
|
-
if (options.agent) {
|
|
915
|
-
output.print(formatAgentBuildContext(context));
|
|
916
|
-
output.cleanup();
|
|
917
|
-
return;
|
|
918
|
-
}
|
|
919
855
|
displayBuildContext(output, context);
|
|
920
856
|
output.cleanup();
|
|
921
857
|
} catch (error) {
|
|
@@ -944,14 +880,26 @@ export async function contextComparisonCommand(comparisonId, options = {}, globa
|
|
|
944
880
|
if (!runtime) {
|
|
945
881
|
return;
|
|
946
882
|
}
|
|
947
|
-
let
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
883
|
+
let {
|
|
884
|
+
compact,
|
|
885
|
+
details,
|
|
886
|
+
include,
|
|
887
|
+
query
|
|
888
|
+
} = buildCompactContextRequest(options, globalOptions);
|
|
952
889
|
output.startSpinner('Fetching comparison context...');
|
|
953
890
|
let context = await runtime.provider.getComparisonContext(comparisonId, query);
|
|
954
891
|
output.stopSpinner();
|
|
892
|
+
if (compact) {
|
|
893
|
+
let compactContext = validateCompactContext(context, 'comparison');
|
|
894
|
+
let payload = validateCompactOutputSize(buildCompactComparisonPayload(compactContext, include), 'comparison', details);
|
|
895
|
+
if (globalOptions.json) {
|
|
896
|
+
output.data(payload);
|
|
897
|
+
} else {
|
|
898
|
+
displayCompactComparisonContext(output, payload);
|
|
899
|
+
}
|
|
900
|
+
output.cleanup();
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
955
903
|
if (globalOptions.json) {
|
|
956
904
|
output.data(context);
|
|
957
905
|
output.cleanup();
|
|
@@ -1088,20 +1036,14 @@ export async function contextReviewQueueCommand(options = {}, globalOptions = {}
|
|
|
1088
1036
|
export function validateContextBuildOptions(_options = {}) {
|
|
1089
1037
|
let errors = validateSourceOption(_options.source);
|
|
1090
1038
|
errors.push(...validateIncludeOption(_options.include));
|
|
1039
|
+
errors.push(...validateCursor(_options.cursor));
|
|
1091
1040
|
return errors;
|
|
1092
1041
|
}
|
|
1093
1042
|
export function validateContextComparisonOptions(options = {}) {
|
|
1094
1043
|
let errors = [];
|
|
1095
1044
|
errors.push(...validateSourceOption(options.source));
|
|
1096
|
-
errors.push(...
|
|
1097
|
-
|
|
1098
|
-
}));
|
|
1099
|
-
errors.push(...validateLimitRange(options.recentLimit, '--recent-limit', {
|
|
1100
|
-
max: 50
|
|
1101
|
-
}));
|
|
1102
|
-
errors.push(...validateLimitRange(options.windowSize, '--window-size', {
|
|
1103
|
-
max: 50
|
|
1104
|
-
}));
|
|
1045
|
+
errors.push(...validateIncludeOption(options.include));
|
|
1046
|
+
errors.push(...validateCursor(options.cursor));
|
|
1105
1047
|
return errors;
|
|
1106
1048
|
}
|
|
1107
1049
|
export function validateContextScreenshotOptions(options = {}) {
|