yaver-feedback-react-native 0.9.0 → 0.9.3

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 (144) hide show
  1. package/README.md +102 -1
  2. package/app.plugin.js +126 -9
  3. package/dist/AuthOverlay.d.ts +1 -14
  4. package/dist/AuthOverlay.js +9 -62
  5. package/dist/BlackBox.js +9 -1
  6. package/dist/DeployPanel.js +65 -0
  7. package/dist/Discovery.js +13 -8
  8. package/dist/DogfoodRuntime.d.ts +124 -0
  9. package/dist/DogfoodRuntime.js +273 -0
  10. package/dist/FeedbackModal.js +505 -65
  11. package/dist/LoginScreen.d.ts +1 -5
  12. package/dist/LoginScreen.js +2 -6
  13. package/dist/MachinePickerScreen.d.ts +1 -3
  14. package/dist/MachinePickerScreen.js +6 -18
  15. package/dist/P2PClient.d.ts +178 -2
  16. package/dist/P2PClient.js +493 -3
  17. package/dist/P2PDogfoodDriver.d.ts +12 -0
  18. package/dist/P2PDogfoodDriver.js +118 -0
  19. package/dist/PairDeviceModal.d.ts +2 -3
  20. package/dist/ShakeDetector.js +2 -0
  21. package/dist/VibeChatScreen.d.ts +11 -1
  22. package/dist/VibeChatScreen.js +200 -41
  23. package/dist/YaverFeedback.d.ts +132 -0
  24. package/dist/YaverFeedback.js +633 -65
  25. package/dist/YaverModeBadge.d.ts +22 -0
  26. package/dist/YaverModeBadge.js +219 -0
  27. package/dist/__tests__/AuthDevices.test.js +1 -48
  28. package/dist/__tests__/BlackBox.relayPassword.test.d.ts +1 -0
  29. package/dist/__tests__/BlackBox.relayPassword.test.js +105 -0
  30. package/dist/__tests__/BlackBoxAutoStart.test.d.ts +1 -0
  31. package/dist/__tests__/BlackBoxAutoStart.test.js +91 -0
  32. package/dist/__tests__/BlackBoxAutoStartColdStart.test.d.ts +1 -0
  33. package/dist/__tests__/BlackBoxAutoStartColdStart.test.js +156 -0
  34. package/dist/__tests__/BrowserLaneIcon.test.d.ts +3 -0
  35. package/dist/__tests__/BrowserLaneIcon.test.js +80 -0
  36. package/dist/__tests__/DogfoodRuntime.test.d.ts +1 -0
  37. package/dist/__tests__/DogfoodRuntime.test.js +116 -0
  38. package/dist/__tests__/P2PClient.test.js +2 -2
  39. package/dist/__tests__/P2PDogfoodDriver.test.d.ts +1 -0
  40. package/dist/__tests__/P2PDogfoodDriver.test.js +64 -0
  41. package/dist/__tests__/ReportIdentity.test.d.ts +25 -0
  42. package/dist/__tests__/ReportIdentity.test.js +180 -0
  43. package/dist/__tests__/SDKToken.test.js +1 -1
  44. package/dist/__tests__/ShakeToggle.test.d.ts +1 -0
  45. package/dist/__tests__/ShakeToggle.test.js +121 -0
  46. package/dist/__tests__/YaverFeedback.test.js +13 -3
  47. package/dist/__tests__/deviceDogfood.test.d.ts +1 -0
  48. package/dist/__tests__/deviceDogfood.test.js +86 -0
  49. package/dist/__tests__/dogfoodPolicy.test.d.ts +1 -0
  50. package/dist/__tests__/dogfoodPolicy.test.js +33 -0
  51. package/dist/__tests__/pickTargetDevice.test.d.ts +1 -0
  52. package/dist/__tests__/pickTargetDevice.test.js +89 -0
  53. package/dist/__tests__/reloadActions.test.d.ts +1 -0
  54. package/dist/__tests__/reloadActions.test.js +129 -0
  55. package/dist/__tests__/reloadActionsParity.test.d.ts +1 -0
  56. package/dist/__tests__/reloadActionsParity.test.js +42 -0
  57. package/dist/__tests__/types.test.js +5 -5
  58. package/dist/_core/ansi.d.ts +117 -0
  59. package/dist/_core/ansi.js +468 -0
  60. package/dist/_core/ansi.test.d.ts +1 -0
  61. package/dist/_core/ansi.test.js +225 -0
  62. package/dist/_core/buildFeedbackPrompt.d.ts +4 -9
  63. package/dist/_core/buildFeedbackPrompt.js +18 -72
  64. package/dist/_core/constants.d.ts +19 -6
  65. package/dist/_core/constants.js +20 -7
  66. package/dist/_core/device.d.ts +1 -5
  67. package/dist/_core/device.js +5 -14
  68. package/dist/_core/endpoints.d.ts +0 -7
  69. package/dist/_core/endpoints.js +0 -7
  70. package/dist/_core/index.d.ts +4 -0
  71. package/dist/_core/index.js +4 -0
  72. package/dist/_core/remoteless.d.ts +44 -0
  73. package/dist/_core/remoteless.js +75 -0
  74. package/dist/_core/trace.d.ts +47 -0
  75. package/dist/_core/trace.js +38 -0
  76. package/dist/_core/trace.test.d.ts +1 -0
  77. package/dist/_core/trace.test.js +60 -0
  78. package/dist/auth.d.ts +3 -60
  79. package/dist/auth.js +6 -88
  80. package/dist/deviceDogfood.d.ts +53 -0
  81. package/dist/deviceDogfood.js +137 -0
  82. package/dist/dogfoodPolicy.d.ts +25 -0
  83. package/dist/dogfoodPolicy.js +24 -0
  84. package/dist/index.d.ts +17 -4
  85. package/dist/index.js +33 -7
  86. package/dist/reloadActions.d.ts +88 -0
  87. package/dist/reloadActions.js +200 -0
  88. package/dist/storeShots.d.ts +67 -0
  89. package/dist/storeShots.js +137 -0
  90. package/dist/types.d.ts +208 -8
  91. package/package.json +13 -4
  92. package/src/AuthOverlay.tsx +20 -106
  93. package/src/BlackBox.ts +10 -1
  94. package/src/DeployPanel.tsx +74 -0
  95. package/src/Discovery.ts +13 -8
  96. package/src/DogfoodRuntime.ts +373 -0
  97. package/src/FeedbackModal.tsx +619 -79
  98. package/src/LoginScreen.tsx +2 -22
  99. package/src/MachinePickerScreen.tsx +6 -21
  100. package/src/P2PClient.ts +580 -4
  101. package/src/P2PDogfoodDriver.ts +132 -0
  102. package/src/PairDeviceModal.tsx +2 -3
  103. package/src/ShakeDetector.ts +1 -0
  104. package/src/VibeChatScreen.tsx +232 -42
  105. package/src/YaverFeedback.ts +631 -68
  106. package/src/YaverModeBadge.tsx +234 -0
  107. package/src/__tests__/AuthDevices.test.ts +1 -52
  108. package/src/__tests__/BlackBox.relayPassword.test.ts +129 -0
  109. package/src/__tests__/BlackBoxAutoStart.test.ts +111 -0
  110. package/src/__tests__/BlackBoxAutoStartColdStart.test.ts +191 -0
  111. package/src/__tests__/BrowserLaneIcon.test.ts +85 -0
  112. package/src/__tests__/DogfoodRuntime.test.ts +135 -0
  113. package/src/__tests__/P2PClient.test.ts +2 -2
  114. package/src/__tests__/P2PDogfoodDriver.test.ts +80 -0
  115. package/src/__tests__/ReportIdentity.test.ts +212 -0
  116. package/src/__tests__/SDKToken.test.ts +1 -1
  117. package/src/__tests__/ShakeToggle.test.ts +153 -0
  118. package/src/__tests__/YaverFeedback.test.ts +15 -3
  119. package/src/__tests__/deviceDogfood.test.ts +77 -0
  120. package/src/__tests__/dogfoodPolicy.test.ts +34 -0
  121. package/src/__tests__/pickTargetDevice.test.ts +101 -0
  122. package/src/__tests__/reloadActions.test.ts +171 -0
  123. package/src/__tests__/reloadActionsParity.test.ts +49 -0
  124. package/src/__tests__/types.test.ts +5 -5
  125. package/src/_core/ansi.test.ts +250 -0
  126. package/src/_core/ansi.ts +475 -0
  127. package/src/_core/buildFeedbackPrompt.ts +18 -95
  128. package/src/_core/constants.ts +20 -6
  129. package/src/_core/device.ts +5 -16
  130. package/src/_core/endpoints.ts +0 -7
  131. package/src/_core/index.ts +4 -0
  132. package/src/_core/remoteless.ts +110 -0
  133. package/src/_core/trace.test.ts +58 -0
  134. package/src/_core/trace.ts +75 -0
  135. package/src/auth.ts +7 -156
  136. package/src/deviceDogfood.ts +171 -0
  137. package/src/dogfoodPolicy.ts +40 -0
  138. package/src/index.ts +61 -11
  139. package/src/reloadActions.ts +273 -0
  140. package/src/storeShots.ts +189 -0
  141. package/src/types.ts +209 -8
  142. package/dist/GuestOnboardingScreen.d.ts +0 -8
  143. package/dist/GuestOnboardingScreen.js +0 -282
  144. package/src/GuestOnboardingScreen.tsx +0 -307
