browser-debugger-cli 0.13.0 → 0.14.0

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.
Files changed (112) hide show
  1. package/.claude/skills/bdg/SKILL.md +100 -186
  2. package/README.md +4 -4
  3. package/dist/commands/console.js +5 -1
  4. package/dist/commands/dom/a11y.d.ts +1 -1
  5. package/dist/commands/dom/a11y.js +20 -20
  6. package/dist/commands/dom/eval.d.ts +2 -1
  7. package/dist/commands/dom/eval.js +21 -3
  8. package/dist/commands/dom/formInteraction.js +1 -1
  9. package/dist/commands/dom/get.js +25 -7
  10. package/dist/commands/dom/index.js +7 -2
  11. package/dist/commands/dom/query.d.ts +2 -1
  12. package/dist/commands/dom/query.js +5 -3
  13. package/dist/commands/dom/screenshot.js +1 -0
  14. package/dist/commands/helpJson.js +1 -1
  15. package/dist/commands/network/list.js +46 -3
  16. package/dist/commands/optionBehaviors.d.ts +25 -2
  17. package/dist/commands/optionBehaviors.js +55 -42
  18. package/dist/commands/peek.js +3 -0
  19. package/dist/commands/shared/CommandRunner.js +13 -13
  20. package/dist/commands/shared/daemonErrorHandler.js +2 -2
  21. package/dist/commands/shared/dataFetcher.d.ts +4 -2
  22. package/dist/commands/shared/dataFetcher.js +11 -3
  23. package/dist/commands/shared/handleValidationError.js +3 -3
  24. package/dist/commands/shared/optionTypes.d.ts +14 -3
  25. package/dist/commands/shared/startHelpers.js +3 -3
  26. package/dist/connection/chromeIdentity.d.ts +8 -2
  27. package/dist/connection/chromeIdentity.js +85 -13
  28. package/dist/constants.d.ts +29 -1
  29. package/dist/constants.js +35 -1
  30. package/dist/daemon/SessionController.js +2 -0
  31. package/dist/daemon/session/Session.d.ts +2 -1
  32. package/dist/daemon/session/Session.js +10 -2
  33. package/dist/daemon/session/TelemetryStore.d.ts +7 -0
  34. package/dist/daemon/session/TelemetryStore.js +6 -0
  35. package/dist/daemon/session/commandRegistry.js +23 -5
  36. package/dist/daemon/session/matchedStylesReset.d.ts +26 -0
  37. package/dist/daemon/session/matchedStylesReset.js +46 -0
  38. package/dist/daemon/session/plugins.js +1 -0
  39. package/dist/daemon/session/triggeredRequests.d.ts +0 -5
  40. package/dist/daemon/session/triggeredRequests.js +13 -7
  41. package/dist/daemon.js +742 -460
  42. package/dist/errors/messages.d.ts +8 -0
  43. package/dist/errors/messages.js +10 -0
  44. package/dist/index.js +710 -518
  45. package/dist/ipc/protocol/commands.d.ts +4 -0
  46. package/dist/ipc/protocol/inspectTypes.d.ts +5 -2
  47. package/dist/ipc/session/types.d.ts +5 -1
  48. package/dist/program.d.ts +14 -0
  49. package/dist/program.js +53 -0
  50. package/dist/runtime/dom/elementGeometry.d.ts +23 -0
  51. package/dist/runtime/dom/elementGeometry.js +17 -15
  52. package/dist/runtime/dom/elementInfo.d.ts +6 -4
  53. package/dist/runtime/dom/elementInfo.js +7 -4
  54. package/dist/runtime/dom/frameScopedConnection.d.ts +7 -0
  55. package/dist/runtime/dom/frameScopedConnection.js +2 -2
  56. package/dist/runtime/dom/inspect.d.ts +17 -3
  57. package/dist/runtime/dom/inspect.js +40 -26
  58. package/dist/runtime/dom/inspectModel.d.ts +3 -3
  59. package/dist/runtime/dom/inspectRules.d.ts +29 -3
  60. package/dist/runtime/dom/inspectRules.js +205 -11
  61. package/dist/runtime/dom/layout.d.ts +0 -2
  62. package/dist/runtime/dom/layout.js +1 -2
  63. package/dist/runtime/dom/reactEventHelpers.d.ts +4 -1
  64. package/dist/runtime/dom/reactEventHelpers.js +9 -2
  65. package/dist/runtime/dom/targetNode.d.ts +10 -6
  66. package/dist/runtime/dom/targetNode.js +15 -8
  67. package/dist/telemetry/a11y.d.ts +15 -1
  68. package/dist/telemetry/a11y.js +83 -0
  69. package/dist/telemetry/har/builder.js +1 -1
  70. package/dist/telemetry/network.d.ts +13 -16
  71. package/dist/telemetry/network.js +30 -52
  72. package/dist/telemetry/networkRetention.d.ts +83 -0
  73. package/dist/telemetry/networkRetention.js +117 -0
  74. package/dist/types.d.ts +26 -0
  75. package/dist/ui/OutputBuilder.d.ts +10 -0
  76. package/dist/ui/OutputBuilder.js +12 -0
  77. package/dist/ui/formatters/a11y.d.ts +5 -7
  78. package/dist/ui/formatters/a11y.js +7 -61
  79. package/dist/ui/formatters/console/chronological.js +4 -4
  80. package/dist/ui/formatters/console/follow.d.ts +4 -2
  81. package/dist/ui/formatters/console/follow.js +6 -3
  82. package/dist/ui/formatters/console/json.d.ts +3 -6
  83. package/dist/ui/formatters/console/json.js +9 -13
  84. package/dist/ui/formatters/console/shared.d.ts +17 -2
  85. package/dist/ui/formatters/console/shared.js +17 -0
  86. package/dist/ui/formatters/console/summarize.d.ts +2 -2
  87. package/dist/ui/formatters/console/summarize.js +22 -7
  88. package/dist/ui/formatters/console.d.ts +1 -1
  89. package/dist/ui/formatters/console.js +1 -5
  90. package/dist/ui/formatters/details.js +1 -1
  91. package/dist/ui/formatters/dom.d.ts +13 -4
  92. package/dist/ui/formatters/dom.js +25 -7
  93. package/dist/ui/formatters/layout.js +2 -1
  94. package/dist/ui/formatters/longValues.d.ts +14 -0
  95. package/dist/ui/formatters/longValues.js +23 -0
  96. package/dist/ui/formatters/networkList.d.ts +8 -2
  97. package/dist/ui/formatters/networkList.js +11 -2
  98. package/dist/ui/formatters/preview.d.ts +4 -1
  99. package/dist/ui/formatters/preview.js +55 -13
  100. package/dist/ui/formatters/status.js +7 -0
  101. package/dist/ui/formatters/triggeredRequests.js +2 -1
  102. package/dist/ui/messages/chrome.d.ts +20 -1
  103. package/dist/ui/messages/chrome.js +29 -3
  104. package/dist/ui/messages/commands.d.ts +29 -8
  105. package/dist/ui/messages/commands.js +36 -8
  106. package/dist/ui/messages/networkMessages.d.ts +24 -0
  107. package/dist/ui/messages/networkMessages.js +45 -0
  108. package/dist/utils/http.d.ts +9 -2
  109. package/dist/utils/http.js +4 -3
  110. package/dist/utils/strings.d.ts +19 -0
  111. package/dist/utils/strings.js +16 -0
  112. package/package.json +2 -2
