yaver-feedback-react-native 0.8.13 → 0.9.2

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 (76) hide show
  1. package/app.plugin.js +126 -9
  2. package/dist/BlackBox.js +9 -1
  3. package/dist/DeployPanel.js +65 -0
  4. package/dist/Discovery.js +13 -2
  5. package/dist/FeedbackModal.js +477 -66
  6. package/dist/MachinePickerScreen.js +14 -5
  7. package/dist/P2PClient.d.ts +94 -1
  8. package/dist/P2PClient.js +281 -2
  9. package/dist/ShakeDetector.js +2 -0
  10. package/dist/VibeChatScreen.d.ts +6 -1
  11. package/dist/VibeChatScreen.js +204 -1
  12. package/dist/YaverFeedback.d.ts +98 -0
  13. package/dist/YaverFeedback.js +509 -46
  14. package/dist/__tests__/BlackBox.relayPassword.test.d.ts +1 -0
  15. package/dist/__tests__/BlackBox.relayPassword.test.js +105 -0
  16. package/dist/__tests__/BlackBoxAutoStart.test.d.ts +1 -0
  17. package/dist/__tests__/BlackBoxAutoStart.test.js +91 -0
  18. package/dist/__tests__/BlackBoxAutoStartColdStart.test.d.ts +1 -0
  19. package/dist/__tests__/BlackBoxAutoStartColdStart.test.js +156 -0
  20. package/dist/__tests__/BrowserLaneIcon.test.d.ts +3 -0
  21. package/dist/__tests__/BrowserLaneIcon.test.js +80 -0
  22. package/dist/__tests__/P2PClient.test.js +2 -2
  23. package/dist/__tests__/ReportIdentity.test.d.ts +1 -0
  24. package/dist/__tests__/ReportIdentity.test.js +168 -0
  25. package/dist/__tests__/SDKToken.test.js +1 -1
  26. package/dist/__tests__/ShakeToggle.test.d.ts +1 -0
  27. package/dist/__tests__/ShakeToggle.test.js +121 -0
  28. package/dist/__tests__/pickTargetDevice.test.d.ts +1 -0
  29. package/dist/__tests__/pickTargetDevice.test.js +89 -0
  30. package/dist/__tests__/reloadActions.test.d.ts +1 -0
  31. package/dist/__tests__/reloadActions.test.js +129 -0
  32. package/dist/__tests__/reloadActionsParity.test.d.ts +1 -0
  33. package/dist/__tests__/reloadActionsParity.test.js +42 -0
  34. package/dist/__tests__/types.test.js +5 -5
  35. package/dist/_core/device.d.ts +19 -9
  36. package/dist/_core/device.js +20 -14
  37. package/dist/capture.d.ts +6 -0
  38. package/dist/capture.js +53 -0
  39. package/dist/index.d.ts +4 -0
  40. package/dist/index.js +11 -1
  41. package/dist/reloadActions.d.ts +88 -0
  42. package/dist/reloadActions.js +200 -0
  43. package/dist/storeShots.d.ts +67 -0
  44. package/dist/storeShots.js +137 -0
  45. package/dist/types.d.ts +215 -3
  46. package/dist/voice.d.ts +61 -0
  47. package/dist/voice.js +246 -0
  48. package/package.json +14 -3
  49. package/src/BlackBox.ts +10 -1
  50. package/src/DeployPanel.tsx +74 -0
  51. package/src/Discovery.ts +13 -2
  52. package/src/FeedbackModal.tsx +563 -87
  53. package/src/MachinePickerScreen.tsx +12 -3
  54. package/src/P2PClient.ts +314 -2
  55. package/src/ShakeDetector.ts +1 -0
  56. package/src/VibeChatScreen.tsx +219 -0
  57. package/src/YaverFeedback.ts +498 -46
  58. package/src/__tests__/BlackBox.relayPassword.test.ts +129 -0
  59. package/src/__tests__/BlackBoxAutoStart.test.ts +111 -0
  60. package/src/__tests__/BlackBoxAutoStartColdStart.test.ts +191 -0
  61. package/src/__tests__/BrowserLaneIcon.test.ts +85 -0
  62. package/src/__tests__/P2PClient.test.ts +2 -2
  63. package/src/__tests__/ReportIdentity.test.ts +203 -0
  64. package/src/__tests__/SDKToken.test.ts +1 -1
  65. package/src/__tests__/ShakeToggle.test.ts +153 -0
  66. package/src/__tests__/pickTargetDevice.test.ts +101 -0
  67. package/src/__tests__/reloadActions.test.ts +171 -0
  68. package/src/__tests__/reloadActionsParity.test.ts +49 -0
  69. package/src/__tests__/types.test.ts +5 -5
  70. package/src/_core/device.ts +20 -14
  71. package/src/capture.ts +51 -0
  72. package/src/index.ts +21 -0
  73. package/src/reloadActions.ts +273 -0
  74. package/src/storeShots.ts +189 -0
  75. package/src/types.ts +217 -3
  76. package/src/voice.ts +270 -0
