@reticlehq/browser 3.6.0 → 3.8.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 (129) hide show
  1. package/dist/actions/action-report.d.ts +64 -0
  2. package/dist/actions/action-report.js +87 -0
  3. package/dist/actions/actions-dom.d.ts +5 -0
  4. package/dist/actions/actions-dom.js +32 -4
  5. package/dist/actions/actions-press.d.ts +1 -0
  6. package/dist/actions/actions-press.js +25 -1
  7. package/dist/actions/actions.d.ts +2 -37
  8. package/dist/actions/actions.js +56 -80
  9. package/dist/actions/appeared-text.js +5 -2
  10. package/dist/actions/click-target.d.ts +1 -0
  11. package/dist/actions/click-target.js +14 -0
  12. package/dist/actions/danger-context.d.ts +4 -1
  13. package/dist/actions/danger-context.js +45 -4
  14. package/dist/actions/value-input.d.ts +1 -0
  15. package/dist/actions/value-input.js +11 -0
  16. package/dist/activate.d.ts +14 -0
  17. package/dist/activate.js +43 -0
  18. package/dist/busy.d.ts +3 -0
  19. package/dist/busy.js +42 -0
  20. package/dist/commands/commands.js +49 -17
  21. package/dist/connect-options.d.ts +12 -1
  22. package/dist/connect-options.js +18 -1
  23. package/dist/dom/a11y.d.ts +2 -0
  24. package/dist/dom/a11y.js +353 -80
  25. package/dist/dom/addressing/coverage-markers.d.ts +6 -0
  26. package/dist/dom/addressing/coverage-markers.js +13 -0
  27. package/dist/dom/chart.d.ts +3 -2
  28. package/dist/dom/chart.js +3 -2
  29. package/dist/dom/dom-ignore.d.ts +8 -0
  30. package/dist/dom/dom-ignore.js +57 -2
  31. package/dist/dom/layout-audit.d.ts +15 -0
  32. package/dist/dom/layout-audit.js +208 -0
  33. package/dist/dom/paint-context.d.ts +15 -0
  34. package/dist/dom/paint-context.js +78 -0
  35. package/dist/dom/query.d.ts +2 -1
  36. package/dist/dom/query.js +163 -38
  37. package/dist/dom/render-page.d.ts +12 -0
  38. package/dist/dom/render-page.js +135 -0
  39. package/dist/dom/shadow-registry.d.ts +1 -0
  40. package/dist/dom/shadow-registry.js +27 -0
  41. package/dist/dom/snapshot.d.ts +5 -0
  42. package/dist/dom/snapshot.js +135 -36
  43. package/dist/dom/stable-selector.d.ts +1 -0
  44. package/dist/dom/stable-selector.js +19 -0
  45. package/dist/dom/text-match.d.ts +12 -0
  46. package/dist/dom/text-match.js +115 -0
  47. package/dist/dom/theme.d.ts +1 -0
  48. package/dist/dom/theme.js +21 -9
  49. package/dist/dom/virtualized.js +5 -1
  50. package/dist/index.d.ts +2 -1
  51. package/dist/index.js +5 -0
  52. package/dist/observers/blind-spots.js +3 -2
  53. package/dist/observers/canvas.d.ts +18 -0
  54. package/dist/observers/canvas.js +76 -0
  55. package/dist/observers/dom.js +96 -32
  56. package/dist/observers/field.d.ts +11 -0
  57. package/dist/observers/field.js +116 -30
  58. package/dist/observers/install-all.js +4 -0
  59. package/dist/observers/media.d.ts +2 -0
  60. package/dist/observers/media.js +32 -0
  61. package/dist/observers/net-detail/network-body.d.ts +2 -0
  62. package/dist/observers/net-detail/network-body.js +8 -1
  63. package/dist/observers/network.js +82 -13
  64. package/dist/observers/perf.js +53 -1
  65. package/dist/observers/route.d.ts +2 -0
  66. package/dist/observers/route.js +39 -9
  67. package/dist/presenter/carousel/carousel.js +2 -7
  68. package/dist/presenter/carousel/panel-slides.d.ts +2 -4
  69. package/dist/presenter/carousel/panel-slides.js +17 -15
  70. package/dist/presenter/chrome/log-memory.d.ts +1 -0
  71. package/dist/presenter/chrome/presenter-log.d.ts +7 -0
  72. package/dist/presenter/chrome/presenter-log.js +48 -8
  73. package/dist/presenter/chrome/presenter-morph.d.ts +1 -0
  74. package/dist/presenter/chrome/presenter-morph.js +34 -0
  75. package/dist/presenter/chrome/presenter-report-copy.d.ts +12 -4
  76. package/dist/presenter/chrome/presenter-report-copy.js +12 -4
  77. package/dist/presenter/hud-telemetry.js +55 -4
  78. package/dist/presenter/presenter-account-styles.js +3 -3
  79. package/dist/presenter/presenter-account.d.ts +27 -5
  80. package/dist/presenter/presenter-account.js +83 -6
  81. package/dist/presenter/presenter-agent-link.d.ts +9 -0
  82. package/dist/presenter/presenter-agent-link.js +62 -0
  83. package/dist/presenter/presenter-chat-views.d.ts +25 -4
  84. package/dist/presenter/presenter-chat-views.js +339 -72
  85. package/dist/presenter/presenter-config.d.ts +3 -0
  86. package/dist/presenter/presenter-config.js +2 -0
  87. package/dist/presenter/presenter-controls.d.ts +0 -1
  88. package/dist/presenter/presenter-controls.js +13 -91
  89. package/dist/presenter/presenter-design-tokens.js +1 -1
  90. package/dist/presenter/presenter-frame.d.ts +3 -2
  91. package/dist/presenter/presenter-frame.js +31 -24
  92. package/dist/presenter/presenter-harness-row.d.ts +56 -0
  93. package/dist/presenter/presenter-harness-row.js +170 -0
  94. package/dist/presenter/presenter-personas.d.ts +35 -0
  95. package/dist/presenter/presenter-personas.js +95 -0
  96. package/dist/presenter/presenter-plan.js +25 -24
  97. package/dist/presenter/presenter-report-styles.js +23 -22
  98. package/dist/presenter/presenter-report.js +29 -13
  99. package/dist/presenter/presenter-run-state.js +2 -1
  100. package/dist/presenter/presenter-settings-styles.js +8 -4
  101. package/dist/presenter/presenter-settings.d.ts +4 -4
  102. package/dist/presenter/presenter-settings.js +66 -28
  103. package/dist/presenter/presenter-shell-styles.js +12 -3
  104. package/dist/presenter/presenter-shell.d.ts +11 -3
  105. package/dist/presenter/presenter-shell.js +60 -22
  106. package/dist/presenter/presenter-styles.js +0 -2
  107. package/dist/presenter/presenter-workspace.js +10 -3
  108. package/dist/presenter/presenter.d.ts +3 -2
  109. package/dist/presenter/presenter.js +107 -25
  110. package/dist/registry/declared-platform.js +2 -0
  111. package/dist/registry/store-adapters.d.ts +5 -2
  112. package/dist/registry/store-adapters.js +40 -10
  113. package/dist/reticle-inject.js +771 -703
  114. package/dist/reticle.d.ts +4 -2
  115. package/dist/reticle.js +72 -8
  116. package/dist/security/page-location.d.ts +2 -0
  117. package/dist/security/page-location.js +7 -0
  118. package/dist/security/serialization.js +1 -1
  119. package/dist/storage-keys.d.ts +4 -2
  120. package/dist/storage-keys.js +8 -3
  121. package/dist/timers/native/native-timers.d.ts +1 -0
  122. package/dist/timers/native/native-timers.js +5 -0
  123. package/dist/transport/transport.d.ts +1 -0
  124. package/dist/transport/transport.js +23 -3
  125. package/dist/transport/unreachable-message.d.ts +1 -1
  126. package/dist/transport/unreachable-message.js +3 -5
  127. package/package.json +6 -2
  128. package/dist/presenter/carousel/offer-card.d.ts +0 -21
  129. package/dist/presenter/carousel/offer-card.js +0 -56