@@ -1,12 +1,13 @@
1
1
  import { CDPHandlerRegistry } from '../connection/handlers.js';
2
2
  import { TypedCDPConnection } from '../connection/typed-cdp.js';
3
- import { MAX_NETWORK_REQUESTS, MAX_RESPONSE_SIZE, CHROME_NETWORK_BUFFER_TOTAL, CHROME_NETWORK_BUFFER_PER_RESOURCE, CHROME_POST_DATA_LIMIT, } from '../constants.js';
3
+ import { MAX_NETWORK_REQUESTS, MAX_RESPONSE_SIZE, MAX_TOTAL_BODY_BYTES, CHROME_NETWORK_BUFFER_TOTAL, CHROME_NETWORK_BUFFER_PER_RESOURCE, CHROME_POST_DATA_LIMIT, } from '../constants.js';
4
4
  import { attachChildTargets } from './attachedTargets.js';
5
5
  import { createLogger } from '../ui/logging/index.js';
6
6
  import { getErrorMessage } from '../utils/errors.js';
7
7
  import { filterDefined } from '../utils/objects.js';
8
8
  import { shouldExcludeDomain, shouldExcludeUrl, shouldFetchBodyWithReason } from './filters.js';
9
9
  import { ExtraInfoTracker } from './networkExtraInfo.js';
10
+ import { RequestRetention, skippedBodyPlaceholder, } from './networkRetention.js';
10
11
  const log = createLogger('network');
11
12
  /**
12
13
  * Check if a request should be filtered out based on domain and URL patterns.
@@ -21,27 +22,26 @@ function shouldFilterRequest(url, includeAll, networkInclude, networkExclude) {
21
22
  return false;
22
23
  }
23
24
  /**
24
- * Fetch response body for a request with cancellation support.
25
+ * Fetch response body for a request with cancellation support: a body that
26
+ * arrives after its fetch was cancelled (removed from `pendingFetches`: the
27
+ * collector stopped, or the request was dropped) is discarded.
25
28
  *
26
29
  * @param cdp - CDP connection instance
27
30
  * @param requestId - Request ID to fetch body for
28
- * @param request - Network request object to populate with body
31
+ * @param request - Network request the body belongs to
29
32
  * @param pendingFetches - Set to track pending fetch operations for cleanup
33
+ * @param retention - Stores the body within the session's body budget
34
+ * @param sessionId - Session of the iframe or worker that made the request
30
35
  */