@@ -6,8 +6,17 @@ const Discovery_1 = require("./Discovery");
6
6
  const BlackBox_1 = require("./BlackBox");
7
7
  const P2PClient_1 = require("./P2PClient");
8
8
  const ShakeDetector_1 = require("./ShakeDetector");
9
+ const storeShots_1 = require("./storeShots");
9
10
  const auth_1 = require("./auth");
10
11
  const preferences_1 = require("./preferences");
12
+ const dogfoodPolicy_1 = require("./dogfoodPolicy");
13
+ const deviceDogfood_1 = require("./deviceDogfood");
14
+ function unrefTimer(timer) {
15
+ const maybeNodeTimer = timer;
16
+ if (typeof maybeNodeTimer.unref === 'function') {
17
+ maybeNodeTimer.unref();
18
+ }
19
+ }
11
20
  // Is this JS runtime the Yaver mobile app's super-host bridge? The
12
21
  // YaverInfo native module is only registered inside Yaver's container
13
22
  // (mobile/ios/Yaver/YaverInfo.{swift,m} + Android counterpart); a
@@ -133,10 +142,19 @@ if (IS_HOST_MODE) {
133
142
  let config = null;
134
143
  let enabled = false;
135
144
  let p2pClient = null;
145
+ let renderP2PClient = null;
136
146
  let shakeDetector = null;
147
+ /** Unsubscribe for the BlackBox command handler registered by init(). Held so
148
+ * a re-init (or destroy) can drop the old one instead of stacking. */
149
+ let commandUnsubscribe = null;
137
150
  let p2pAuthToken = null;
138
151
  let p2pRelayPassword = '';
152
+ /** Pending BlackBox auto-start retry. Held so destroy()/re-init can cancel it
153
+ * instead of leaving a timer chain running against a torn-down config. */
154
+ let autoStartTimer = null;
139
155
  let reportLaunchInFlight = false;
156
+ let crashReportInFlight = false;
157
+ let dogfoodOnboarding = null;
140
158
  /** Resolve the user's relay password by validating their auth token
141
159
  * against Convex. Used whenever we (re)build the P2PClient so a
142
160
  * relay-routed agentUrl carries a valid X-Relay-Password — without
@@ -182,6 +200,7 @@ async function resolveRelayPassword(authToken, convexUrl) {
182
200
  /** Ring buffer of captured errors. */
183
201
  let errorBuffer = [];
184
202
  let maxErrors = 5;
203
+ let crashHandlerInstalled = false;
185
204
  /** Track whether BlackBox was running before disable (to restart on enable). */
186
205
  let blackBoxWasStreaming = false;
187
206
  /**
@@ -203,40 +222,37 @@ const flagCache = new Map();
203
222
  */
204
223
  class YaverFeedback {
205
224
  static async resolveP2PAuthToken() {
206
- if (!config?.authToken)
207
- return null;
208
- if (!config.preferredDeviceId)
209
- return config.authToken;
210
- const devices = await (0, auth_1.listReachableDevices)(config.authToken);
211
- const all = [...devices.owned, ...devices.shared];
212
- const selected = all.find((device) => device.deviceId === config?.preferredDeviceId);
213
- if (!selected || !selected.isGuest || selected.accessScope !== 'shared-scoped') {
214
- return config.authToken;
215
- }
216
- if (!selected.hostUserId) {
217
- return config.authToken;
218
- }
219
- const delegated = await (0, auth_1.mintGuestSdkToken)(config.authToken, selected.hostUserId, selected.deviceId);
220
- return delegated.token;
225
+ return config?.authToken ?? null;
221
226
  }
222
227
  static async rebuildP2PClient(agentUrl) {
223
- if (!config)
228
+ const currentConfig = config;
229
+ if (!currentConfig)
224
230
  return;
225
- const effectiveUrl = agentUrl ?? config.agentUrl;
231
+ const effectiveUrl = agentUrl ?? currentConfig.agentUrl;
226
232
  if (!effectiveUrl) {
227
233
  p2pClient = null;
234
+ renderP2PClient = null;
228
235
  p2pAuthToken = null;
229
236
  return;
230
237
  }
231
238
  const token = await YaverFeedback.resolveP2PAuthToken();
239
+ // reset() can clear config while relay-password resolution is awaiting.
240
+ // Do not resurrect a client from that stale init generation.
241
+ if (config !== currentConfig)
242
+ return;
232
243
  if (!token) {
233
244
  p2pClient = null;
245
+ renderP2PClient = null;
234
246
  p2pAuthToken = null;
235
247
  return;
236
248
  }
237
249
  p2pAuthToken = token;
238
250
  const rp = await resolveRelayPassword(token);
251
+ if (config !== currentConfig)
252
+ return;
239
253
  p2pClient = new P2PClient_1.P2PClient(effectiveUrl, token, rp);
254
+ const renderUrl = currentConfig.renderAgentUrl || effectiveUrl;
255
+ renderP2PClient = renderUrl === effectiveUrl ? p2pClient : new P2PClient_1.P2PClient(renderUrl, token, rp);
240
256
  }
241
257
  /**
242
258
  * Initialize the feedback SDK with the given configuration.
@@ -245,12 +261,127 @@ class YaverFeedback {
245
261
  * If no `agentUrl` is provided, the SDK will attempt auto-discovery
246
262
  * via `YaverDiscovery` on the first `startReport()` call.
247
263
  */
264
+ /** The runtime lane, when running on web inside a Yaver preview WebView. */
265
+ static detectWebLane() {
266
+ try {
267
+ if (react_native_1.Platform.OS !== 'web' || typeof window === 'undefined')
268
+ return null;
269
+ const l = String(window.__yaverLane || '').toLowerCase();
270
+ if (l === 'browser' || l === 'webrtc')
271
+ return l;
272
+ }
273
+ catch { /* noop */ }
274
+ return null;
275
+ }
276
+ /**
277
+ * Mount a draggable DOM floating "Y" icon for the BROWSER lane (RN-web). It
278
+ * lives in the WebView's document.body (position:fixed, max z-index), so it
279
+ * renders ON TOP of the page inside the WebView and is never occluded by the
280
+ * fullScreen preview modal — the one occlusion-proof affordance on iOS.
281
+ * Tap opens the SDK's existing report flow (the RN FeedbackModal renders in
282
+ * the same WebView DOM, also un-occluded). No-op off-web or off-lane, and
283
+ * idempotent (a re-init reuses the existing node).
284
+ */
285
+ static mountBrowserLaneIcon() {
286
+ if (YaverFeedback.detectWebLane() !== 'browser')
287
+ return;
288
+ try {
289
+ const doc = globalThis.document;
290
+ if (!doc || !doc.body)
291
+ return;
292
+ if (doc.getElementById('yaver-feedback-btn'))
293
+ return; // idempotent
294
+ const btn = doc.createElement('div');
295
+ btn.id = 'yaver-feedback-btn';
296
+ btn.textContent = 'Y';
297
+ btn.title = 'Yaver Feedback — drag to move · tap to report';
298
+ btn.style.cssText =
299
+ 'position:fixed;bottom:20px;right:20px;width:44px;height:44px;border-radius:50%;' +
300
+ 'background:#6366f1;color:#fff;display:flex;align-items:center;justify-content:center;' +
301
+ 'cursor:pointer;z-index:99999;font-size:18px;font-weight:bold;touch-action:none;' +
302
+ 'box-shadow:0 4px 12px rgba(99,102,241,0.4);transition:transform 0.2s;';
303
+ const drag = { on: false, moved: false, sx: 0, sy: 0, ox: 0, oy: 0 };
304
+ const w = window;
305
+ const applyPos = (x, y) => {
306
+ const m = 8;
307
+ const nx = Math.max(m, Math.min(w.innerWidth - 44 - m, x));
308
+ const ny = Math.max(m, Math.min(w.innerHeight - 44 - m, y));
309
+ btn.style.left = nx + 'px';
310
+ btn.style.top = ny + 'px';
311
+ btn.style.right = 'auto';
312
+ btn.style.bottom = 'auto';
313
+ try {
314
+ localStorage.setItem('yaver-feedback-btn-pos', JSON.stringify({ x: nx, y: ny }));
315
+ }
316
+ catch { /* noop */ }
317
+ };
318
+ try {
319
+ const s = localStorage.getItem('yaver-feedback-btn-pos');
320
+ if (s) {
321
+ const p = JSON.parse(s);
322
+ if (typeof p?.x === 'number')
323
+ applyPos(p.x, p.y);
324
+ }
325
+ }
326
+ catch { /* noop */ }
327
+ btn.addEventListener('pointerdown', (e) => {
328
+ drag.on = true;
329
+ drag.moved = false;
330
+ const r = btn.getBoundingClientRect();
331
+ drag.sx = e.clientX;
332
+ drag.sy = e.clientY;
333
+ drag.ox = r.left;
334
+ drag.oy = r.top;
335
+ btn.setPointerCapture(e.pointerId);
336
+ btn.style.transition = 'none';
337
+ });
338
+ btn.addEventListener('pointermove', (e) => {
339
+ if (!drag.on)
340
+ return;
341
+ const dx = e.clientX - drag.sx, dy = e.clientY - drag.sy;
342
+ if (Math.abs(dx) + Math.abs(dy) > 5)
343
+ drag.moved = true;
344
+ if (drag.moved)
345
+ applyPos(drag.ox + dx, drag.oy + dy);
346
+ });
347
+ const end = (e) => {
348
+ if (!drag.on)
349
+ return;
350
+ drag.on = false;
351
+ btn.style.transition = 'transform 0.2s';
352
+ try {
353
+ btn.releasePointerCapture(e.pointerId);
354
+ }
355
+ catch { /* noop */ }
356
+ if (!drag.moved) {
357
+ try {
358
+ void YaverFeedback.startReport();
359
+ }
360
+ catch { /* noop */ }
361
+ }
362
+ };
363
+ btn.addEventListener('pointerup', end);
364
+ btn.addEventListener('pointercancel', end);
365
+ // Also answer the host's forwarded shake (Yaver injects yaver-feedback:launch).
366
+ const launch = () => { try {
367
+ void YaverFeedback.startReport();
368
+ }
369
+ catch { /* noop */ } };
370
+ window.addEventListener('yaver-feedback:launch', launch);
371
+ window.__yaverFeedbackLaunch = launch;
372
+ doc.body.appendChild(btn);
373
+ }
374
+ catch { /* noop — never break the guest app over a feedback affordance */ }
375
+ }
248
376
  static init(cfg) {
249
377
  config = {
250
378
  trigger: 'shake',
251
379
  maxRecordingDuration: 120,
252
380
  autoLogin: true,
253
381
  ...cfg,
382
+ agentUrl: cfg.codingAgentUrl || cfg.agentUrl,
383
+ codingDeviceId: cfg.codingDeviceId || cfg.preferredDeviceId,
384
+ renderDeviceId: cfg.renderDeviceId || cfg.codingDeviceId || cfg.preferredDeviceId,
254
385
  };
255
386
  if (IS_HOST_MODE) {
256
387
  // sfmg / talos / etc. running inside Yaver mobile (compile-time
@@ -272,6 +403,9 @@ class YaverFeedback {
272
403
  }
273
404
  maxErrors = cfg.maxCapturedErrors ?? 5;
274
405
  errorBuffer = [];
406
+ if (cfg.crashReporting?.enabled && cfg.crashReporting?.installGlobalHandler) {
407
+ YaverFeedback.installCrashHandler();
408
+ }
275
409
  return;
276
410
  }
277
411
  if (config.disableShakeGesture && (!config.quickIcon || config.quickIcon === 'auto')) {
@@ -321,12 +455,17 @@ class YaverFeedback {
321
455
  // is set, so set a placeholder header here that won't 401 a
322
456
  // direct-LAN url and will be overwritten before any relay hop.
323
457
  p2pClient = new P2PClient_1.P2PClient(config.agentUrl, config.authToken ?? '', p2pRelayPassword);
458
+ const renderUrl = config.renderAgentUrl || config.agentUrl;
459
+ renderP2PClient = renderUrl === config.agentUrl
460
+ ? p2pClient
461
+ : new P2PClient_1.P2PClient(renderUrl, config.authToken ?? '', p2pRelayPassword);
324
462
  if (config.authToken) {
325
463
  void YaverFeedback.rebuildP2PClient(config.agentUrl);
326
464
  }
327
465
  }
328
466
  else {
329
467
  p2pClient = null;
468
+ renderP2PClient = null;
330
469
  // Auto-discover agent in the background when convexUrl or LAN is available
331
470
  if (enabled && (config.authToken || config.preferredDeviceId)) {
332
471
  YaverFeedback.discoverAgent();
@@ -335,23 +474,23 @@ class YaverFeedback {
335
474
  // Set up error capture buffer size
336
475
  maxErrors = cfg.maxCapturedErrors ?? 5;
337
476
  errorBuffer = [];
338
- // Wire up shake detection when trigger is 'shake'
339
- if (shakeDetector) {
340
- shakeDetector.stop();
341
- shakeDetector = null;
342
- }
343
- if (enabled && config.trigger === 'shake' && !config.disableShakeGesture) {
344
- shakeDetector = new ShakeDetector_1.ShakeDetector();
345
- shakeDetector.start(() => {
346
- YaverFeedback.notifyShake();
347
- if (config?.reportingOnly) {
348
- YaverFeedback.sendAutoReport();
349
- }
350
- else {
351
- YaverFeedback.startReport();
352
- }
353
- });
477
+ if (cfg.crashReporting?.enabled && cfg.crashReporting?.installGlobalHandler) {
478
+ YaverFeedback.installCrashHandler();
354
479
  }
480
+ // Wire up shake detection when trigger is 'shake'. Rebuild from scratch:
481
+ // config was just replaced, so the old detector may be closing over stale
482
+ // settings.
483
+ YaverFeedback.stopShakeDetector();
484
+ YaverFeedback.syncShakeDetector();
485
+ // Browser lane (RN-web inside Yaver's fullScreen WebView preview): the
486
+ // native shake module doesn't exist on web, and Yaver's own container
487
+ // overlay is OCCLUDED behind the WebView modal on iOS. So mount an
488
+ // occlusion-proof DRAGGABLE DOM icon INSIDE the WebView — same pattern as
489
+ // yaver-feedback-web. Lane is stamped by Yaver via
490
+ // window.__yaverLane='browser' (mobile PREVIEW_LANE_SCRIPT). See
491
+ // docs/audits/feedback-sdk-lanes-audit-2026-07-28.md.
492
+ if (enabled)
493
+ YaverFeedback.mountBrowserLaneIcon();
355
494
  // Wire up BlackBox command handlers for reload + status signals from
356
495
  // the agent. Opens an SSE channel the agent uses to:
357
496
  // - broadcast reload_bundle so the phone picks up a fresh Hermes
@@ -360,7 +499,14 @@ class YaverFeedback {
360
499
  // assets…", "Done") so the SDK can surface it like a normal
361
500
  // "Working…" spinner instead of a silent 60-second freeze.
362
501
  if (enabled) {
363
- BlackBox_1.BlackBox.onCommand((cmd) => {
502
+ // onCommand appends to a static array and hands back an unsubscribe.
503
+ // Dropping that unsubscribe meant every re-init stacked another handler
504
+ // on top of the last, so an app that re-initialised (say, a settings
505
+ // toggle) reloaded twice per agent command, then three times, and so on.
506
+ // destroy() never cleared them either. Drop the previous registration
507
+ // first so init() is idempotent here.
508
+ commandUnsubscribe?.();
509
+ commandUnsubscribe = BlackBox_1.BlackBox.onCommand((cmd) => {
364
510
  if (cmd.command === 'reload') {
365
511
  if (cfg.onReload) {
366
512
  cfg.onReload();
@@ -423,17 +569,24 @@ class YaverFeedback {
423
569
  // SFMG used to call BlackBox.start() inside YaverFeedbackWidget
424
570
  // after auth — that path still works (start() is idempotent), so
425
571
  // upgrading SDK without removing the manual call is safe.
572
+ // 4. RETRY, rather than checking once. This was a single 500ms
573
+ // timeout, and on a cold start it could not succeed: an app that
574
+ // passes neither agentUrl nor authToken to init() (the normal
575
+ // case — they're restored from storage) gets its session from
576
+ // `void hydrateSession()`, which reads two AsyncStorage keys and
577
+ // then awaits discoverAgent(), a Convex round trip plus LAN
578
+ // probing. That does not finish in 500ms, so the check found no
579
+ // agentUrl, gave up, and BlackBox never started — meaning no SSE
580
+ // channel, so the agent's `reload_bundle` command had nowhere to
581
+ // land and Hermes hot reload silently did nothing until the user
582
+ // shook the device and drove discovery by hand.
583
+ //
584
+ // The both-conditions guard is what keeps the 401 retry storm
585
+ // from coming back, so it is preserved exactly; we only re-check
586
+ // it over time instead of once. Bounded so a device that never
587
+ // signs in doesn't poll forever.
426
588
  if (cfg.autoStartBlackBox !== false) {
427
- setTimeout(() => {
428
- if (config?.agentUrl && (config?.authToken || p2pAuthToken)) {
429
- try {
430
- BlackBox_1.BlackBox.start();
431
- }
432
- catch (err) {
433
- console.warn('[YaverFeedback] BlackBox auto-start failed:', err);
434
- }
435
- }
436
- }, 500);
589
+ YaverFeedback.scheduleBlackBoxAutoStart();
437
590
  }
438
591
  }
439
592
  // NOTE: We intentionally do NOT hook ErrorUtils.setGlobalHandler().
@@ -446,6 +599,41 @@ class YaverFeedback {
446
599
  // - YaverFeedback.wrapErrorHandler(existingHandler) to create a
447
600
  // pass-through wrapper they insert into their own error chain
448
601
  }
602
+ /**
603
+ * Opt-in global crash handler. This wraps the existing React Native
604
+ * ErrorUtils handler and still calls it, so Sentry/Crashlytics/Bugsnag can
605
+ * remain the system of record. The SDK only uploads a crash report and,
606
+ * when configured, asks the agent to create a fix task.
607
+ */
608
+ static installCrashHandler() {
609
+ if (crashHandlerInstalled)
610
+ return;
611
+ try {
612
+ const errorUtils = globalThis?.ErrorUtils;
613
+ if (!errorUtils?.getGlobalHandler || !errorUtils?.setGlobalHandler) {
614
+ return;
615
+ }
616
+ const next = errorUtils.getGlobalHandler();
617
+ errorUtils.setGlobalHandler((error, isFatal) => {
618
+ YaverFeedback.attachError(error, {
619
+ source: 'global-handler',
620
+ crashAware: true,
621
+ });
622
+ if (errorBuffer.length > 0) {
623
+ errorBuffer[errorBuffer.length - 1].isFatal = isFatal ?? true;
624
+ }
625
+ void YaverFeedback.reportCrash(error, {
626
+ isFatal: isFatal ?? true,
627
+ source: 'global-handler',
628
+ });
629
+ next?.(error, isFatal);
630
+ });
631
+ crashHandlerInstalled = true;
632
+ }
633
+ catch (err) {
634
+ console.warn('[YaverFeedback] installCrashHandler failed:', err);
635
+ }
636
+ }
449
637
  /**
450
638
  * Run agent discovery in the background.
451
639
  * Called automatically from init() when no agentUrl is provided.
@@ -547,11 +735,64 @@ class YaverFeedback {
547
735
  else {
548
736
  await YaverFeedback.discoverAgent();
549
737
  }
738
+ // Signing in is the event the auto-start was waiting for. If its bounded
739
+ // retry already gave up (device left signed out past the window), this is
740
+ // what brings the command channel up without requiring an app restart.
741
+ if (config.autoStartBlackBox !== false && !BlackBox_1.BlackBox.isStreaming) {
742
+ YaverFeedback.scheduleBlackBoxAutoStart();
743
+ }
550
744
  }
551
745
  /** Returns true once the SDK has a session token it can use. */
552
746
  static isAuthed() {
553
747
  return Boolean(config?.authToken);
554
748
  }
749
+ /**
750
+ * On-device App Store screenshot capture (Engine 2). Walks the routes
751
+ * the host hands us, screenshots each, and uploads to the agent which
752
+ * runs the App Store Connect backend. agentUrl / authToken / relay
753
+ * password default to the SDK's resolved session; the host only has to
754
+ * supply a navigation ref + the ordered route list.
755
+ *
756
+ * YaverFeedback.captureStoreScreenshots({
757
+ * app: 'sfmg',
758
+ * navigationRef,
759
+ * routes: ['/(tabs)/dashboard', '/(tabs)/messages', '/(tabs)/clients'],
760
+ * submit: true,
761
+ * })
762
+ */
763
+ static async captureStoreScreenshots(opts) {
764
+ const agentUrl = opts.agentUrl ?? config?.agentUrl ?? '';
765
+ const authToken = opts.authToken ?? config?.authToken ?? p2pAuthToken ?? '';
766
+ const relayPassword = opts.relayPassword ?? p2pRelayPassword;
767
+ if (!agentUrl || !authToken) {
768
+ return {
769
+ ok: false,
770
+ captured: 0,
771
+ uploaded: 0,
772
+ message: 'YaverFeedback not connected to an agent — init() with agentUrl + authToken first.',
773
+ };
774
+ }
775
+ return (0, storeShots_1.captureStoreScreenshots)({ ...opts, agentUrl, authToken, relayPassword });
776
+ }
777
+ /**
778
+ * Let the mobile app / CLI kick THIS device into self-capturing. Registers
779
+ * a BlackBox command listener that fires captureStoreScreenshots when the
780
+ * agent pushes a `capture_store_shots` command (its `data` may override
781
+ * `submit` / `locale`). Returns an unsubscribe fn. Call once after init,
782
+ * passing the app's navigation ref + the routes to walk.
783
+ */
784
+ static enableStoreShotsOnCommand(opts) {
785
+ return BlackBox_1.BlackBox.onCommand((cmd) => {
786
+ if (cmd?.command !== 'capture_store_shots')
787
+ return;
788
+ const data = cmd.data ?? {};
789
+ void YaverFeedback.captureStoreScreenshots({
790
+ ...opts,
791
+ submit: typeof data.submit === 'boolean' ? data.submit : opts.submit,
792
+ locale: typeof data.locale === 'string' ? data.locale : opts.locale,
793
+ });
794
+ });
795
+ }
555
796
  /**
556
797
  * Request the embedded FeedbackModal to show the login screen. Works by
557
798
  * emitting an event the modal listens for — avoids forcing the host app
@@ -571,6 +812,25 @@ class YaverFeedback {
571
812
  const { DeviceEventEmitter } = require('react-native');
572
813
  DeviceEventEmitter.emit('yaverFeedback:startMachinePicker');
573
814
  }
815
+ /** Begin the reusable host-app Dogfood wizard. Owner OAuth and machine
816
+ * selection intentionally happen before installation enrollment/runtime
817
+ * controls because starting builds or runners is owner-level authority. */
818
+ static beginDogfoodOnboarding(options) {
819
+ dogfoodOnboarding = options;
820
+ if (!config)
821
+ YaverFeedback.init({ autoLogin: true });
822
+ if (!YaverFeedback.isAuthed()) {
823
+ YaverFeedback.showLogin();
824
+ return;
825
+ }
826
+ YaverFeedback.showMachinePicker();
827
+ }
828
+ static getDogfoodOnboarding() {
829
+ return dogfoodOnboarding;
830
+ }
831
+ static clearDogfoodOnboarding() {
832
+ dogfoodOnboarding = null;
833
+ }
574
834
  /**
575
835
  * Update the selected remote device. Resets the cached agent URL so the
576
836
  * next `startReport()` (or FloatingButton press) rediscovers against the
@@ -579,9 +839,19 @@ class YaverFeedback {
579
839
  static async setPreferredDevice(deviceId) {
580
840
  if (!config)
581
841
  return;
842
+ const wasUnified = !config.renderDeviceId
843
+ || config.renderDeviceId === config.codingDeviceId
844
+ || config.renderDeviceId === config.preferredDeviceId;
582
845
  config.preferredDeviceId = deviceId;
846
+ config.codingDeviceId = deviceId;
847
+ if (wasUnified)
848
+ config.renderDeviceId = deviceId;
583
849
  config.agentUrl = undefined;
850
+ config.codingAgentUrl = undefined;
851
+ if (wasUnified)
852
+ config.renderAgentUrl = undefined;
584
853
  p2pClient = null;
854
+ renderP2PClient = null;
585
855
  p2pAuthToken = null;
586
856
  await YaverFeedback.discoverAgent();
587
857
  }
@@ -591,8 +861,7 @@ class YaverFeedback {
591
861
  return null;
592
862
  const preferredDeviceId = config.preferredDeviceId;
593
863
  const devices = await (0, auth_1.listReachableDevices)(config.authToken);
594
- const all = [...devices.owned, ...devices.shared];
595
- return all.find((device) => device.deviceId === preferredDeviceId) ?? null;
864
+ return devices.owned.find((device) => device.deviceId === preferredDeviceId) ?? null;
596
865
  }
597
866
  /**
598
867
  * Trigger remote device-auth for a CLI runner on the selected agent
@@ -642,6 +911,7 @@ class YaverFeedback {
642
911
  config.agentUrl = undefined;
643
912
  }
644
913
  p2pClient = null;
914
+ renderP2PClient = null;
645
915
  p2pAuthToken = null;
646
916
  }
647
917
  /**
@@ -747,32 +1017,138 @@ class YaverFeedback {
747
1017
  }
748
1018
  BlackBox_1.BlackBox.unwrapConsole(); // ensure console is restored even if BlackBox wasn't started
749
1019
  errorBuffer = [];
750
- if (shakeDetector) {
751
- shakeDetector.stop();
752
- shakeDetector = null;
753
- }
1020
+ YaverFeedback.stopShakeDetector();
754
1021
  }
755
1022
  else {
756
1023
  // === ENABLE ===
757
1024
  if (blackBoxWasStreaming) {
758
1025
  BlackBox_1.BlackBox.start(); // restart with previous config
759
1026
  }
760
- // Restart shake detector if trigger is 'shake'
761
- if (config?.trigger === 'shake' && !config?.disableShakeGesture && !shakeDetector) {
762
- shakeDetector = new ShakeDetector_1.ShakeDetector();
763
- shakeDetector.start(() => {
764
- YaverFeedback.notifyShake();
765
- if (config?.reportingOnly) {
766
- YaverFeedback.sendAutoReport();
767
- }
768
- else {
769
- YaverFeedback.startReport();
770
- }
771
- });
772
- }
1027
+ enabled = true; // syncShakeDetector reads this
1028
+ YaverFeedback.syncShakeDetector();
773
1029
  }
774
1030
  enabled = value;
775
1031
  }
1032
+ /**
1033
+ * Turn shake-to-report on or off without tearing the SDK down.
1034
+ *
1035
+ * `trigger` is read only inside init(), so before this the only ways to
1036
+ * stop listening for a shake were setEnabled(false) — which also kills the
1037
+ * flight recorder and the agent command channel — or a re-init, which used
1038
+ * to stack a duplicate command handler. Neither is what "turn the shake
1039
+ * catcher off" should cost.
1040
+ *
1041
+ * Persists onto config.disableShakeGesture, so a later setEnabled(true)
1042
+ * honours it rather than resurrecting the listener.
1043
+ */
1044
+ static setShakeEnabled(value) {
1045
+ if (!config)
1046
+ return;
1047
+ config.disableShakeGesture = !value;
1048
+ YaverFeedback.syncShakeDetector();
1049
+ }
1050
+ /** Whether the shake listener is currently armed. */
1051
+ static isShakeEnabled() {
1052
+ return shakeDetector !== null;
1053
+ }
1054
+ /**
1055
+ * Bring the shake listener in line with the current config. Idempotent —
1056
+ * safe to call whenever `enabled`, `trigger`, or `disableShakeGesture`
1057
+ * moves.
1058
+ */
1059
+ static syncShakeDetector() {
1060
+ const want = enabled && config?.trigger === 'shake' && !config?.disableShakeGesture;
1061
+ if (want && !shakeDetector) {
1062
+ shakeDetector = new ShakeDetector_1.ShakeDetector();
1063
+ shakeDetector.start(() => {
1064
+ YaverFeedback.notifyShake();
1065
+ if (config?.reportingOnly) {
1066
+ YaverFeedback.sendAutoReport();
1067
+ }
1068
+ else {
1069
+ YaverFeedback.startReport();
1070
+ }
1071
+ });
1072
+ }
1073
+ else if (!want && shakeDetector) {
1074
+ YaverFeedback.stopShakeDetector();
1075
+ }
1076
+ }
1077
+ static stopShakeDetector() {
1078
+ if (shakeDetector) {
1079
+ shakeDetector.stop();
1080
+ shakeDetector = null;
1081
+ }
1082
+ }
1083
+ /** Cancel a pending BlackBox auto-start retry chain. */
1084
+ static cancelBlackBoxAutoStart() {
1085
+ if (autoStartTimer) {
1086
+ clearTimeout(autoStartTimer);
1087
+ autoStartTimer = null;
1088
+ }
1089
+ }
1090
+ /**
1091
+ * Start BlackBox as soon as we have BOTH an agentUrl and a token, polling
1092
+ * until then.
1093
+ *
1094
+ * Both conditions are mandatory and deliberate: starting without a token
1095
+ * makes the SSE channel 401 and retry with backoff, which is the tight
1096
+ * string-concat + JSON-parse loop that used to SIGSEGV Hermes on iOS 18.3.1
1097
+ * during Screenshot & Fix. This only re-checks that same guard over time.
1098
+ *
1099
+ * Bounded at ~60s: long enough for AsyncStorage hydration plus a Convex
1100
+ * discovery round trip on a cold, off-LAN start, short enough that a device
1101
+ * whose user never signs in stops polling. Giving up here costs nothing —
1102
+ * setAuthToken() and startReport() both reschedule, so signing in later
1103
+ * still brings the channel up.
1104
+ */
1105
+ static scheduleBlackBoxAutoStart(attempt = 0) {
1106
+ const MAX_ATTEMPTS = 60;
1107
+ const FIRST_DELAY_MS = 500;
1108
+ const RETRY_DELAY_MS = 1000;
1109
+ // Re-run discovery every Nth tick rather than every tick: it is a Convex
1110
+ // round trip plus LAN probing, so once a second would hammer both. Every
1111
+ // 3s is responsive enough that waking the dev machine reconnects the phone
1112
+ // on its own, without the user wondering whether to restart the app.
1113
+ const REDISCOVER_EVERY = 3;
1114
+ YaverFeedback.cancelBlackBoxAutoStart();
1115
+ if (!enabled || attempt >= MAX_ATTEMPTS)
1116
+ return;
1117
+ const timer = setTimeout(() => {
1118
+ autoStartTimer = null;
1119
+ // A destroy() or setEnabled(false) between scheduling and firing.
1120
+ if (!enabled || !config)
1121
+ return;
1122
+ if (BlackBox_1.BlackBox.isStreaming)
1123
+ return;
1124
+ if (config.agentUrl && (config.authToken || p2pAuthToken)) {
1125
+ try {
1126
+ // Pass the host's BlackBox config through. This used to call
1127
+ // start() bare, so a host that configured the flight recorder
1128
+ // — as the README itself shows — had it silently replaced by
1129
+ // defaults, and appName came out ''.
1130
+ BlackBox_1.BlackBox.start(config.blackBox);
1131
+ }
1132
+ catch (err) {
1133
+ console.warn('[YaverFeedback] BlackBox auto-start failed:', err);
1134
+ }
1135
+ return;
1136
+ }
1137
+ // No agent yet. Re-attempt discovery instead of only re-reading the
1138
+ // flag: hydrateSession() runs discoverAgent() exactly once at init, so
1139
+ // if the dev machine was unreachable at that moment — asleep, phone
1140
+ // still on cellular before the relay came up, agent not yet serving —
1141
+ // agentUrl stayed null for the whole process lifetime and only an app
1142
+ // restart could recover it. discoverAgent() self-guards on "already
1143
+ // have a URL" and "no token", so calling it again is cheap and safe.
1144
+ if (!config.agentUrl && attempt > 0 && attempt % REDISCOVER_EVERY === 0) {
1145
+ void YaverFeedback.discoverAgent();
1146
+ }
1147
+ YaverFeedback.scheduleBlackBoxAutoStart(attempt + 1);
1148
+ }, attempt === 0 ? FIRST_DELAY_MS : RETRY_DELAY_MS);
1149
+ autoStartTimer = timer;
1150
+ unrefTimer(timer);
1151
+ }
776
1152
  /** Returns whether the SDK is currently enabled. */
777
1153
  static isEnabled() {
778
1154
  return enabled;
@@ -781,6 +1157,61 @@ class YaverFeedback {
781
1157
  static getConfig() {
782
1158
  return config;
783
1159
  }
1160
+ /** Current third-party SDK mode. Fails closed unless enabled + account match. */
1161
+ static getDogfoodStatus() {
1162
+ return (0, dogfoodPolicy_1.resolveSDKDogfood)(config?.dogfood);
1163
+ }
1164
+ /** One-call, account-free Dogfood bootstrap for third-party apps. On first
1165
+ * launch it creates/proves the installation key and returns pending; after
1166
+ * owner approval the same call obtains a short-lived scoped Yaver session
1167
+ * and enables Dogfood UX without the host app implementing OAuth. */
1168
+ static async enableDeviceDogfood(options) {
1169
+ const client = new deviceDogfood_1.YaverDeviceDogfood(options);
1170
+ let status = await client.status();
1171
+ if (status === 'unregistered' || status === 'pending' || status === 'cancelled' || status === 'revoked' || status === 'superseded') {
1172
+ const enrolled = status === 'unregistered' || status === 'pending'
1173
+ ? await client.enroll(react_native_1.Platform.OS)
1174
+ : await client.reRegister(react_native_1.Platform.OS);
1175
+ status = enrolled.status;
1176
+ }
1177
+ const info = await client.enrollmentInfo();
1178
+ const session = status === 'active' ? await client.session() : null;
1179
+ if (!config)
1180
+ YaverFeedback.init({ autoLogin: false });
1181
+ if (config) {
1182
+ config.dogfood = {
1183
+ enabled: status === 'active' && !!session,
1184
+ appId: options.appId,
1185
+ installationId: info.installationId,
1186
+ installationStatus: status === 'active' && session ? 'active' : status === 'unregistered' ? 'pending' : status,
1187
+ label: options.label,
1188
+ };
1189
+ // A normal Yaver OAuth session remains authoritative for the runtime
1190
+ // wizard. Only account-free hosts adopt the narrow installation token.
1191
+ if (session && !config.authToken)
1192
+ await YaverFeedback.setAuthToken(session.token);
1193
+ }
1194
+ try {
1195
+ const { DeviceEventEmitter } = require('react-native');
1196
+ DeviceEventEmitter.emit('yaverFeedback:dogfoodChanged', { active: !!session, status });
1197
+ }
1198
+ catch { /* noop */ }
1199
+ return { status, installationId: info.installationId, session };
1200
+ }
1201
+ /** Confirmed by the Y badge/UI before this is called. Reverts this SDK to
1202
+ * normal Feedback mode for the remainder of the app run. */
1203
+ static async exitDogfoodMode() {
1204
+ const cfg = config?.dogfood;
1205
+ if (!cfg)
1206
+ return;
1207
+ cfg.enabled = false;
1208
+ await cfg.onExit?.();
1209
+ try {
1210
+ const { DeviceEventEmitter } = require('react-native');
1211
+ DeviceEventEmitter.emit('yaverFeedback:dogfoodChanged', { active: false, exited: true });
1212
+ }
1213
+ catch { /* noop */ }
1214
+ }
784
1215
  /** Returns the resolved relay password the SDK is currently using.
785
1216
  * Empty string when no relay routing is in play (direct LAN agent
786
1217
  * URLs need no password). Callers attaching it to relay-routed
@@ -843,6 +1274,104 @@ class YaverFeedback {
843
1274
  next?.(error, isFatal);
844
1275
  };
845
1276
  }
1277
+ /**
1278
+ * Upload a crash-aware feedback bundle. Apps can call this from their own
1279
+ * error boundary/global handler. If crashReporting.autoFix is true, the SDK
1280
+ * also triggers the agent's feedback-fix task; reload delivery still happens
1281
+ * through the existing BlackBox command stream.
1282
+ */
1283
+ static async reportCrash(error, opts) {
1284
+ if (!config || !enabled || !config.crashReporting?.enabled)
1285
+ return {};
1286
+ if (crashReportInFlight)
1287
+ return {};
1288
+ crashReportInFlight = true;
1289
+ try {
1290
+ YaverFeedback.attachError(error, {
1291
+ ...(config.crashReporting.metadata || {}),
1292
+ ...(opts?.metadata || {}),
1293
+ crashAware: true,
1294
+ });
1295
+ if (errorBuffer.length > 0) {
1296
+ errorBuffer[errorBuffer.length - 1].isFatal = opts?.isFatal ?? true;
1297
+ }
1298
+ if (!config.agentUrl) {
1299
+ await YaverFeedback.discoverAgent();
1300
+ }
1301
+ if (!config.agentUrl) {
1302
+ console.warn('[YaverFeedback] No agent URL — cannot send crash report.');
1303
+ return {};
1304
+ }
1305
+ const { Platform, Dimensions } = require('react-native');
1306
+ const { uploadFeedback } = require('./upload');
1307
+ const { width, height } = Dimensions.get('window');
1308
+ let screenshotPath;
1309
+ if (config.crashReporting.captureScreenshot !== false) {
1310
+ try {
1311
+ const { captureScreenshot } = require('./capture');
1312
+ screenshotPath = await captureScreenshot();
1313
+ }
1314
+ catch { }
1315
+ }
1316
+ const autoFix = opts?.autoFix ?? config.crashReporting.autoFix === true;
1317
+ const identity = (0, P2PClient_1.resolveReportIdentity)({
1318
+ projectName: config?.projectName,
1319
+ bundleId: config?.bundleId,
1320
+ surface: config?.surface,
1321
+ surfaces: config?.surfaces,
1322
+ stack: config?.stack,
1323
+ stacks: config?.stacks,
1324
+ testSurfaces: config?.testSurfaces,
1325
+ feedbackSdk: config?.feedbackSdk,
1326
+ feedbackTransport: config?.feedbackTransport,
1327
+ voiceCapabilities: config?.voiceCapabilities,
1328
+ sttProvider: config?.sttProvider,
1329
+ ttsProvider: config?.ttsProvider,
1330
+ });
1331
+ const bundle = {
1332
+ metadata: {
1333
+ timestamp: new Date().toISOString(),
1334
+ reportKind: 'crash',
1335
+ deviceInfo: {
1336
+ platform: Platform.OS,
1337
+ osVersion: String(Platform.Version),
1338
+ model: Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
1339
+ screenWidth: width,
1340
+ screenHeight: height,
1341
+ appName: identity.appName,
1342
+ },
1343
+ app: identity.app,
1344
+ project: identity.project,
1345
+ userNote: '[Crash report]',
1346
+ crash: {
1347
+ message: error.message,
1348
+ isFatal: opts?.isFatal ?? true,
1349
+ source: opts?.source ?? 'manual',
1350
+ autoFixRequested: autoFix,
1351
+ },
1352
+ },
1353
+ screenshots: screenshotPath ? [screenshotPath] : [],
1354
+ errors: errorBuffer.length > 0 ? [...errorBuffer] : undefined,
1355
+ };
1356
+ const uploaded = await uploadFeedback(config.agentUrl, config.authToken ?? '', bundle, p2pRelayPassword);
1357
+ const reportId = uploaded?.id ??
1358
+ uploaded?.reportId;
1359
+ let taskId;
1360
+ if (autoFix && reportId) {
1361
+ const client = p2pClient ?? new P2PClient_1.P2PClient(config.agentUrl, config.authToken ?? '', p2pRelayPassword);
1362
+ const fix = await client.triggerFix(reportId);
1363
+ taskId = fix?.taskId;
1364
+ }
1365
+ return { reportId, taskId };
1366
+ }
1367
+ catch (err) {
1368
+ console.warn('[YaverFeedback] Crash report failed:', err);
1369
+ return {};
1370
+ }
1371
+ finally {
1372
+ crashReportInFlight = false;
1373
+ }
1374
+ }
846
1375
  /**
847
1376
  * Returns the P2P client instance.
848
1377
  * Available after init if agentUrl is set, or after first successful discovery.
@@ -850,6 +1379,16 @@ class YaverFeedback {
850
1379
  static getP2PClient() {
851
1380
  return p2pClient;
852
1381
  }
1382
+ /** Renderer/reload route; intentionally distinct from the coding client. */
1383
+ static getRenderP2PClient() {
1384
+ return renderP2PClient ?? p2pClient;
1385
+ }
1386
+ static getMachineRouting() {
1387
+ return {
1388
+ codingDeviceId: config?.codingDeviceId || config?.preferredDeviceId,
1389
+ renderDeviceId: config?.renderDeviceId || config?.codingDeviceId || config?.preferredDeviceId,
1390
+ };
1391
+ }
853
1392
  // ─── One-stop SaaS replacement methods ─────────────────────────
854
1393
  //
855
1394
  // These are the three solo-dev SaaS-replacement entry points
@@ -960,6 +1499,7 @@ class YaverFeedback {
960
1499
  config.agentUrl = result.url;
961
1500
  const rp = await resolveRelayPassword(config.authToken ?? '');
962
1501
  p2pClient = new P2PClient_1.P2PClient(result.url, config.authToken ?? '', rp);
1502
+ renderP2PClient = p2pClient;
963
1503
  }
964
1504
  }
965
1505
  catch { }
@@ -981,17 +1521,33 @@ class YaverFeedback {
981
1521
  catch {
982
1522
  // Screenshot capture may fail (e.g. no view ref) — continue without it
983
1523
  }
1524
+ const identity = (0, P2PClient_1.resolveReportIdentity)({
1525
+ projectName: config?.projectName,
1526
+ bundleId: config?.bundleId,
1527
+ surface: config?.surface,
1528
+ surfaces: config?.surfaces,
1529
+ stack: config?.stack,
1530
+ stacks: config?.stacks,
1531
+ testSurfaces: config?.testSurfaces,
1532
+ feedbackSdk: config?.feedbackSdk,
1533
+ feedbackTransport: config?.feedbackTransport,
1534
+ voiceCapabilities: config?.voiceCapabilities,
1535
+ sttProvider: config?.sttProvider,
1536
+ ttsProvider: config?.ttsProvider,
1537
+ });
984
1538
  const bundle = {
985
1539
  metadata: {
986
1540
  timestamp: new Date().toISOString(),
987
- device: {
1541
+ deviceInfo: {
988
1542
  platform: Platform.OS,
989
1543
  osVersion: String(Platform.Version),
990
1544
  model: Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
991
1545
  screenWidth: width,
992
1546
  screenHeight: height,
1547
+ appName: identity.appName,
993
1548
  },
994
- app: {},
1549
+ app: identity.app,
1550
+ project: identity.project,
995
1551
  userNote: '[Auto-report via shake]',
996
1552
  },
997
1553
  screenshots: screenshotPath ? [screenshotPath] : [],
@@ -1143,10 +1699,22 @@ class YaverFeedback {
1143
1699
  shakeDetector.stop();
1144
1700
  shakeDetector = null;
1145
1701
  }
1702
+ // Drop the agent command handler and un-monkey-patch console. Without
1703
+ // these, destroy() left the SDK half-alive: console stayed wrapped, and
1704
+ // the reload handler kept firing on a "destroyed" SDK — then a later
1705
+ // init() stacked a second one on top.
1706
+ commandUnsubscribe?.();
1707
+ commandUnsubscribe = null;
1708
+ // Before `enabled = false` / `config = null` below, so an in-flight retry
1709
+ // can't fire against a torn-down config.
1710
+ YaverFeedback.cancelBlackBoxAutoStart();
1711
+ BlackBox_1.BlackBox.stop();
1712
+ BlackBox_1.BlackBox.unwrapConsole();
1146
1713
  firstShakeFired = false;
1147
1714
  enabled = false;
1148
1715
  config = null;
1149
1716
  p2pClient = null;
1717
+ renderP2PClient = null;
1150
1718
  errorBuffer = [];
1151
1719
  }
1152
1720
  }