package/dist/reticle.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { EventType, RETICLE_URL_PARAM, HudVisibility, type ReticleEvent } from '@reticlehq/core';
2
2
  import { type CapabilitiesInput } from './registry/capabilities.js';
3
- import type { ReticleConnectOptions } from './connect-options.js';
3
+ import { type ReticleConnectOptions } from './connect-options.js';
4
+ export { helloMode, shouldBlockProduction } from './connect-options.js';
4
5
  export type { ReticleConnectOptions };
5
- export declare function shouldBlockProduction(nodeEnv: string | undefined, allowInProduction: boolean): boolean;
6
6
  export declare function withBridgePath(bridgeUrl: string): {
7
7
  url: string;
8
8
  adjusted: boolean;
@@ -43,7 +43,9 @@ export declare class Reticle {
43
43
  #private;
44
44
  connect(options?: ReticleConnectOptions): void;
45
45
  get connected(): boolean;
46
+ routes(patterns: readonly string[]): void;
46
47
  signal(name: string, data?: Record<string, unknown>): void;
48
+ busy<T>(work: Promise<T>): Promise<T>;
47
49
  state(name: string, value: unknown): void;
48
50
  renderCommit(commits: number): void;
49
51
  describe(input: CapabilitiesInput): void;
package/dist/reticle.js CHANGED
@@ -1,4 +1,8 @@
1
- import { setPresenterVisible } from './dom/dom-ignore.js';
1
+ import { setPresenterViewer, setPresenterVisible } from './dom/dom-ignore.js';
2
+ import { trackBusy } from './busy.js';
3
+ import { declareRoutes } from './observers/route.js';
4
+ import { setOutwardPolicy } from './actions/danger-context.js';
5
+ import { coverageMarkers, notApplicableFor } from './dom/addressing/coverage-markers.js';
2
6
  import { EventType, RETICLE_DEFAULT_PORT, RETICLE_PROTOCOL_VERSION, RETICLE_URL_PARAM, RETICLE_WS_PATH, bridgeWsUrl, ReticleCommand, MessageKind, SESSION_AUTO, SessionState, TRANSPORT_LIMITS, buildRedactionPolicy, isLoopbackHostname, isLocalPage, setActiveRedactionPolicy, wireRedactionKeys, RETICLE_ROOT_GLOBAL, RETICLE_SDK_VERSION_GLOBAL, CONTRACT_FINGERPRINT, CONTRACT_PARTS, newDocumentId, NO_EDITS_OBSERVED, PresenterMode, CommandActor, HudCorner, HudVisibility, } from '@reticlehq/core';
3
7
  import { rememberSessionLabel } from './session-continuity.js';
4
8
  import { editEpoch } from './edit-epoch.js';
@@ -12,6 +16,8 @@ import { registerCapabilities, setCapabilitiesListener, hasCapabilities, } from
12
16
  import { installAllObservers, runTeardowns } from './observers/install-all.js';
13
17
  import { setTestIdAttr } from './dom/addressing/testid-attr.js';
14
18
  import { setNetworkBodyMaxChars } from './observers/net-detail/network-body.js';
19
+ import { pageHref } from './security/page-location.js';
20
+ import { scrubKnownSecrets } from './security/serialization.js';
15
21
  import { installOverlay } from './presenter/chrome/overlay.js';
16
22
  import { LOG_ACTOR, LOG_KIND, LOG_RESULT } from './presenter/chrome/log-kinds.js';
17
23
  import { actionVerb } from './presenter/chrome/presenter-verbs.js';
@@ -20,9 +26,8 @@ import { resetClock } from './timers/clock.js';
20
26
  import { nativeWarn } from './timers/console/native-console.js';
21
27
  import { installRecorder } from './recorder/recorder.js';
22
28
  import { Annotator } from './review/annotator.js';
23
- export function shouldBlockProduction(nodeEnv, allowInProduction) {
24
- return 'production' === nodeEnv && !allowInProduction;
25
- }
29
+ import { helloMode, leaseToken, shouldBlockProduction, } from './connect-options.js';
30
+ export { helloMode, shouldBlockProduction } from './connect-options.js';
26
31
  export function withBridgePath(bridgeUrl) {
27
32
  let parsed;
28
33
  try {
@@ -73,6 +78,7 @@ export function connectionPolicy(pageHostname, bridgeUrl, allowNonLocalhost, tok
73
78
  }
74
79
  return { allowed: true };
75
80
  }
81
+ const MAX_PENDING_NARRATIONS = 20;
76
82
  const KILLED_MESSAGE = '[Reticle] killed for this page. Restart your dev server or reload the page to bring it back.';
77
83
  const BRIDGE_LOST_SUMMARY = 'Session ended - lost connection to Reticle (the agent is no longer running).';
78
84
  export function resolveSessionLabel(option, gen) {
@@ -141,6 +147,7 @@ export class Reticle {
141
147
  #overlay;
142
148
  #presenter;
143
149
  #pendingPushes = new Map();
150
+ #pendingNarrations = [];
144
151
  #pendingHud;
145
152
  #readPanelSettings;
146
153
  #panelRequested = false;
@@ -149,6 +156,7 @@ export class Reticle {
149
156
  #annotator;
150
157
  #eventCount = 0;
151
158
  #token;
159
+ #mode = {};
152
160
  #sdkVersion;
153
161
  #captureBodies = false;
154
162
  #sourceMapping = undefined;
@@ -156,8 +164,15 @@ export class Reticle {
156
164
  #redactKeys = [];
157
165
  #actHandle;
158
166
  connect(options = {}) {
159
- if (this.#connected)
167
+ if (this.#connected) {
168
+ const leased = leaseToken(options, this.#token);
169
+ if (leased !== undefined) {
170
+ this.#token = leased;
171
+ this.#transport?.redialAfterRefusal();
172
+ }
173
+ this.#adoptLateProjectId(options);
160
174
  return;
175
+ }
161
176
  if ('undefined' === typeof window || 'undefined' === typeof document)
162
177
  return;
163
178
  const proc = globalThis.process;
@@ -194,6 +209,7 @@ export class Reticle {
194
209
  this.#token =
195
210
  options.token !== undefined && options.token.length > 0 ? options.token : undefined;
196
211
  this.#projectId = identity.projectId;
212
+ this.#mode = helloMode(options);
197
213
  this.#applyRedaction(options.redact);
198
214
  this.#start = performance.now();
199
215
  this.#registry = createCommandRegistry();
@@ -281,6 +297,8 @@ export class Reticle {
281
297
  if (options.idleEndMs !== undefined)
282
298
  panelOptions.idleEndMs = options.idleEndMs;
283
299
  panelOptions.sessionId = this.#session;
300
+ if (this.#projectId !== undefined)
301
+ panelOptions.projectId = this.#projectId;
284
302
  panelOptions.onControl = (intent) => this.#emit(EventType.HUMAN_CONTROL, intent.text !== undefined
285
303
  ? { kind: intent.kind, text: intent.text }
286
304
  : { kind: intent.kind });
@@ -292,9 +310,14 @@ export class Reticle {
292
310
  const panel = new Presenter(panelOptions);
293
311
  this.#presenter = panel;
294
312
  panel.mount();
313
+ panel.showSdkVersion(this.#sdkVersion);
295
314
  for (const buffered of this.#pendingPushes.values())
296
315
  panel.handlePush(buffered);
297
316
  this.#pendingPushes.clear();
317
+ if (0 < this.#pendingNarrations.length)
318
+ panel.sessionStart();
319
+ for (const early of this.#pendingNarrations.splice(0))
320
+ panel.narrate(early.text, early.level, early.actor, early.summary);
298
321
  if (this.#pendingHud !== undefined)
299
322
  panel.placeHud(this.#pendingHud, undefined);
300
323
  this.#pendingHud = undefined;
@@ -333,9 +356,17 @@ export class Reticle {
333
356
  get connected() {
334
357
  return this.#connected;
335
358
  }
359
+ routes(patterns) {
360
+ declareRoutes(patterns);
361
+ }
336
362
  signal(name, data = {}) {
337
363
  this.#emit(EventType.SIGNAL, { name, data });
338
364
  }
365
+ busy(work) {
366
+ return trackBusy(work, (name, data) => {
367
+ this.signal(name, data);
368
+ });
369
+ }
339
370
  state(name, value) {
340
371
  this.#emit(EventType.STATE_CHANGE, { name, value });
341
372
  }
@@ -396,16 +427,26 @@ export class Reticle {
396
427
  setActiveRedactionPolicy(buildRedactionPolicy(config, nativeWarn));
397
428
  this.#redactKeys = wireRedactionKeys(config);
398
429
  }
430
+ #adoptLateProjectId(options) {
431
+ if (this.#projectId !== undefined)
432
+ return;
433
+ const late = resolveConnectIdentity(options, window.location.search).projectId;
434
+ if (late === undefined)
435
+ return;
436
+ this.#projectId = late;
437
+ this.#transport?.reannounce();
438
+ }
399
439
  #hello() {
400
440
  return {
401
441
  kind: MessageKind.HELLO,
402
442
  protocolVersion: RETICLE_PROTOCOL_VERSION,
403
443
  sessionId: this.#session,
404
444
  ...(this.#projectId === undefined ? {} : { projectId: this.#projectId }),
405
- url: location.href,
406
- title: document.title,
445
+ url: pageHref(),
446
+ title: scrubKnownSecrets(document.title),
407
447
  adapters: adapterNames(),
408
448
  ...(this.#token === undefined ? {} : { token: this.#token }),
449
+ ...this.#mode,
409
450
  hasCapabilities: hasCapabilities(),
410
451
  channels: declaredChannels(),
411
452
  platform: declaredPlatform(),
@@ -423,9 +464,25 @@ export class Reticle {
423
464
  };
424
465
  }
425
466
  async #handleCommand(command) {
467
+ if (command.name === ReticleCommand.MARKERS) {
468
+ const markers = coverageMarkers(document);
469
+ const buildPlugin = this.#sdkVersion !== undefined || this.#sourceMapping !== undefined;
470
+ const na = notApplicableFor({ buildPlugin, adapters: adapterNames(), markers });
471
+ return { ok: true, result: { markers, ...(0 === na.length ? {} : { notApplicable: na }) } };
472
+ }
426
473
  if (command.name === ReticleCommand.NARRATE) {
474
+ if (this.#presenter === undefined && this.#panelRequested) {
475
+ if (this.#pendingNarrations.length < MAX_PENDING_NARRATIONS)
476
+ this.#pendingNarrations.push({
477
+ text: str(command.args['text']),
478
+ level: str(command.args['level'], 'info'),
479
+ actor: narrationActor(command.args),
480
+ summary: true === command.args['summary'],
481
+ });
482
+ return { ok: true, result: { shown: false, buffered: true } };
483
+ }
427
484
  this.#presenter?.sessionStart();
428
- this.#presenter?.narrate(str(command.args['text']), str(command.args['level'], 'info'));
485
+ this.#presenter?.narrate(str(command.args['text']), str(command.args['level'], 'info'), narrationActor(command.args), true === command.args['summary']);
429
486
  return { ok: true, result: { shown: this.#presenter !== undefined } };
430
487
  }
431
488
  if (command.name === ReticleCommand.SESSION_CONFIG) {
@@ -471,6 +528,8 @@ export class Reticle {
471
528
  }
472
529
  this.#presenter?.sessionStart();
473
530
  await this.#presentBefore(command);
531
+ setPresenterViewer(CommandActor.HARNESS === command.by);
532
+ setOutwardPolicy(command.outward?.allowed);
474
533
  try {
475
534
  const result = await handler(command.args);
476
535
  this.#actHandle?.result(LOG_RESULT.PASS);
@@ -481,6 +540,8 @@ export class Reticle {
481
540
  return { ok: false, error: error instanceof Error ? error.message : String(error) };
482
541
  }
483
542
  finally {
543
+ setPresenterViewer(false);
544
+ setOutwardPolicy(undefined);
484
545
  this.#actHandle = undefined;
485
546
  this.#presenter?.scheduleIdle();
486
547
  }
@@ -515,6 +576,9 @@ export class Reticle {
515
576
  }
516
577
  }
517
578
  }
579
+ function narrationActor(args) {
580
+ return CommandActor.HARNESS === args['actor'] ? LOG_ACTOR.HARNESS : LOG_ACTOR.AGENT;
581
+ }
518
582
  function oneOf(values, raw) {
519
583
  return Object.values(values).includes(raw) ? raw : undefined;
520
584
  }
@@ -0,0 +1,2 @@
1
+ export declare function pageHref(): string;
2
+ export declare function pageRoute(withHash?: boolean): string;
@@ -0,0 +1,7 @@
1
+ import { redactUrl } from '@reticlehq/core';
2
+ export function pageHref() {
3
+ return redactUrl(location.href);
4
+ }
5
+ export function pageRoute(withHash = true) {
6
+ return redactUrl(`${location.pathname}${location.search}${withHash ? location.hash : ''}`);
7
+ }
@@ -29,7 +29,7 @@ function sanitize(value, state, depth, key) {
29
29
  if (null === value || 'boolean' === typeof value)
30
30
  return value;
31
31
  if ('string' === typeof value) {
32
- return boundedString(value, state, 'error' === key?.toLowerCase()
32
+ return boundedString(scrubKnownSecrets(value), state, 'error' === key?.toLowerCase()
33
33
  ? TRANSPORT_LIMITS.MAX_ERROR_LENGTH
34
34
  : TRANSPORT_LIMITS.MAX_STRING_LENGTH);
35
35
  }
@@ -5,10 +5,12 @@ export declare const ReticleStorageKey: {
5
5
  readonly PRESENTER_LOG_OWNER: "reticle-presenter-log-owner";
6
6
  readonly PRESENTER_SETTINGS: "reticle-presenter-settings";
7
7
  readonly PRESENTER_MINIMISED: "reticle-presenter-minimised";
8
+ readonly PRESENTER_OPEN: "reticle-presenter-open";
9
+ readonly PRESENTER_VIEW: "reticle-presenter-view";
8
10
  readonly CAROUSEL_DISMISSED: "reticle.carousel.dismissed";
9
- readonly HARNESS_OFFER_DISMISSED: "reticle.harnessOffer.dismissed";
10
11
  readonly ANNOTATION_HISTORY: "reticle.annotations.history.v1";
11
- readonly RECENT_PLAYS: "reticle.flow.recent.v1";
12
12
  };
13
13
  export declare const TOUR_SEEN_KEY_PREFIX = "reticle.tour.seen.";
14
+ export declare const PERSONA_KEY_PREFIX = "reticle.harness.persona.";
15
+ export declare const IMPACT_SEEN_KEY_PREFIX = "reticle.impact.seen.";
14
16
  export declare function isReticleStorageKey(key: string): boolean;
@@ -5,13 +5,18 @@ export const ReticleStorageKey = {
5
5
  PRESENTER_LOG_OWNER: 'reticle-presenter-log-owner',
6
6
  PRESENTER_SETTINGS: 'reticle-presenter-settings',
7
7
  PRESENTER_MINIMISED: 'reticle-presenter-minimised',
8
+ PRESENTER_OPEN: 'reticle-presenter-open',
9
+ PRESENTER_VIEW: 'reticle-presenter-view',
8
10
  CAROUSEL_DISMISSED: 'reticle.carousel.dismissed',
9
- HARNESS_OFFER_DISMISSED: 'reticle.harnessOffer.dismissed',
10
11
  ANNOTATION_HISTORY: 'reticle.annotations.history.v1',
11
- RECENT_PLAYS: 'reticle.flow.recent.v1',
12
12
  };
13
13
  export const TOUR_SEEN_KEY_PREFIX = 'reticle.tour.seen.';
14
+ export const PERSONA_KEY_PREFIX = 'reticle.harness.persona.';
15
+ export const IMPACT_SEEN_KEY_PREFIX = 'reticle.impact.seen.';
14
16
  const OWN_KEYS = new Set(Object.values(ReticleStorageKey));
15
17
  export function isReticleStorageKey(key) {
16
- return OWN_KEYS.has(key) || key.startsWith(TOUR_SEEN_KEY_PREFIX);
18
+ return (OWN_KEYS.has(key) ||
19
+ key.startsWith(TOUR_SEEN_KEY_PREFIX) ||
20
+ key.startsWith(PERSONA_KEY_PREFIX) ||
21
+ key.startsWith(IMPACT_SEEN_KEY_PREFIX));
17
22
  }
@@ -8,4 +8,5 @@ interface FrameOutcome {
8
8
  export declare const boundedFrame: (budgetMs?: number) => Promise<FrameOutcome>;
9
9
  export declare const nativeFrame: () => Promise<void>;
10
10
  export declare const settle: (budgetMs?: number) => Promise<FrameOutcome>;
11
+ export declare function asSettleBudget(raw: unknown): number | undefined;
11
12
  export {};
@@ -46,3 +46,8 @@ export const settle = async (budgetMs = FRAME_BUDGET_MS) => {
46
46
  await Promise.resolve();
47
47
  return boundedFrame(budgetMs);
48
48
  };
49
+ export function asSettleBudget(raw) {
50
+ if ('number' !== typeof raw || !Number.isFinite(raw))
51
+ return undefined;
52
+ return Math.max(0, Math.min(raw, FRAME_BUDGET_MS));
53
+ }
@@ -26,6 +26,7 @@ export declare class Transport {
26
26
  constructor(deps: TransportDeps);
27
27
  connect(): void;
28
28
  sendEvent(event: ReticleEvent): void;
29
+ redialAfterRefusal(): void;
29
30
  reannounce(): void;
30
31
  close(): void;
31
32
  }
@@ -26,6 +26,8 @@ export class Transport {
26
26
  #signalQueue = [];
27
27
  #insertOrder = 0;
28
28
  #closed = false;
29
+ #refused = false;
30
+ #redialOnRefusal = false;
29
31
  #disconnectedSince;
30
32
  #reconnectDelay = RECONNECT_DELAY_MS;
31
33
  #lost = false;
@@ -55,6 +57,7 @@ export class Transport {
55
57
  this.#reconnectDelay = RECONNECT_DELAY_MS;
56
58
  if (this.#ws !== undefined)
57
59
  return;
60
+ this.#refused = false;
58
61
  this.#open();
59
62
  }
60
63
  #open() {
@@ -92,9 +95,14 @@ export class Transport {
92
95
  ws.onclose = (event) => {
93
96
  this.#ws = undefined;
94
97
  if (event.code === WS_POLICY_VIOLATION) {
95
- this.#closed = true;
98
+ if (this.#redialOnRefusal && !this.#closed) {
99
+ this.#redialOnRefusal = false;
100
+ this.#open();
101
+ return;
102
+ }
103
+ this.#refused = true;
96
104
  const reason = event.reason.length > 0 ? event.reason : 'policy violation';
97
- nativeWarn(`[reticle] bridge refused the connection: ${reason} — not retrying.`);
105
+ nativeWarn(`[reticle] bridge refused the connection: ${reason} — not retrying until you come back to this tab.`);
98
106
  this.#deps.onConnectionLost?.();
99
107
  return;
100
108
  }
@@ -107,7 +115,7 @@ export class Transport {
107
115
  };
108
116
  }
109
117
  #scheduleReopen() {
110
- if (this.#closed)
118
+ if (this.#closed || this.#refused)
111
119
  return;
112
120
  const delay = this.#reconnectDelay;
113
121
  this.#reconnectDelay = nextReconnectDelay(delay);
@@ -195,6 +203,18 @@ export class Transport {
195
203
  type: event.type,
196
204
  });
197
205
  }
206
+ redialAfterRefusal() {
207
+ if (this.#closed) {
208
+ return;
209
+ }
210
+ if (this.#refused && this.#ws === undefined) {
211
+ this.#refused = false;
212
+ this.#reconnectDelay = RECONNECT_DELAY_MS;
213
+ this.#open();
214
+ return;
215
+ }
216
+ this.#redialOnRefusal = true;
217
+ }
198
218
  reannounce() {
199
219
  if (this.#ws === undefined || this.#ws.readyState !== WebSocket.OPEN)
200
220
  return;
@@ -1,2 +1,2 @@
1
1
  export declare function unreachableMessage(url: string, attempts: number): string;
2
- export declare function unreachableStripText(url: string, attempts: number): string;
2
+ export declare const UNREACHABLE_HEADLINE = "Can't reach Reticle from this page. Run reticle status to check, or reticle serve to start it.";
@@ -5,9 +5,7 @@ export function unreachableMessage(url, attempts) {
5
5
  `that is not there from one it is not allowed to reach, so this is not evidence about the ` +
6
6
  `daemon. What answers it: run \`npx @reticlehq/server status\` to see whether one is listening ` +
7
7
  `and on which port. If your app runs in a container, devcontainer or WSL, the daemon is on a ` +
8
- `different host, so set the URL explicitly (Vite: VITE_RETICLE_WS_URL, or ` +
9
- `reticle.connect({ url })). An https page cannot open a ws:// socket at all. Still retrying…`);
10
- }
11
- export function unreachableStripText(url, attempts) {
12
- return `no bridge at ${url} — ${String(attempts)} attempt${1 === attempts ? '' : 's'}`;
8
+ `different host, so point the page at it: RETICLE_PORT or reticle({ port }) with the Vite ` +
9
+ `plugin, reticle.connect({ url }) by hand. An https page cannot open a ws:// socket. Still retrying…`);
13
10
  }
11
+ export const UNREACHABLE_HEADLINE = "Can't reach Reticle from this page. Run reticle status to check, or reticle serve to start it.";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reticlehq/browser",
3
- "version": "3.6.0",
3
+ "version": "3.8.0",
4
4
  "description": "Reticle browser SDK — installs observers, builds semantic snapshots, executes actions, talks to the bridge.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -15,10 +15,14 @@
15
15
  "./inject": {
16
16
  "types": "./dist/reticle-inject.d.ts",
17
17
  "default": "./dist/reticle-inject.js"
18
+ },
19
+ "./activate": {
20
+ "types": "./dist/activate.d.ts",
21
+ "default": "./dist/activate.js"
18
22
  }
19
23
  },
20
24
  "dependencies": {
21
- "@reticlehq/core": "3.6.0"
25
+ "@reticlehq/core": "3.8.0"
22
26
  },
23
27
  "devDependencies": {
24
28
  "@iconify-json/heroicons-outline": "^1.2.1",
@@ -1,21 +0,0 @@
1
- export declare const OFFER_ATTR = "data-reticle-offer";
2
- export declare const OFFER_CLAIM_ATTR = "data-reticle-offer-claim";
3
- export declare const OFFER_DISMISSED_KEY: "reticle.harnessOffer.dismissed";
4
- export declare const OFFER_TEXT: {
5
- readonly HEADLINE: "Get Harness free for 3 months";
6
- readonly BODY: "Explores your app and saves every journey as a free replay.";
7
- readonly CLAIM: "Claim now";
8
- readonly DISMISS: "Not now";
9
- readonly claimed: (days: number) => string;
10
- readonly ending: (days: number) => string;
11
- };
12
- export declare function offerDismissed(storage: Pick<Storage, 'getItem'> | undefined): boolean;
13
- export interface OfferState {
14
- claimed: boolean;
15
- daysRemaining?: number | undefined;
16
- claimUrl?: string | undefined;
17
- eligible?: boolean | undefined;
18
- drivable?: boolean | undefined;
19
- }
20
- export declare function offerHtml(offer: OfferState | undefined, dismissed: boolean): string;
21
- export declare const OFFER_CSS: string;
@@ -1,56 +0,0 @@
1
- import { ReticleStorageKey } from '../../storage-keys.js';
2
- export const OFFER_ATTR = 'data-reticle-offer';
3
- export const OFFER_CLAIM_ATTR = 'data-reticle-offer-claim';
4
- export const OFFER_DISMISSED_KEY = ReticleStorageKey.HARNESS_OFFER_DISMISSED;
5
- export const OFFER_TEXT = {
6
- HEADLINE: 'Get Harness free for 3 months',
7
- BODY: 'Explores your app and saves every journey as a free replay.',
8
- CLAIM: 'Claim now',
9
- DISMISS: 'Not now',
10
- claimed: (days) => `Harness active — ${String(days)} days left`,
11
- ending: (days) => 1 >= days
12
- ? 'Harness free period ends today'
13
- : `Harness free period ends in ${String(days)} days`,
14
- };
15
- const ENDING_SOON_DAYS = 14;
16
- export function offerDismissed(storage) {
17
- try {
18
- return '1' === storage?.getItem(OFFER_DISMISSED_KEY);
19
- }
20
- catch {
21
- return false;
22
- }
23
- }
24
- const escape = (text) => text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
25
- export function offerHtml(offer, dismissed) {
26
- if (offer === undefined)
27
- return '';
28
- if (offer.claimed) {
29
- const days = offer.daysRemaining;
30
- if (days === undefined || 0 > days)
31
- return '';
32
- const text = ENDING_SOON_DAYS >= days ? OFFER_TEXT.ending(days) : OFFER_TEXT.claimed(days);
33
- return `<div ${OFFER_ATTR} class="reticle-offer reticle-offer-claimed">${escape(text)}</div>`;
34
- }
35
- if (false === offer.eligible)
36
- return '';
37
- if (false === offer.drivable)
38
- return '';
39
- if (dismissed)
40
- return '';
41
- const url = offer.claimUrl;
42
- if (url === undefined || 0 === url.length)
43
- return '';
44
- return `<div ${OFFER_ATTR} class="reticle-offer" role="note">
45
- <span class="reticle-offer-title">${escape(OFFER_TEXT.HEADLINE)}</span>
46
- <p class="reticle-offer-body">${escape(OFFER_TEXT.BODY)}</p>
47
- <a ${OFFER_CLAIM_ATTR} class="reticle-offer-claim" href="${escape(url)}" target="_blank" rel="noopener noreferrer">${escape(OFFER_TEXT.CLAIM)}</a>
48
- </div>`;
49
- }
50
- export const OFFER_CSS = `
51
- .reticle-offer{margin:8px 10px;padding:10px 12px;border:1px solid var(--reticle-line,#2a2f3a);border-radius:10px;background:var(--reticle-surface-inset,rgba(255,255,255,.03));}
52
- .reticle-offer-title{display:block;font-weight:600;font-size:12px;padding-right:18px;}
53
- .reticle-offer-body{margin:6px 0 8px;font-size:11px;line-height:1.45;opacity:.8;}
54
- .reticle-offer-claim{display:inline-block;font-size:11px;font-weight:600;padding:4px 10px;border-radius:6px;background:var(--reticle-accent,#4f7cff);color:#fff;text-decoration:none;}
55
- .reticle-offer-claimed{margin:6px 10px;font-size:10px;opacity:.6;}
56
- `;