31
- function fetchResponseBody(cdp, requestId, request, pendingFetches, sessionId) {
36
+ function fetchResponseBody(cdp, requestId, request, pendingFetches, retention, sessionId) {
32
37
  pendingFetches.add(requestId);
33
38
  void cdp
34
39
  .send('Network.getResponseBody', { requestId }, sessionId)
35
40
  .then((response) => {
36
41
  if (!pendingFetches.has(requestId))
37
42
  return;
38
- const typedResponse = response;
39
- request.responseBody = typedResponse.body;
40
- if (typedResponse.base64Encoded)
41
- request.responseBodyBase64 = true;
42
- if (typedResponse.body) {
43
- request.decodedBodyLength = Buffer.byteLength(typedResponse.body, typedResponse.base64Encoded ? 'base64' : 'utf-8');
44
- }
43
+ const { body, base64Encoded } = response;
44
+ retention.storeBody(request, body, base64Encoded);
45
45
  })
46
46
  .catch((error) => {
47
47
  log.debug(`Failed to fetch response body for request ${requestId}: ${getErrorMessage(error)}`);
@@ -83,25 +83,6 @@ function createNetworkRequest(params, getCurrentNavigationId) {
83
83
  ...(params.type !== undefined && { resourceType: params.type }),
84
84
  };
85
85
  }
86
- const SKIPPED_BODY_PATTERN = /^\[SKIPPED: (.*)\]$/s;
87
- /**
88
- * Placeholder stored instead of a response body that was not fetched.
89
- *
90
- * @param reason - Why the body was skipped
91
- * @returns Placeholder text shown by `bdg details`
92
- */
93
- export function skippedBodyPlaceholder(reason) {
94
- return `[SKIPPED: ${reason}]`;
95
- }
96
- /**
97
- * Extract the reason from a skipped-body placeholder.
98
- *
99
- * @param body - Stored response body
100
- * @returns Reason if `body` is a placeholder, otherwise undefined
101
- */
102
- export function skippedBodyReason(body) {
103
- return body === undefined ? undefined : SKIPPED_BODY_PATTERN.exec(body)?.[1];
104
- }
105
86
  /**
106
87
  * Copy response fields (status, headers, timing, connection) onto a request.
107
88
  *
@@ -272,21 +253,31 @@ async function collectChildTargetNetwork(cdp) {
272
253
  *
273
254
  * @remarks
274
255
  * - Chrome buffer limits: 50MB total, 10MB per resource, 1MB POST data (with fallback)
275
- * - Stale requests (incomplete after 60s) are removed from tracking but NOT added to output
276
- * - Request limit of 10,000 prevents memory issues in long-running sessions
256
+ * - The newest 10,000 finished requests are kept: past that the oldest finished
257
+ * ones are dropped (counted in `evictions.requestsDropped`); requests in flight
258
+ * are tracked separately and never dropped mid-flight
259
+ * - Stored response bodies total at most 100MB: past that the oldest bodies are
260
+ * replaced by a placeholder (counted in `evictions.bodiesEvicted`), their
261
+ * request metadata stays
277
262
  * - Response bodies are automatically skipped for images, fonts, CSS, and source maps (see DEFAULT_SKIP_BODY_PATTERNS)
278
263
  * - Response bodies larger than 5MB are skipped with a placeholder message
279
264
  * - By default, common tracking/analytics domains are filtered out (use includeAll to disable)
280
265
  * - Pattern precedence: include patterns always trump exclude patterns
281
266
  */
282
267
  export async function startNetworkCollection(cdp, requests, options = {}) {
283
- const { includeAll = false, fetchAllBodies = false, fetchBodiesInclude = [], fetchBodiesExclude = [], networkInclude = [], networkExclude = [], maxBodySize = MAX_RESPONSE_SIZE, getCurrentNavigationId, } = options;
268
+ const { includeAll = false, fetchAllBodies = false, fetchBodiesInclude = [], fetchBodiesExclude = [], networkInclude = [], networkExclude = [], maxBodySize = MAX_RESPONSE_SIZE, maxRequests = MAX_NETWORK_REQUESTS, maxTotalBodyBytes = MAX_TOTAL_BODY_BYTES, evictions = { requestsDropped: 0, bodiesEvicted: 0 }, getCurrentNavigationId, } = options;
284
269
  const requestMap = options.pendingRequests ?? new Map();
285
270
  const pendingFetches = new Set();
286
271
  const redirectHops = new Map();
287
272
  const extraInfo = new ExtraInfoTracker((requestId) => requestMap.get(requestId)?.request);
288
273
  const registry = new CDPHandlerRegistry();
289
274
  const typed = new TypedCDPConnection(cdp);
275
+ const retention = new RequestRetention(requests, { maxRequests, maxTotalBodyBytes }, evictions);
276
+ const record = (request) => {
277
+ const dropped = retention.add(request);
278
+ if (dropped)
279
+ pendingFetches.delete(dropped.requestId);
280
+ };
290
281
  let bodiesFetched = 0;
291
282
  let bodiesSkipped = 0;
292
283
  try {
@@ -305,11 +296,9 @@ export async function startNetworkCollection(cdp, requests, options = {}) {
305
296
  const entry = requestMap.get(requestId);
306
297
  if (!entry)
307
298
  return;
308
- if (requests.length < MAX_NETWORK_REQUESTS) {
309
- applyFailure(entry.request, failure);
310
- requests.push(entry.request);
311
- extraInfo.complete(requestId, entry.request);
312
- }
299
+ applyFailure(entry.request, failure);
300
+ record(entry.request);
301
+ extraInfo.complete(requestId, entry.request);
313
302
  requestMap.delete(requestId);
314
303
  redirectHops.delete(requestId);
315
304
  };
@@ -349,16 +338,11 @@ export async function startNetworkCollection(cdp, requests, options = {}) {
349
338
  const hop = completeRedirectHop(previous.request, params, redirectHops);
350
339
  extraInfo.applyResponse(params.requestId, hop);
351
340
  extraInfo.recordRedirectHop(params.requestId, hop);
352
- if (requests.length < MAX_NETWORK_REQUESTS)
353
- requests.push(hop);
341
+ record(hop);
354
342
  }
355
343
  if (shouldFilterRequest(params.request.url, includeAll, networkInclude, networkExclude)) {
356
344
  return;
357
345
  }
358
- if (requestMap.size >= MAX_NETWORK_REQUESTS) {
359
- log.debug(`Warning: Network request limit reached (${MAX_NETWORK_REQUESTS}), dropping new requests`);
360
- return;
361
- }
362
346
  const request = createNetworkRequest(params, getCurrentNavigationId);
363
347
  extraInfo.applyRequest(params.requestId, request);
364
348
  if (params.type === 'Document' && params.loaderId) {
@@ -399,12 +383,6 @@ export async function startNetworkCollection(cdp, requests, options = {}) {
399
383
  const entry = requestMap.get(params.requestId);
400
384
  if (!entry)
401
385
  return;
402
- if (requests.length >= MAX_NETWORK_REQUESTS) {
403
- log.debug(`Warning: Network request limit reached (${MAX_NETWORK_REQUESTS})`);
404
- requestMap.delete(params.requestId);
405
- redirectHops.delete(params.requestId);
406
- return;
407
- }
408
386
  const request = entry.request;
409
387
  if (params.encodedDataLength !== undefined) {
410
388
  request.encodedDataLength = params.encodedDataLength;
@@ -419,13 +397,13 @@ export async function startNetworkCollection(cdp, requests, options = {}) {
419
397
  });
420
398
  if (decision.should) {
421
399
  bodiesFetched++;
422
- fetchResponseBody(cdp, params.requestId, request, pendingFetches, entry.sessionId);
400
+ fetchResponseBody(cdp, params.requestId, request, pendingFetches, retention, entry.sessionId);
423
401
  }
424
402
  else {
425
403
  bodiesSkipped++;
426
404
  request.responseBody = skippedBodyPlaceholder(decision.reason ?? 'not captured');
427
405
  }
428
- requests.push(request);
406
+ record(request);
429
407
  extraInfo.complete(params.requestId, request);
430
408
  requestMap.delete(params.requestId);
431
409
  redirectHops.delete(params.requestId);
@@ -0,0 +1,83 @@
1
+ /**
2
+ * What the network collector keeps of a long session: the newest finished
3
+ * requests up to a cap, and the newest response bodies up to a total size.
4
+ */
5
+ import type { NetworkRequest } from '../types.js';
6
+ /** Counts of what the session let go at its limits */
7
+ export interface NetworkEvictions {
8
+ /** Finished requests dropped at the request cap, oldest first */
9
+ requestsDropped: number;
10
+ /** Response bodies replaced by a placeholder at the total body budget, oldest first */
11
+ bodiesEvicted: number;
12
+ }
13
+ /** Limits of {@link RequestRetention} */
14
+ export interface RetentionLimits {
15
+ /** Finished requests kept at most */
16
+ maxRequests: number;
17
+ /** Total size of the stored response bodies (bytes) */
18
+ maxTotalBodyBytes: number;
19
+ }
20
+ /**
21
+ * Keeps finished requests, oldest first, dropping the oldest past the cap
22
+ * (requests in flight are never in the list, so never dropped), and tracks
23
+ * the size of their stored bodies, replacing the oldest bodies past the
24
+ * budget with a placeholder that says why.
25
+ *
26
+ * Stored bodies are kept in a Map, which iterates in insertion order: its
27
+ * first entry is the oldest body, and a dropped request's body is removed in
28
+ * O(1). Dropping from the front of the list is `Array.shift`, which V8 does
29
+ * without copying for arrays of this size.
30
+ */
31
+ export declare class RequestRetention {
32
+ private readonly requests;
33
+ private readonly limits;
34
+ private readonly evictions;
35
+ private readonly bodySizes;
36
+ private storedBodyBytes;
37
+ /**
38
+ * @param requests - Finished requests, oldest first (updated in place)
39
+ * @param limits - Request cap and body budget
40
+ * @param evictions - Counters updated on each drop and eviction
41
+ */
42
+ constructor(requests: NetworkRequest[], limits: RetentionLimits, evictions: NetworkEvictions);
43
+ /**
44
+ * Add a finished request, dropping the oldest one past the cap.
45
+ *
46
+ * @param request - Finished request
47
+ * @returns The dropped request, if one was
48
+ */
49
+ add(request: NetworkRequest): NetworkRequest | undefined;
50
+ /**
51
+ * Store a fetched response body on a kept request, then evict the oldest
52
+ * bodies until the total fits the budget (a body larger than the whole
53
+ * budget is evicted itself). Storing a body again replaces the earlier one.
54
+ *
55
+ * @param request - Request the body belongs to
56
+ * @param body - Body as Chrome returned it
57
+ * @param base64Encoded - Whether `body` is base64
58
+ */
59
+ storeBody(request: NetworkRequest, body: string, base64Encoded: boolean): void;
60
+ /** Evict the oldest stored bodies while their total is over the budget. */
61
+ private enforceBodyBudget;
62
+ /**
63
+ * Stop counting a request's stored body.
64
+ *
65
+ * @param request - Request whose body is let go
66
+ */
67
+ private forgetBody;
68
+ }
69
+ /**
70
+ * Placeholder stored instead of a response body that was not fetched.
71
+ *
72
+ * @param reason - Why the body was skipped
73
+ * @returns Placeholder text shown by `bdg details`
74
+ */
75
+ export declare function skippedBodyPlaceholder(reason: string): string;
76
+ /**
77
+ * Extract the reason from a skipped-body placeholder.
78
+ *
79
+ * @param body - Stored response body
80
+ * @returns Reason if `body` is a placeholder, otherwise undefined
81
+ */
82
+ export declare function skippedBodyReason(body: string | undefined): string | undefined;
83
+ //# sourceMappingURL=networkRetention.d.ts.map
@@ -0,0 +1,117 @@
1
+ /**
2
+ * What the network collector keeps of a long session: the newest finished
3
+ * requests up to a cap, and the newest response bodies up to a total size.
4
+ */
5
+ import { bodyEvictedReason } from '../ui/messages/networkMessages.js';
6
+ /**
7
+ * Keeps finished requests, oldest first, dropping the oldest past the cap
8
+ * (requests in flight are never in the list, so never dropped), and tracks
9
+ * the size of their stored bodies, replacing the oldest bodies past the
10
+ * budget with a placeholder that says why.
11
+ *
12
+ * Stored bodies are kept in a Map, which iterates in insertion order: its
13
+ * first entry is the oldest body, and a dropped request's body is removed in
14
+ * O(1). Dropping from the front of the list is `Array.shift`, which V8 does
15
+ * without copying for arrays of this size.
16
+ */
17
+ export class RequestRetention {
18
+ requests;
19
+ limits;
20
+ evictions;
21
+ bodySizes = new Map();
22
+ storedBodyBytes = 0;
23
+ /**
24
+ * @param requests - Finished requests, oldest first (updated in place)
25
+ * @param limits - Request cap and body budget
26
+ * @param evictions - Counters updated on each drop and eviction
27
+ */
28
+ constructor(requests, limits, evictions) {
29
+ this.requests = requests;
30
+ this.limits = limits;
31
+ this.evictions = evictions;
32
+ }
33
+ /**
34
+ * Add a finished request, dropping the oldest one past the cap.
35
+ *
36
+ * @param request - Finished request
37
+ * @returns The dropped request, if one was
38
+ */
39
+ add(request) {
40
+ this.requests.push(request);
41
+ if (this.requests.length <= this.limits.maxRequests)
42
+ return undefined;
43
+ const dropped = this.requests.shift();
44
+ if (!dropped)
45
+ return undefined;
46
+ this.evictions.requestsDropped++;
47
+ this.forgetBody(dropped);
48
+ return dropped;
49
+ }
50
+ /**
51
+ * Store a fetched response body on a kept request, then evict the oldest
52
+ * bodies until the total fits the budget (a body larger than the whole
53
+ * budget is evicted itself). Storing a body again replaces the earlier one.
54
+ *
55
+ * @param request - Request the body belongs to
56
+ * @param body - Body as Chrome returned it
57
+ * @param base64Encoded - Whether `body` is base64
58
+ */
59
+ storeBody(request, body, base64Encoded) {
60
+ this.forgetBody(request);
61
+ request.responseBody = body;
62
+ if (base64Encoded)
63
+ request.responseBodyBase64 = true;
64
+ if (body) {
65
+ request.decodedBodyLength = Buffer.byteLength(body, base64Encoded ? 'base64' : 'utf-8');
66
+ }
67
+ const size = Buffer.byteLength(body);
68
+ this.bodySizes.set(request, size);
69
+ this.storedBodyBytes += size;
70
+ this.enforceBodyBudget();
71
+ }
72
+ /** Evict the oldest stored bodies while their total is over the budget. */
73
+ enforceBodyBudget() {
74
+ while (this.storedBodyBytes > this.limits.maxTotalBodyBytes) {
75
+ const oldest = this.bodySizes.keys().next();
76
+ if (oldest.done)
77
+ return;
78
+ const request = oldest.value;
79
+ this.forgetBody(request);
80
+ request.responseBody = skippedBodyPlaceholder(bodyEvictedReason(this.limits.maxTotalBodyBytes));
81
+ delete request.responseBodyBase64;
82
+ this.evictions.bodiesEvicted++;
83
+ }
84
+ }
85
+ /**
86
+ * Stop counting a request's stored body.
87
+ *
88
+ * @param request - Request whose body is let go
89
+ */
90
+ forgetBody(request) {
91
+ const size = this.bodySizes.get(request);
92
+ if (size === undefined)
93
+ return;
94
+ this.bodySizes.delete(request);
95
+ this.storedBodyBytes -= size;
96
+ }
97
+ }
98
+ const SKIPPED_BODY_PATTERN = /^\[SKIPPED: (.*)\]$/s;
99
+ /**
100
+ * Placeholder stored instead of a response body that was not fetched.
101
+ *
102
+ * @param reason - Why the body was skipped
103
+ * @returns Placeholder text shown by `bdg details`
104
+ */
105
+ export function skippedBodyPlaceholder(reason) {
106
+ return `[SKIPPED: ${reason}]`;
107
+ }
108
+ /**
109
+ * Extract the reason from a skipped-body placeholder.
110
+ *
111
+ * @param body - Stored response body
112
+ * @returns Reason if `body` is a placeholder, otherwise undefined
113
+ */
114
+ export function skippedBodyReason(body) {
115
+ return body === undefined ? undefined : SKIPPED_BODY_PATTERN.exec(body)?.[1];
116
+ }
117
+ //# sourceMappingURL=networkRetention.js.map
package/dist/types.d.ts CHANGED
@@ -204,6 +204,8 @@ export interface ConsoleMessage {
204
204
  stackTrace?: StackFrame[];
205
205
  /** Browser subsystem of a browser message (`network`, `security`, ...); absent for page console calls */
206
206
  source?: Protocol.Log.LogEntry['source'];
207
+ /** Original length of `text` when JSON output cut it (`--full` keeps it whole) */
208
+ truncatedFrom?: number;
207
209
  }
208
210
  /**
209
211
  * Console message level categories for user-facing filtering.
@@ -234,6 +236,10 @@ export interface BdgOutput {
234
236
  console: number;
235
237
  /** Console messages dropped at the limit, oldest first (indices start after them) */
236
238
  consoleDropped?: number;
239
+ /** Finished network requests dropped at the request cap, oldest first */
240
+ networkDropped?: number;
241
+ /** Response bodies evicted at the total body budget, oldest first */
242
+ networkBodiesEvicted?: number;
237
243
  };
238
244
  error?: string;
239
245
  partial?: boolean;
@@ -286,6 +292,24 @@ export interface A11yTree {
286
292
  /** Total node count */
287
293
  count: number;
288
294
  }
295
+ /** An accessibility node as `dom a11y tree` lists it */
296
+ export type ListedA11yNode = Omit<A11yNode, 'childIds'> & {
297
+ /** Indentation level (0 = root; left-out wrappers add none) */
298
+ depth: number;
299
+ };
300
+ /**
301
+ * The part of an accessibility tree `dom a11y tree` lists (`--limit`, `--depth`).
302
+ */
303
+ export interface ListedA11yTree {
304
+ /** Listed nodes, depth-first from the root */
305
+ nodes: ListedA11yNode[];
306
+ /** Nodes in the whole tree (listed + omitted + skipped) */
307
+ count: number;
308
+ /** Nodes left out by `--limit` or `--depth` */
309
+ omitted?: number;
310
+ /** Nodes never listed: text boxes, blank or repeated text, nameless layout wrappers */
311
+ skipped?: number;
312
+ }
289
313
  /**
290
314
  * Query pattern for searching accessibility tree.
291
315
  *
@@ -463,6 +487,8 @@ export interface DomGetResult {
463
487
  attributes?: Record<string, unknown>;
464
488
  classes?: string[];
465
489
  outerHTML?: string;
490
+ /** Original length of `outerHTML` when JSON output cut it (`--full` keeps it whole) */
491
+ truncatedFrom?: number;
466
492
  }>;
467
493
  }
468
494
  /**
@@ -9,6 +9,16 @@ import type { BdgResponse } from '../types.js';
9
9
  * @returns `{ version, success: true, data }`
10
10
  */
11
11
  export declare function buildSuccessResponse<T>(data: T): BdgResponse<T>;
12
+ /**
13
+ * Serialize a `--json` response envelope for stdout.
14
+ *
15
+ * Indented by two spaces when stdout is a terminal, so a person can read it;
16
+ * on one line otherwise, since agents and pipes only pay for the whitespace.
17
+ *
18
+ * @param envelope - Response envelope (or other `--json` payload)
19
+ * @returns JSON text
20
+ */
21
+ export declare function stringifyEnvelope(envelope: unknown): string;
12
22
  /** Builders for JSON error envelopes. */
13
23
  export declare class OutputBuilder {
14
24
  /**
@@ -11,6 +11,18 @@ import { VERSION } from '../utils/version.js';
11
11
  export function buildSuccessResponse(data) {
12
12
  return { version: VERSION, success: true, data };
13
13
  }
14
+ /**
15
+ * Serialize a `--json` response envelope for stdout.
16
+ *
17
+ * Indented by two spaces when stdout is a terminal, so a person can read it;
18
+ * on one line otherwise, since agents and pipes only pay for the whitespace.
19
+ *
20
+ * @param envelope - Response envelope (or other `--json` payload)
21
+ * @returns JSON text
22
+ */
23
+ export function stringifyEnvelope(envelope) {
24
+ return process.stdout.isTTY ? JSON.stringify(envelope, null, 2) : JSON.stringify(envelope);
25
+ }
14
26
  /** Builders for JSON error envelopes. */
15
27
  export class OutputBuilder {
16
28
  /**
@@ -1,5 +1,5 @@
1
1
  import type { DomContext } from '../../types.js';
2
- import type { A11yTree, A11yQueryResult, A11yNode } from '../../types.js';
2
+ import type { A11yQueryResult, A11yNode, ListedA11yTree } from '../../types.js';
3
3
  /**
4
4
  * Data structure for a11y node with DOM context.
5
5
  */
@@ -8,15 +8,13 @@ interface A11yNodeWithContext {
8
8
  domContext: DomContext | null;
9
9
  }
10
10
  /**
11
- * Format accessibility tree for human-readable output.
11
+ * Format the listed part of an accessibility tree for human-readable output:
12
+ * one indented line per node, and when nodes were cut, how to see more.
12
13
  *
13
- * Displays the tree structure with role, name, and key properties.
14
- * Shows up to 50 nodes by default for manageable output.
15
- *
16
- * @param tree - Accessibility tree data
14
+ * @param tree - Listed accessibility tree
17
15
  * @returns Formatted output string
18
16
  */
19
- export declare function formatA11yTree(tree: A11yTree): string;
17
+ export declare function formatA11yTree(tree: ListedA11yTree): string;
20
18
  /**
21
19
  * Format query result for human-readable output.
22
20
  *
@@ -1,79 +1,25 @@
1
1
  import { OutputFormatter, areHintsHidden } from '../formatting.js';
2
- import { a11yMoreMatchesNote } from '../messages/commands.js';
3
- /**
4
- * Maximum number of nodes to display in tree output before truncating.
5
- * Prevents overwhelming terminal output for large accessibility trees.
6
- */
7
- const MAX_TREE_NODES_DISPLAY = 50;
2
+ import { a11yMoreMatchesNote, a11yTreeMoreNote, a11yTreeShownNote, } from '../messages/commands.js';
8
3
  /**
9
4
  * Separator width for section dividers in formatted output.
10
5
  */
11
6
  const SEPARATOR_WIDTH = 50;
12
7
  /**
13
- * Format accessibility tree for human-readable output.
14
- *
15
- * Displays the tree structure with role, name, and key properties.
16
- * Shows up to 50 nodes by default for manageable output.
8
+ * Format the listed part of an accessibility tree for human-readable output:
9
+ * one indented line per node, and when nodes were cut, how to see more.
17
10
  *
18
- * @param tree - Accessibility tree data
11
+ * @param tree - Listed accessibility tree
19
12
  * @returns Formatted output string
20
13
  */
21
14
  export function formatA11yTree(tree) {
22
15
  const fmt = new OutputFormatter();
23
16
  fmt.text(`Accessibility Tree (${tree.count} nodes)`).separator('─', SEPARATOR_WIDTH).blank();
24
- const { lines, truncated } = treeLines(tree);
25
- lines.forEach((line) => fmt.text(line));
26
- if (truncated) {
27
- fmt
28
- .blank()
29
- .text(`Showing the first ${MAX_TREE_NODES_DISPLAY} nodes (text boxes and repeated text left out)`)
30
- .text('Use --json flag for complete output, or bdg dom a11y query "role:<role>" to search');
17
+ tree.nodes.forEach((node) => fmt.text(' '.repeat(node.depth) + formatA11yNodeOneLine(node)));
18
+ if (tree.omitted) {
19
+ fmt.blank().text(a11yTreeShownNote(tree.nodes.length)).text(a11yTreeMoreNote(tree.omitted));
31
20
  }
32
21
  return fmt.build();
33
22
  }
34
- /** Roles that only lay out their children and say nothing themselves */
35
- const LAYOUT_ROLES = new Set([
36
- 'generic',
37
- 'none',
38
- 'presentation',
39
- 'LayoutTable',
40
- 'LayoutTableRow',
41
- 'LayoutTableCell',
42
- ]);
43
- /**
44
- * The tree as indented lines, depth-first from the root. Text boxes, blank
45
- * text, text that repeats its parent's name, and nameless layout wrappers are left out
46
- * (their children move up a level), so the budget goes to meaningful nodes.
47
- *
48
- * @param tree - Accessibility tree
49
- * @returns Up to {@link MAX_TREE_NODES_DISPLAY} lines, and whether nodes were left
50
- */
51
- function treeLines(tree) {
52
- const lines = [];
53
- const visited = new Set();
54
- let truncated = false;
55
- const visit = (node, depth, parentName) => {
56
- if (visited.has(node.nodeId))
57
- return;
58
- visited.add(node.nodeId);
59
- if (lines.length >= MAX_TREE_NODES_DISPLAY) {
60
- truncated = true;
61
- return;
62
- }
63
- const skip = node.role === 'InlineTextBox' ||
64
- (node.role === 'StaticText' && (node.name === parentName || !node.name?.trim())) ||
65
- (LAYOUT_ROLES.has(node.role) && !node.name);
66
- if (!skip)
67
- lines.push(' '.repeat(depth) + formatA11yNodeOneLine(node));
68
- for (const childId of node.childIds ?? []) {
69
- const child = tree.nodes.get(childId);
70
- if (child)
71
- visit(child, skip ? depth : depth + 1, node.name ?? parentName);
72
- }
73
- };
74
- visit(tree.root, 0, undefined);
75
- return { lines, truncated };
76
- }
77
23
  /** Roles whose elements take a value (the next step is fill, not click) */
78
24
  const FILLABLE_ROLES = new Set([
79
25
  'textbox',
@@ -2,11 +2,11 @@
2
2
  * Chronological list view (--list mode): all messages with timestamps,
3
3
  * level prefixes, and navigation reload markers.
4
4
  */
5
+ import { MAX_CONSOLE_TEXT_LENGTH } from '../../../constants.js';
6
+ import { capForDisplay } from '../longValues.js';
5
7
  import { OutputFormatter } from '../../formatting.js';
6
8
  import { consoleDroppedNote, consoleIndexGapNote } from '../../messages/consoleMessages.js';
7
- import { truncateByLength } from '../../../utils/strings.js';
8
9
  import { formatSourceLocation, formatTimestamp } from './shared.js';
9
- const MAX_LIST_TEXT_LENGTH = 200;
10
10
  /**
11
11
  * Format console output as chronological list (--list mode).
12
12
  *
@@ -46,8 +46,8 @@ export function formatConsoleChronological(messages, options) {
46
46
  }
47
47
  lastNavigationId = msg.navigationId;
48
48
  }
49
- const truncatedText = truncateByLength(msg.text, MAX_LIST_TEXT_LENGTH);
50
- fmt.text(`${index} ${level} ${time} ${truncatedText}`);
49
+ const text = capForDisplay(msg.text, MAX_CONSOLE_TEXT_LENGTH, options.full);
50
+ fmt.text(`${index} ${level} ${time} ${text}`);
51
51
  const source = formatSourceLocation(msg.stackTrace);
52
52
  if (source) {
53
53
  fmt.text(`${sourceIndent}→ ${source}`);
@@ -6,14 +6,16 @@ import type { ConsoleMessage } from '../../../types.js';
6
6
  /**
7
7
  * Lines of the console stream: the new messages since the last poll, with
8
8
  * a rule the first time (the stream banner is on stderr) and a separator
9
- * after a navigation.
9
+ * after a navigation. Texts are cut like `console --list` cuts them.
10
10
  *
11
11
  * @param messages - New messages
12
- * @param options - `header` the first time; `navigationId` when the page changed
12
+ * @param options - `header` the first time; `navigationId` when the page
13
+ * changed; `full` to print the texts whole
13
14
  * @returns Text to print (empty when there is nothing new)
14
15
  */
15
16
  export declare function formatConsoleFollowLines(messages: ConsoleMessage[], options?: {
16
17
  header?: boolean;
17
18
  navigationId?: number;
19
+ full?: boolean | undefined;
18
20
  }): string;
19
21
  //# sourceMappingURL=follow.d.ts.map
@@ -2,15 +2,18 @@
2
2
  * Follow-mode (live streaming) view. Compact format optimised for
3
3
  * repeated polling output.
4
4
  */
5
+ import { MAX_CONSOLE_TEXT_LENGTH } from '../../../constants.js';
6
+ import { capForDisplay } from '../longValues.js';
5
7
  import { OutputFormatter } from '../../formatting.js';
6
8
  import { formatSourceLocation, formatTimestamp } from './shared.js';
7
9
  /**
8
10
  * Lines of the console stream: the new messages since the last poll, with
9
11
  * a rule the first time (the stream banner is on stderr) and a separator
10
- * after a navigation.
12
+ * after a navigation. Texts are cut like `console --list` cuts them.
11
13
  *
12
14
  * @param messages - New messages
13
- * @param options - `header` the first time; `navigationId` when the page changed
15
+ * @param options - `header` the first time; `navigationId` when the page
16
+ * changed; `full` to print the texts whole
14
17
  * @returns Text to print (empty when there is nothing new)
15
18
  */
16
19
  export function formatConsoleFollowLines(messages, options = {}) {
@@ -26,7 +29,7 @@ export function formatConsoleFollowLines(messages, options = {}) {
26
29
  for (const msg of messages) {
27
30
  const time = formatTimestamp(msg.timestamp);
28
31
  const level = msg.type.padEnd(7);
29
- fmt.text(`${time} ${level} ${msg.text}`);
32
+ fmt.text(`${time} ${level} ${capForDisplay(msg.text, MAX_CONSOLE_TEXT_LENGTH, options.full)}`);
30
33
  const source = formatSourceLocation(msg.stackTrace);
31
34
  if (source)
32
35
  fmt.text(` → ${source}`);