@@ -53,7 +53,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
53
53
  exports.VibeChatScreen = VibeChatScreen;
54
54
  const react_1 = __importStar(require("react"));
55
55
  const react_native_1 = require("react-native");
56
- function VibeChatScreen({ client, initialTaskId, initialUserPrompt, onClose, onReload, }) {
56
+ const voice_1 = require("./voice");
57
+ const capture_1 = require("./capture");
58
+ function VibeChatScreen({ client, initialTaskId, initialUserPrompt, onClose, onReload, project, model, runner, }) {
57
59
  const [taskId, setTaskId] = (0, react_1.useState)(initialTaskId);
58
60
  const [turns, setTurns] = (0, react_1.useState)(() => [
59
61
  {
@@ -76,6 +78,16 @@ function VibeChatScreen({ client, initialTaskId, initialUserPrompt, onClose, onR
76
78
  const [isReloading, setIsReloading] = (0, react_1.useState)(false);
77
79
  const scrollRef = (0, react_1.useRef)(null);
78
80
  const abortRef = (0, react_1.useRef)(null);
81
+ // ── Voice vibe coding ──────────────────────────────────────────────
82
+ const [voiceState, setVoiceState] = (0, react_1.useState)('idle');
83
+ const [voiceAvailable, setVoiceAvailable] = (0, react_1.useState)(false);
84
+ // "local" = whisper.cpp on the host (free, private); "flux" = Deepgram
85
+ // nova-3 streaming. fluxAvailable gates the toggle on whether the agent
86
+ // has a Deepgram key. activeEngine is echoed back by the agent.
87
+ const [voiceMode, setVoiceMode] = (0, react_1.useState)('local');
88
+ const [fluxAvailable, setFluxAvailable] = (0, react_1.useState)(false);
89
+ const [activeEngine, setActiveEngine] = (0, react_1.useState)('');
90
+ const voiceSessionRef = (0, react_1.useRef)(null);
79
91
  // Subscribe to the current task's SSE stream. Re-runs whenever the
80
92
  // taskId changes (resumeTask reuses the same id, so this only fires
81
93
  // once per task — which is fine).
@@ -195,6 +207,170 @@ function VibeChatScreen({ client, initialTaskId, initialUserPrompt, onClose, onR
195
207
  setIsReloading(false);
196
208
  }
197
209
  }, [isReloading, onReload]);
210
+ // Probe whether voice is usable: deps present (expo-av + expo-file-
211
+ // system + buffer) AND the agent reports STT/TTS ready. Hide the mic
212
+ // entirely otherwise so users never tap a dead button.
213
+ (0, react_1.useEffect)(() => {
214
+ let cancelled = false;
215
+ (async () => {
216
+ if (!(0, capture_1.isVoiceCaptureSupported)() || !(0, voice_1.isVoiceStreamSupported)())
217
+ return;
218
+ try {
219
+ const res = await fetch(`${client.agentBaseUrl}/voice/status`, { headers: client.voiceAuthHeaders() });
220
+ if (!res.ok)
221
+ return;
222
+ const body = await res.json();
223
+ if (cancelled)
224
+ return;
225
+ // Local whisper is always usable when voice is enabled; Flux needs
226
+ // a Deepgram key on the agent. Show the mic if either path works.
227
+ const localOk = !!body?.enabled;
228
+ const fluxOk = !!body?.enabled && !!body?.deepgramSet;
229
+ if (localOk || fluxOk)
230
+ setVoiceAvailable(true);
231
+ setFluxAvailable(fluxOk);
232
+ if (!localOk && fluxOk)
233
+ setVoiceMode('flux');
234
+ }
235
+ catch { /* leave hidden */ }
236
+ })();
237
+ return () => { cancelled = true; };
238
+ }, [client]);
239
+ (0, react_1.useEffect)(() => () => { voiceSessionRef.current?.close(); }, []);
240
+ // Local TTS: the agent streams no audio for "local"/"device" engines,
241
+ // so the client speaks the result text with the device synthesizer.
242
+ // expo-speech is optional — if absent, the text is still shown.
243
+ const speakLocalText = (0, react_1.useCallback)((text) => {
244
+ if (!text)
245
+ return;
246
+ try {
247
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
248
+ const Speech = require('expo-speech');
249
+ const headline = text.length > 280 ? `${text.slice(0, 280)} — see screen for the rest.` : text;
250
+ Speech.stop?.();
251
+ Speech.speak?.(headline);
252
+ }
253
+ catch { /* expo-speech not installed — text remains visible */ }
254
+ }, []);
255
+ const playTTS = (0, react_1.useCallback)(async (pcm, sampleRate) => {
256
+ try {
257
+ const wavUri = await (0, voice_1.pcmToTempWavURI)(pcm, sampleRate);
258
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
259
+ const { Audio } = require('expo-av');
260
+ const { sound } = await Audio.Sound.createAsync({ uri: wavUri }, { shouldPlay: true });
261
+ sound.setOnPlaybackStatusUpdate((st) => {
262
+ if (st.didJustFinish)
263
+ sound.unloadAsync().catch(() => { });
264
+ });
265
+ }
266
+ catch { /* playback best-effort */ }
267
+ }, []);
268
+ const stopVoiceAndProcess = (0, react_1.useCallback)(async () => {
269
+ setVoiceState('uploading');
270
+ let uri = null;
271
+ try {
272
+ uri = await (0, capture_1.stopPcmRecording)();
273
+ }
274
+ catch (e) {
275
+ setVoiceState('idle');
276
+ setTurns((prev) => [...prev, { id: `status-verr-${Date.now()}`, role: 'status', text: `voice: ${e instanceof Error ? e.message : String(e)}`, timestamp: Date.now() }]);
277
+ return;
278
+ }
279
+ if (!uri) {
280
+ setVoiceState('idle');
281
+ return;
282
+ }
283
+ const useFlux = voiceMode === 'flux' && fluxAvailable;
284
+ const session = new voice_1.SDKVoiceSession({
285
+ onProviders: (stt, tts) => setActiveEngine(stt === 'deepgram' ? 'Flux (Deepgram)' : stt === 'local' ? 'Local (whisper)' : stt),
286
+ onTranscriptPartial: (t) => {
287
+ setTurns((prev) => {
288
+ const next = prev.filter((x) => x.id !== 'voice-partial');
289
+ next.push({ id: 'voice-partial', role: 'status', text: `🎙 ${t}`, timestamp: Date.now() });
290
+ return next;
291
+ });
292
+ },
293
+ onTranscriptFinal: (t) => {
294
+ setVoiceState('thinking');
295
+ setTurns((prev) => [
296
+ ...prev.filter((x) => x.id !== 'voice-partial'),
297
+ { id: `user-voice-${Date.now()}`, role: 'user', text: t, timestamp: Date.now() },
298
+ { id: `status-${Date.now()}`, role: 'status', text: 'thinking…', timestamp: Date.now() },
299
+ ]);
300
+ },
301
+ onTaskCreated: (id) => {
302
+ // Hand the chat's SSE subscription the new task so its agent
303
+ // output streams into the transcript exactly like a typed turn.
304
+ if (id) {
305
+ setStatus('running');
306
+ setStreamBuffer('');
307
+ setTaskId(id);
308
+ }
309
+ },
310
+ onTaskResult: (_id, text) => {
311
+ setVoiceState('speaking');
312
+ // Local TTS path: agent sends no audio frames, so speak here.
313
+ if (!useFlux)
314
+ speakLocalText(text);
315
+ },
316
+ onTTSReady: (pcm, sr) => { void playTTS(pcm, sr); },
317
+ onDone: () => setTimeout(() => setVoiceState('idle'), 1200),
318
+ onError: (msg) => {
319
+ setVoiceState('idle');
320
+ setTurns((prev) => [...prev.filter((x) => x.id !== 'voice-partial'), { id: `status-verr-${Date.now()}`, role: 'status', text: `voice: ${msg}`, timestamp: Date.now() }]);
321
+ },
322
+ });
323
+ voiceSessionRef.current = session;
324
+ try {
325
+ await session.start({
326
+ wsUrl: client.voiceStreamUrl(),
327
+ headers: client.voiceAuthHeaders(),
328
+ project,
329
+ model,
330
+ runner,
331
+ surface: 'feedback-sdk',
332
+ ttsBudget: 280,
333
+ // Local: whisper.cpp on the host + device synth. Flux: Deepgram
334
+ // nova-3 STT + Aura TTS streamed back as PCM.
335
+ sttProvider: useFlux ? 'deepgram' : 'local',
336
+ ttsProvider: useFlux ? 'deepgram' : 'local',
337
+ });
338
+ await session.streamAudioFile(uri, { skipWavHeader: true });
339
+ session.finalize();
340
+ }
341
+ catch (e) {
342
+ setVoiceState('idle');
343
+ session.close();
344
+ setTurns((prev) => [...prev, { id: `status-verr-${Date.now()}`, role: 'status', text: `voice: ${e instanceof Error ? e.message : String(e)}`, timestamp: Date.now() }]);
345
+ }
346
+ }, [client, project, model, runner, playTTS, voiceMode, fluxAvailable, speakLocalText]);
347
+ const handleVoicePress = (0, react_1.useCallback)(async () => {
348
+ if (voiceState === 'recording') {
349
+ void stopVoiceAndProcess();
350
+ return;
351
+ }
352
+ if (voiceState !== 'idle') {
353
+ // Mid-flow tap cancels.
354
+ voiceSessionRef.current?.close();
355
+ voiceSessionRef.current = null;
356
+ setVoiceState('idle');
357
+ return;
358
+ }
359
+ try {
360
+ await (0, capture_1.startPcmRecording)();
361
+ setVoiceState('recording');
362
+ }
363
+ catch (e) {
364
+ setTurns((prev) => [...prev, { id: `status-verr-${Date.now()}`, role: 'status', text: `voice: ${e instanceof Error ? e.message : String(e)}`, timestamp: Date.now() }]);
365
+ }
366
+ }, [voiceState, stopVoiceAndProcess]);
367
+ const voiceLabel = {
368
+ idle: '🎙 speak',
369
+ recording: '■ stop',
370
+ uploading: 'sending…',
371
+ thinking: 'thinking…',
372
+ speaking: 'speaking…',
373
+ };
198
374
  return (<react_native_1.View style={styles.container}>
199
375
  <react_native_1.View style={styles.header}>
200
376
  <react_native_1.Text style={styles.title}>Vibe</react_native_1.Text>
@@ -226,8 +402,30 @@ function VibeChatScreen({ client, initialTaskId, initialUserPrompt, onClose, onR
226
402
  </react_native_1.ScrollView>
227
403
 
228
404
  <react_native_1.View style={styles.footer}>
405
+ {voiceAvailable && voiceState !== 'idle' && (<react_native_1.Text style={styles.engineCaption}>
406
+ {voiceState === 'recording' ? 'listening' : voiceState === 'uploading' ? 'sending' : voiceState === 'thinking' ? 'agent working' : 'speaking'}
407
+ {activeEngine ? ` · ${activeEngine}` : ` · ${voiceMode === 'flux' ? 'Flux (Deepgram)' : 'Local (whisper)'}`}
408
+ </react_native_1.Text>)}
229
409
  <react_native_1.TextInput style={styles.input} value={followUp} onChangeText={setFollowUp} placeholder={status === 'running' ? 'wait for the agent…' : 'follow up…'} placeholderTextColor="#666" editable={status !== 'running' && !isResuming} multiline/>
230
410
  <react_native_1.View style={styles.actions}>
411
+ {voiceAvailable && (<>
412
+ {/* Local ↔ Flux engine toggle. Only shows Flux when the
413
+ agent has a Deepgram key; otherwise the label just
414
+ states "Local" so the active engine is always clear. */}
415
+ {fluxAvailable ? (<react_native_1.TouchableOpacity style={[styles.actionBtn, styles.engineToggle]} onPress={() => setVoiceMode((m) => (m === 'local' ? 'flux' : 'local'))} disabled={voiceState !== 'idle'} accessibilityLabel="Toggle voice engine">
416
+ <react_native_1.Text style={styles.engineToggleText}>{voiceMode === 'flux' ? '⚡ Flux' : '🔒 Local'}</react_native_1.Text>
417
+ </react_native_1.TouchableOpacity>) : (<react_native_1.View style={[styles.actionBtn, styles.engineToggle]}>
418
+ <react_native_1.Text style={styles.engineToggleText}>🔒 Local</react_native_1.Text>
419
+ </react_native_1.View>)}
420
+ <react_native_1.TouchableOpacity style={[
421
+ styles.actionBtn,
422
+ styles.voiceBtn,
423
+ voiceState === 'recording' && styles.voiceBtnActive,
424
+ (voiceState === 'uploading' || voiceState === 'thinking' || voiceState === 'speaking') && styles.actionBtnDisabled,
425
+ ]} onPress={handleVoicePress} disabled={voiceState === 'uploading' || voiceState === 'thinking' || voiceState === 'speaking'} accessibilityLabel="Vibe code by voice">
426
+ <react_native_1.Text style={styles.actionText}>{voiceLabel[voiceState]}</react_native_1.Text>
427
+ </react_native_1.TouchableOpacity>
428
+ </>)}
231
429
  {onReload && (<react_native_1.TouchableOpacity style={[
232
430
  styles.actionBtn,
233
431
  styles.reloadBtn,
@@ -323,6 +521,11 @@ const styles = react_native_1.StyleSheet.create({
323
521
  },
324
522
  actionBtnDisabled: { opacity: 0.5 },
325
523
  reloadBtn: { backgroundColor: 'rgba(255,255,255,0.08)' },
524
+ voiceBtn: { backgroundColor: 'rgba(16,185,129,0.18)' },
525
+ voiceBtnActive: { backgroundColor: '#ef4444' },
526
+ engineToggle: { backgroundColor: 'rgba(255,255,255,0.06)', marginRight: 'auto', marginLeft: 0 },
527
+ engineToggleText: { color: '#cbd5e1', fontSize: 12, fontWeight: '600' },
528
+ engineCaption: { color: '#9ca3af', fontSize: 11, marginBottom: 6, marginLeft: 4 },
326
529
  sendBtn: { backgroundColor: '#7582f5' },
327
530
  actionText: { color: '#fff', fontSize: 13, fontWeight: '600' },
328
531
  });
@@ -1,5 +1,6 @@
1
1
  import { FeedbackConfig, CapturedError } from './types';
2
2
  import { P2PClient } from './P2PClient';
3
+ import { CaptureStoreScreenshotsOptions, CaptureStoreScreenshotsResult } from './storeShots';
3
4
  import { QuickIconColorPreset } from './preferences';
4
5
  /**
5
6
  * Main entry point for the Yaver Feedback SDK.
@@ -15,7 +16,26 @@ export declare class YaverFeedback {
15
16
  * If no `agentUrl` is provided, the SDK will attempt auto-discovery
16
17
  * via `YaverDiscovery` on the first `startReport()` call.
17
18
  */
19
+ /** The runtime lane, when running on web inside a Yaver preview WebView. */
20
+ static detectWebLane(): 'browser' | 'webrtc' | null;
21
+ /**
22
+ * Mount a draggable DOM floating "Y" icon for the BROWSER lane (RN-web). It
23
+ * lives in the WebView's document.body (position:fixed, max z-index), so it
24
+ * renders ON TOP of the page inside the WebView and is never occluded by the
25
+ * fullScreen preview modal — the one occlusion-proof affordance on iOS.
26
+ * Tap opens the SDK's existing report flow (the RN FeedbackModal renders in
27
+ * the same WebView DOM, also un-occluded). No-op off-web or off-lane, and
28
+ * idempotent (a re-init reuses the existing node).
29
+ */
30
+ static mountBrowserLaneIcon(): void;
18
31
  static init(cfg: FeedbackConfig): void;
32
+ /**
33
+ * Opt-in global crash handler. This wraps the existing React Native
34
+ * ErrorUtils handler and still calls it, so Sentry/Crashlytics/Bugsnag can
35
+ * remain the system of record. The SDK only uploads a crash report and,
36
+ * when configured, asks the agent to create a fix task.
37
+ */
38
+ static installCrashHandler(): void;
19
39
  /**
20
40
  * Run agent discovery in the background.
21
41
  * Called automatically from init() when no agentUrl is provided.
@@ -46,6 +66,29 @@ export declare class YaverFeedback {
46
66
  static setAuthToken(token: string): Promise<void>;
47
67
  /** Returns true once the SDK has a session token it can use. */
48
68
  static isAuthed(): boolean;
69
+ /**
70
+ * On-device App Store screenshot capture (Engine 2). Walks the routes
71
+ * the host hands us, screenshots each, and uploads to the agent which
72
+ * runs the App Store Connect backend. agentUrl / authToken / relay
73
+ * password default to the SDK's resolved session; the host only has to
74
+ * supply a navigation ref + the ordered route list.
75
+ *
76
+ * YaverFeedback.captureStoreScreenshots({
77
+ * app: 'sfmg',
78
+ * navigationRef,
79
+ * routes: ['/(tabs)/dashboard', '/(tabs)/messages', '/(tabs)/clients'],
80
+ * submit: true,
81
+ * })
82
+ */
83
+ static captureStoreScreenshots(opts: Omit<CaptureStoreScreenshotsOptions, 'agentUrl' | 'authToken' | 'relayPassword'> & Partial<Pick<CaptureStoreScreenshotsOptions, 'agentUrl' | 'authToken' | 'relayPassword'>>): Promise<CaptureStoreScreenshotsResult>;
84
+ /**
85
+ * Let the mobile app / CLI kick THIS device into self-capturing. Registers
86
+ * a BlackBox command listener that fires captureStoreScreenshots when the
87
+ * agent pushes a `capture_store_shots` command (its `data` may override
88
+ * `submit` / `locale`). Returns an unsubscribe fn. Call once after init,
89
+ * passing the app's navigation ref + the routes to walk.
90
+ */
91
+ static enableStoreShotsOnCommand(opts: Omit<CaptureStoreScreenshotsOptions, 'agentUrl' | 'authToken' | 'relayPassword'>): () => void;
49
92
  /**
50
93
  * Request the embedded FeedbackModal to show the login screen. Works by
51
94
  * emitting an event the modal listens for — avoids forcing the host app
@@ -112,6 +155,46 @@ export declare class YaverFeedback {
112
155
  * - All methods become active
113
156
  */
114
157
  static setEnabled(value: boolean): void;
158
+ /**
159
+ * Turn shake-to-report on or off without tearing the SDK down.
160
+ *
161
+ * `trigger` is read only inside init(), so before this the only ways to
162
+ * stop listening for a shake were setEnabled(false) — which also kills the
163
+ * flight recorder and the agent command channel — or a re-init, which used
164
+ * to stack a duplicate command handler. Neither is what "turn the shake
165
+ * catcher off" should cost.
166
+ *
167
+ * Persists onto config.disableShakeGesture, so a later setEnabled(true)
168
+ * honours it rather than resurrecting the listener.
169
+ */
170
+ static setShakeEnabled(value: boolean): void;
171
+ /** Whether the shake listener is currently armed. */
172
+ static isShakeEnabled(): boolean;
173
+ /**
174
+ * Bring the shake listener in line with the current config. Idempotent —
175
+ * safe to call whenever `enabled`, `trigger`, or `disableShakeGesture`
176
+ * moves.
177
+ */
178
+ private static syncShakeDetector;
179
+ private static stopShakeDetector;
180
+ /** Cancel a pending BlackBox auto-start retry chain. */
181
+ private static cancelBlackBoxAutoStart;
182
+ /**
183
+ * Start BlackBox as soon as we have BOTH an agentUrl and a token, polling
184
+ * until then.
185
+ *
186
+ * Both conditions are mandatory and deliberate: starting without a token
187
+ * makes the SSE channel 401 and retry with backoff, which is the tight
188
+ * string-concat + JSON-parse loop that used to SIGSEGV Hermes on iOS 18.3.1
189
+ * during Screenshot & Fix. This only re-checks that same guard over time.
190
+ *
191
+ * Bounded at ~60s: long enough for AsyncStorage hydration plus a Convex
192
+ * discovery round trip on a cold, off-LAN start, short enough that a device
193
+ * whose user never signs in stops polling. Giving up here costs nothing —
194
+ * setAuthToken() and startReport() both reschedule, so signing in later
195
+ * still brings the channel up.
196
+ */
197
+ private static scheduleBlackBoxAutoStart;
115
198
  /** Returns whether the SDK is currently enabled. */
116
199
  static isEnabled(): boolean;
117
200
  /** Returns the current config, or null if not initialized. */
@@ -150,6 +233,21 @@ export declare class YaverFeedback {
150
233
  * // wrapper, and the chain stays intact.
151
234
  */
152
235
  static wrapErrorHandler(next?: ((error: Error, isFatal?: boolean) => void) | null): (error: Error, isFatal?: boolean) => void;
236
+ /**
237
+ * Upload a crash-aware feedback bundle. Apps can call this from their own
238
+ * error boundary/global handler. If crashReporting.autoFix is true, the SDK
239
+ * also triggers the agent's feedback-fix task; reload delivery still happens
240
+ * through the existing BlackBox command stream.
241
+ */
242
+ static reportCrash(error: Error, opts?: {
243
+ isFatal?: boolean;
244
+ source?: 'manual' | 'global-handler';
245
+ metadata?: Record<string, unknown>;
246
+ autoFix?: boolean;
247
+ }): Promise<{
248
+ reportId?: string;
249
+ taskId?: string;
250
+ }>;
153
251
  /**
154
252
  * Returns the P2P client instance.
155
253
  * Available after init if agentUrl is set, or after first successful discovery.