yaver-feedback-react-native 0.9.0 → 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 (65) 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 +160 -6
  6. package/dist/P2PClient.d.ts +64 -1
  7. package/dist/P2PClient.js +222 -2
  8. package/dist/ShakeDetector.js +2 -0
  9. package/dist/YaverFeedback.d.ts +98 -0
  10. package/dist/YaverFeedback.js +509 -46
  11. package/dist/__tests__/BlackBox.relayPassword.test.d.ts +1 -0
  12. package/dist/__tests__/BlackBox.relayPassword.test.js +105 -0
  13. package/dist/__tests__/BlackBoxAutoStart.test.d.ts +1 -0
  14. package/dist/__tests__/BlackBoxAutoStart.test.js +91 -0
  15. package/dist/__tests__/BlackBoxAutoStartColdStart.test.d.ts +1 -0
  16. package/dist/__tests__/BlackBoxAutoStartColdStart.test.js +156 -0
  17. package/dist/__tests__/BrowserLaneIcon.test.d.ts +3 -0
  18. package/dist/__tests__/BrowserLaneIcon.test.js +80 -0
  19. package/dist/__tests__/P2PClient.test.js +2 -2
  20. package/dist/__tests__/ReportIdentity.test.d.ts +1 -0
  21. package/dist/__tests__/ReportIdentity.test.js +168 -0
  22. package/dist/__tests__/SDKToken.test.js +1 -1
  23. package/dist/__tests__/ShakeToggle.test.d.ts +1 -0
  24. package/dist/__tests__/ShakeToggle.test.js +121 -0
  25. package/dist/__tests__/pickTargetDevice.test.d.ts +1 -0
  26. package/dist/__tests__/pickTargetDevice.test.js +89 -0
  27. package/dist/__tests__/reloadActions.test.d.ts +1 -0
  28. package/dist/__tests__/reloadActions.test.js +129 -0
  29. package/dist/__tests__/reloadActionsParity.test.d.ts +1 -0
  30. package/dist/__tests__/reloadActionsParity.test.js +42 -0
  31. package/dist/__tests__/types.test.js +5 -5
  32. package/dist/_core/device.d.ts +19 -9
  33. package/dist/_core/device.js +20 -14
  34. package/dist/index.d.ts +4 -0
  35. package/dist/index.js +11 -1
  36. package/dist/reloadActions.d.ts +88 -0
  37. package/dist/reloadActions.js +200 -0
  38. package/dist/storeShots.d.ts +67 -0
  39. package/dist/storeShots.js +137 -0
  40. package/dist/types.d.ts +158 -1
  41. package/package.json +2 -2
  42. package/src/BlackBox.ts +10 -1
  43. package/src/DeployPanel.tsx +74 -0
  44. package/src/Discovery.ts +13 -2
  45. package/src/FeedbackModal.tsx +176 -14
  46. package/src/P2PClient.ts +235 -2
  47. package/src/ShakeDetector.ts +1 -0
  48. package/src/YaverFeedback.ts +498 -46
  49. package/src/__tests__/BlackBox.relayPassword.test.ts +129 -0
  50. package/src/__tests__/BlackBoxAutoStart.test.ts +111 -0
  51. package/src/__tests__/BlackBoxAutoStartColdStart.test.ts +191 -0
  52. package/src/__tests__/BrowserLaneIcon.test.ts +85 -0
  53. package/src/__tests__/P2PClient.test.ts +2 -2
  54. package/src/__tests__/ReportIdentity.test.ts +203 -0
  55. package/src/__tests__/SDKToken.test.ts +1 -1
  56. package/src/__tests__/ShakeToggle.test.ts +153 -0
  57. package/src/__tests__/pickTargetDevice.test.ts +101 -0
  58. package/src/__tests__/reloadActions.test.ts +171 -0
  59. package/src/__tests__/reloadActionsParity.test.ts +49 -0
  60. package/src/__tests__/types.test.ts +5 -5
  61. package/src/_core/device.ts +20 -14
  62. package/src/index.ts +21 -0
  63. package/src/reloadActions.ts +273 -0
  64. package/src/storeShots.ts +189 -0
  65. package/src/types.ts +164 -1
package/app.plugin.js CHANGED
@@ -387,14 +387,135 @@ function withYaverAndroidHotReload(config) {
387
387
  },
388
388
  ]);
389
389
 
390
- // Patch MainApplication to register the package and use hot bundle
390
+ // Patch MainApplication to register the package and use hot bundle.
391
+ //
392
+ // MainApplication is Kotlin on React Native 0.73+ (every current Expo
393
+ // template) and Java before that. The two need genuinely different code —
394
+ // `new Foo()`, `final`, and `@Override` are all syntax errors in Kotlin —
395
+ // so branch on the language Expo reports rather than assuming.
391
396
  config = withMainApplication(config, (config) => {
392
- let contents = config.modResults.contents;
393
-
394
- if (contents.includes("YaverHotReload")) {
397
+ if (config.modResults.contents.includes("YaverHotReload")) {
395
398
  return config;
396
399
  }
400
+ config.modResults.contents =
401
+ config.modResults.language === "kt"
402
+ ? patchMainApplicationKotlin(config.modResults.contents)
403
+ : patchMainApplicationJava(config.modResults.contents);
404
+ return config;
405
+ });
406
+
407
+ return config;
408
+ }
409
+
410
+ /**
411
+ * Insert `snippet` immediately before the closing brace of the method whose
412
+ * signature contains `anchor`, by matching braces from the method's opening
413
+ * one.
414
+ *
415
+ * The boot guard has to run at the END of onCreate: it touches
416
+ * reactNativeHost, and reaching that before super.onCreate() and
417
+ * SoLoader.init() would initialise React before its native libraries are
418
+ * loaded. Returns the contents unchanged if the anchor isn't found — a
419
+ * missing safety net is survivable, a corrupted MainApplication is not.
420
+ */
421
+ function insertAtEndOfMethod(contents, anchor, snippet) {
422
+ const anchorIdx = contents.indexOf(anchor);
423
+ if (anchorIdx === -1) return contents;
424
+ const open = contents.indexOf("{", anchorIdx);
425
+ if (open === -1) return contents;
426
+
427
+ let depth = 0;
428
+ for (let i = open; i < contents.length; i++) {
429
+ const ch = contents[i];
430
+ if (ch === "{") depth++;
431
+ else if (ch === "}") {
432
+ depth--;
433
+ if (depth === 0) {
434
+ return contents.slice(0, i) + snippet + contents.slice(i);
435
+ }
436
+ }
437
+ }
438
+ return contents;
439
+ }
397
440
 
441
+ /** Kotlin MainApplication (React Native 0.73+). */
442
+ function patchMainApplicationKotlin(contents) {
443
+ // Imports. Kotlin takes no semicolons.
444
+ contents = contents.replace(
445
+ "import com.facebook.react.ReactApplication",
446
+ "import com.facebook.react.ReactApplication\nimport io.yaver.feedback.YaverHotReloadModule\nimport io.yaver.feedback.YaverHotReloadPackage"
447
+ );
448
+
449
+ // Register the package. Anchor on `return packages` rather than on
450
+ // `packages.add(` — the template's only occurrence of that is inside a
451
+ // commented-out example line.
452
+ contents = contents.replace(
453
+ /(\n([ \t]*)return packages\n)/,
454
+ "\n$2packages.add(YaverHotReloadPackage())\n$1"
455
+ );
456
+
457
+ // Load a hot-pushed bundle when one is present. Inserted before
458
+ // getJSMainModuleName, which every Expo template defines.
459
+ if (!contents.includes("getJSBundleFile")) {
460
+ contents = contents.replace(
461
+ /(\n([ \t]*)override fun getJSMainModuleName\(\))/,
462
+ `
463
+ $2override fun getJSBundleFile(): String? {
464
+ $2 // Yaver Feedback SDK: load hot-reloaded bundle if available
465
+ $2 val hotBundle = YaverHotReloadModule.getSavedBundleFile(application.applicationContext)
466
+ $2 return hotBundle?.absolutePath ?: super.getJSBundleFile()
467
+ $2}
468
+ $1`
469
+ );
470
+ }
471
+
472
+ // Crash-revert safety net: clear the boot-attempt counter once the React
473
+ // context initialises (bundle loaded successfully), AND via a 10-s fallback
474
+ // in case that listener never fires (e.g. an infinite loop in the root
475
+ // component). If neither fires, YaverHotReloadModule.getSavedBundleFile()
476
+ // reverts to the APK-bundled bundle after 3 failed cold starts. Parity with
477
+ // YaverHotReload.swift on iOS.
478
+ if (!contents.includes("yaverHotReloadBootListener")) {
479
+ contents = insertAtEndOfMethod(
480
+ contents,
481
+ "override fun onCreate()",
482
+ `
483
+ // Yaver Feedback SDK hot-reload crash-revert safety net
484
+ val yaverHotReloadCtx: android.content.Context = applicationContext
485
+ android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(
486
+ { YaverHotReloadModule.markBootSuccessful(yaverHotReloadCtx) },
487
+ 10000
488
+ )
489
+ try {
490
+ // Anonymous object, not a lambda: SAM conversion needs a Java interface
491
+ // or a Kotlin \`fun interface\`, and as of RN 0.81
492
+ // com.facebook.react.ReactInstanceEventListener is a plain Kotlin
493
+ // interface (ReactInstanceEventListener.kt). A lambda there fails
494
+ // :app:compileReleaseKotlin with "Argument type mismatch: actual type is
495
+ // 'Function0<Unit>'" — i.e. the host app cannot build a release AAB at
496
+ // all. The Java path below already did this correctly.
497
+ reactNativeHost.reactInstanceManager.addReactInstanceEventListener(
498
+ object : com.facebook.react.ReactInstanceEventListener {
499
+ override fun onReactContextInitialized(
500
+ context: com.facebook.react.bridge.ReactContext
501
+ ) {
502
+ YaverHotReloadModule.markBootSuccessful(yaverHotReloadCtx)
503
+ }
504
+ }
505
+ )
506
+ } catch (yaverHotReloadBootListener: Throwable) {
507
+ // Bridgeless / New Architecture does not expose reactInstanceManager;
508
+ // the 10-s fallback above still covers us.
509
+ }
510
+ `
511
+ );
512
+ }
513
+
514
+ return contents;
515
+ }
516
+
517
+ /** Java MainApplication (React Native < 0.73). */
518
+ function patchMainApplicationJava(contents) {
398
519
  // Add import
399
520
  contents = contents.replace(
400
521
  "import com.facebook.react.ReactApplication",
@@ -468,11 +589,7 @@ function withYaverAndroidHotReload(config) {
468
589
  contents.slice(0, insertionPoint) + bootGuard + contents.slice(insertionPoint);
469
590
  }
470
591
 
471
- config.modResults.contents = contents;
472
- return config;
473
- });
474
-
475
- return config;
592
+ return contents;
476
593
  }
477
594
 
478
595
  function withYaverFeedback(config, props) {
package/dist/BlackBox.js CHANGED
@@ -3,6 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.BlackBox = void 0;
4
4
  const react_native_1 = require("react-native");
5
5
  const YaverFeedback_1 = require("./YaverFeedback");
6
+ function unrefTimer(timer) {
7
+ const maybeNodeTimer = timer;
8
+ if (typeof maybeNodeTimer.unref === 'function') {
9
+ maybeNodeTimer.unref();
10
+ }
11
+ }
6
12
  class BlackBox {
7
13
  /**
8
14
  * Start the black box stream. Call after `YaverFeedback.init()`.
@@ -18,7 +24,7 @@ class BlackBox {
18
24
  }
19
25
  BlackBox.baseUrl = feedbackConfig.agentUrl.replace(/\/$/, '');
20
26
  BlackBox.authToken = feedbackConfig.authToken ?? null;
21
- BlackBox.relayPassword = feedbackConfig.relayPassword ?? '';
27
+ BlackBox.relayPassword = YaverFeedback_1.YaverFeedback.getRelayPassword();
22
28
  BlackBox.deviceId = config?.deviceId ?? BlackBox.generateDeviceId();
23
29
  BlackBox.appName = config?.appName ?? '';
24
30
  BlackBox.flushInterval = config?.flushInterval ?? 2000;
@@ -29,6 +35,7 @@ class BlackBox {
29
35
  if (BlackBox.flushTimer)
30
36
  clearInterval(BlackBox.flushTimer);
31
37
  BlackBox.flushTimer = setInterval(() => BlackBox.flush(), BlackBox.flushInterval);
38
+ unrefTimer(BlackBox.flushTimer);
32
39
  // Log the session start
33
40
  BlackBox.push({
34
41
  type: 'lifecycle',
@@ -343,6 +350,7 @@ class BlackBox {
343
350
  if (BlackBox.started)
344
351
  BlackBox.connectSSE();
345
352
  }, 5000);
353
+ unrefTimer(BlackBox.sseReconnectTimer);
346
354
  }
347
355
  // ─── Internal ────────────────────────────────────────────────────
348
356
  static push(event) {
@@ -151,6 +151,45 @@ const DeployPanel = ({ onClose }) => {
151
151
  </react_native_1.Text>
152
152
  </react_native_1.Pressable>);
153
153
  };
154
+ // First-class App Store screenshots: walk the app's routes on THIS
155
+ // device, screenshot each, upload to the agent (which runs the ASC
156
+ // backend). Closes the overlay first so the captures are clean (no
157
+ // modal in frame), then reports via an alert.
158
+ const runStoreShots = () => {
159
+ const cfg = YaverFeedback_1.YaverFeedback.getConfig();
160
+ const ss = cfg?.storeShots;
161
+ if (!ss?.routes?.length) {
162
+ setStatus('Set config.storeShots.routes (and a navigationRef) to enable.');
163
+ setStatusTone('error');
164
+ return;
165
+ }
166
+ const app = resolveAppSlug();
167
+ onClose();
168
+ // Defer so the overlay is fully dismissed before the first capture.
169
+ setTimeout(async () => {
170
+ try {
171
+ const res = await YaverFeedback_1.YaverFeedback.captureStoreScreenshots({
172
+ app,
173
+ routes: ss.routes,
174
+ navigationRef: ss.navigationRef,
175
+ screens: ss.screens,
176
+ submit: ss.submit,
177
+ });
178
+ const msg = res.ok
179
+ ? res.submitted
180
+ ? 'Submitted for App Store review 🎉'
181
+ : res.staged
182
+ ? `Uploaded ${res.uploaded} screenshots — staged. One tap left in App Store Connect.`
183
+ : `Uploaded ${res.uploaded} App Store screenshots.`
184
+ : res.message || 'Capture failed.';
185
+ react_native_1.Alert.alert('App Store screenshots', msg);
186
+ }
187
+ catch (e) {
188
+ react_native_1.Alert.alert('App Store screenshots', e?.message ?? 'Capture failed.');
189
+ }
190
+ }, 500);
191
+ };
192
+ const storeShotsEnabled = (YaverFeedback_1.YaverFeedback.getConfig()?.storeShots?.routes?.length ?? 0) > 0;
154
193
  const triggerDeploy = async (machine) => {
155
194
  if (!options)
156
195
  return;
@@ -233,6 +272,13 @@ const DeployPanel = ({ onClose }) => {
233
272
  <react_native_1.ActivityIndicator color="rgba(255,255,255,0.6)"/>
234
273
  </react_native_1.View>) : error ? (<react_native_1.Text style={styles.error}>{error}</react_native_1.Text>) : options ? (<react_native_1.ScrollView style={styles.list}>{options.devices.map(machineRow)}</react_native_1.ScrollView>) : null}
235
274
 
275
+ {storeShotsEnabled && (<react_native_1.Pressable onPress={runStoreShots} disabled={shipping} style={({ pressed }) => [styles.shotsBtn, pressed && styles.rowPressed]}>
276
+ <react_native_1.Text style={styles.shotsBtnText}>📸 App Store screenshots</react_native_1.Text>
277
+ <react_native_1.Text style={styles.shotsBtnSub}>
278
+ capture this app on-device + upload to App Store Connect
279
+ </react_native_1.Text>
280
+ </react_native_1.Pressable>)}
281
+
236
282
  {status && (<react_native_1.Text style={[
237
283
  styles.status,
238
284
  statusTone === 'success' && styles.statusSuccess,
@@ -339,6 +385,25 @@ const styles = react_native_1.StyleSheet.create({
339
385
  rowMetaWarning: {
340
386
  color: 'rgb(255,178,115)',
341
387
  },
388
+ shotsBtn: {
389
+ marginTop: 12,
390
+ paddingVertical: 12,
391
+ paddingHorizontal: 14,
392
+ borderRadius: 10,
393
+ backgroundColor: 'rgba(124,109,255,0.16)',
394
+ borderWidth: 1,
395
+ borderColor: 'rgba(124,109,255,0.4)',
396
+ },
397
+ shotsBtnText: {
398
+ color: '#fff',
399
+ fontSize: 14,
400
+ fontWeight: '600',
401
+ },
402
+ shotsBtnSub: {
403
+ color: 'rgba(255,255,255,0.55)',
404
+ fontSize: 12,
405
+ marginTop: 2,
406
+ },
342
407
  status: {
343
408
  color: 'rgba(255,255,255,0.55)',
344
409
  fontSize: 12,
package/dist/Discovery.js CHANGED
@@ -34,6 +34,12 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.YaverDiscovery = void 0;
37
+ function unrefTimer(timer) {
38
+ const maybeNodeTimer = timer;
39
+ if (typeof maybeNodeTimer.unref === 'function') {
40
+ maybeNodeTimer.unref();
41
+ }
42
+ }
37
43
  function getAsyncStorage() {
38
44
  try {
39
45
  const mod = require('@react-native-async-storage/async-storage');
@@ -333,14 +339,15 @@ class YaverDiscovery {
333
339
  static async probe(url) {
334
340
  const base = url.replace(/\/$/, '');
335
341
  const start = Date.now();
342
+ let timeoutId = null;
336
343
  try {
337
344
  const controller = new AbortController();
338
- const timeoutId = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
345
+ timeoutId = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
346
+ unrefTimer(timeoutId);
339
347
  const response = await fetch(`${base}/health`, {
340
348
  method: 'GET',
341
349
  signal: controller.signal,
342
350
  });
343
- clearTimeout(timeoutId);
344
351
  if (!response.ok)
345
352
  return null;
346
353
  const latency = Date.now() - start;
@@ -359,6 +366,10 @@ class YaverDiscovery {
359
366
  catch {
360
367
  return null;
361
368
  }
369
+ finally {
370
+ if (timeoutId)
371
+ clearTimeout(timeoutId);
372
+ }
362
373
  }
363
374
  static async connect(url) {
364
375
  const result = await YaverDiscovery.probe(url);
@@ -39,11 +39,13 @@ const react_native_1 = require("react-native");
39
39
  const YaverFeedback_1 = require("./YaverFeedback");
40
40
  const capture_1 = require("./capture");
41
41
  const upload_1 = require("./upload");
42
+ const P2PClient_1 = require("./P2PClient");
42
43
  const AuthOverlay_1 = require("./AuthOverlay");
43
44
  const QuickActionIcon_1 = require("./QuickActionIcon");
44
45
  const VibeChatScreen_1 = require("./VibeChatScreen");
45
46
  const DeployPanel_1 = require("./DeployPanel");
46
47
  const auth_1 = require("./auth");
48
+ const reloadActions_1 = require("./reloadActions");
47
49
  const preferences_1 = require("./preferences");
48
50
  const PRIMARY_RUNNER_IDS = ['claude', 'codex', 'opencode'];
49
51
  function normalizeRunnerStatusRows(rows) {
@@ -136,6 +138,11 @@ const FeedbackModal = () => {
136
138
  // is our guaranteed UI for bringing the icon back — we surface a
137
139
  // small "Show quick icon" row when this is true.
138
140
  const [quickIconHidden, setQuickIconHidden] = (0, react_1.useState)(false);
141
+ // Dev-server snapshot behind the reload buttons. `null` means "we have not
142
+ // been able to ask" — rendered as "not connected", never as "no dev server".
143
+ const [devSnapshot, setDevSnapshot] = (0, react_1.useState)(null);
144
+ // Which reload action is in flight, so only that button spins.
145
+ const [reloadingId, setReloadingId] = (0, react_1.useState)(null);
139
146
  const [runnerAuthModal, setRunnerAuthModal] = (0, react_1.useState)(null);
140
147
  // Vibing-input mode: same expand-on-tap pattern as email login.
141
148
  // Tap "Vibing" once → the button reveals an input + Send; that lets
@@ -163,6 +170,28 @@ const FeedbackModal = () => {
163
170
  const [preferredModel, setPreferredModelState] = (0, react_1.useState)('');
164
171
  const [showOpenCodeConfig, setShowOpenCodeConfig] = (0, react_1.useState)(false);
165
172
  const mountedRef = (0, react_1.useRef)(true);
173
+ /**
174
+ * Ask the machine what its dev server is doing, so the reload actions can
175
+ * be enabled/disabled against reality rather than against a guess.
176
+ *
177
+ * Best-effort and deliberately null-on-failure: null means "we could not
178
+ * ask", which the seam renders as "not connected to a machine yet" — a
179
+ * different sentence from "no dev server is running", because they have
180
+ * different fixes.
181
+ */
182
+ const refreshDevSnapshot = (0, react_1.useCallback)(async () => {
183
+ try {
184
+ const client = YaverFeedback_1.YaverFeedback.getP2PClient();
185
+ if (!client) {
186
+ setDevSnapshot(null);
187
+ return;
188
+ }
189
+ setDevSnapshot(await client.getDevServerStatus());
190
+ }
191
+ catch {
192
+ setDevSnapshot(null);
193
+ }
194
+ }, []);
166
195
  const loadSelectedMachine = (0, react_1.useCallback)(async () => {
167
196
  const cfg = YaverFeedback_1.YaverFeedback.getConfig();
168
197
  if (!cfg?.authToken) {
@@ -384,12 +413,18 @@ const FeedbackModal = () => {
384
413
  (0, react_1.useEffect)(() => {
385
414
  if (!visible)
386
415
  return;
416
+ // Poll the dev server alongside the machine + runners. A reload button
417
+ // whose enabled state was decided once, when the sheet opened, goes
418
+ // stale the moment the user starts Metro from another surface — and a
419
+ // stale "no dev server is running" reads as the product being broken.
420
+ void refreshDevSnapshot();
387
421
  const interval = setInterval(() => {
388
422
  void loadSelectedMachine();
389
423
  void loadRunnerStatuses();
424
+ void refreshDevSnapshot();
390
425
  }, 5000);
391
426
  return () => clearInterval(interval);
392
- }, [loadRunnerStatuses, loadSelectedMachine, visible]);
427
+ }, [loadRunnerStatuses, loadSelectedMachine, refreshDevSnapshot, visible]);
393
428
  (0, react_1.useEffect)(() => {
394
429
  if (!visible) {
395
430
  setKeyboardInset(0);
@@ -473,7 +508,81 @@ const FeedbackModal = () => {
473
508
  await fn(fresh);
474
509
  }
475
510
  }, []);
476
- // ─── 1. Hot reload ─────────────────────────────────────────────────
511
+ // ─── 1. Reload ─────────────────────────────────────────────────────
512
+ //
513
+ // Three actions now, not one: Hot Reload (mode=fast), Full Reload
514
+ // (mode=full — Flutter's hot RESTART), and Rebuild Bundle
515
+ // (/dev/reload-app, the only one that works with Metro down).
516
+ //
517
+ // WHICH of them render, and which are disabled with what reason, is
518
+ // decided by the pure `reloadActions()` seam — never inline here, so the
519
+ // same policy holds on web, Flutter, Unity, Swift and Kotlin. In
520
+ // particular: a production build (`__DEV__ === false`) gets NONE of them.
521
+ const availableReloadActions = (0, reloadActions_1.reloadActions)(devSnapshot, {
522
+ // __DEV__ is React Native's own build flag. A release bundle sets it
523
+ // false, so a shipped app renders no reload UI at all — which is the
524
+ // point, and is what reloadActions.test.ts pins.
525
+ isDevBuild: typeof __DEV__ !== 'undefined' && __DEV__ === true,
526
+ connected: devSnapshot !== null,
527
+ machineLabel: machineCard.device?.name || undefined,
528
+ includeRebuild: true,
529
+ });
530
+ const handleReloadAction = (0, react_1.useCallback)(async (reloadAction) => {
531
+ if (!reloadAction.enabled) {
532
+ // Pressing a disabled action must SAY why. A row that does nothing
533
+ // is the same defect as a spinner that never resolves.
534
+ setToast(reloadAction.disabledReason || 'Reload is unavailable right now.');
535
+ setError(reloadAction.disabledReason || null);
536
+ return;
537
+ }
538
+ setReloadingId(reloadAction.id);
539
+ setAction('hot-reloading');
540
+ setError(null);
541
+ setProgress(0);
542
+ setToast(`${reloadAction.label}…`);
543
+ try {
544
+ await loadSelectedMachine();
545
+ const selected = await YaverFeedback_1.YaverFeedback.getSelectedRemoteDevice();
546
+ if (!selected) {
547
+ YaverFeedback_1.YaverFeedback.showMachinePicker();
548
+ throw new Error('No machine selected. Pick a machine and try again.');
549
+ }
550
+ if (selected.needsAuth) {
551
+ YaverFeedback_1.YaverFeedback.showMachinePicker();
552
+ throw new Error('Selected machine needs pairing again.');
553
+ }
554
+ if (!selected.isOnline) {
555
+ throw new Error('Selected machine is offline. Start `yaver serve` on it first.');
556
+ }
557
+ let ackMessage = `${reloadAction.label} requested.`;
558
+ await runWithReconnect(async (client) => {
559
+ const ack = await client.reloadWithMode(reloadAction.mode, devSnapshot);
560
+ ackMessage = ack.message;
561
+ setToast(ack.message);
562
+ setProgress(0.2);
563
+ });
564
+ setToast(ackMessage);
565
+ if (reloadAction.mode === 'bundle')
566
+ closeSoon(2500);
567
+ }
568
+ catch (err) {
569
+ const message = err instanceof Error ? err.message : String(err);
570
+ setError(message);
571
+ setToast(message.toLowerCase().indexOf('session expired') >= 0
572
+ ? 'Session expired. Sign in again.'
573
+ : message);
574
+ await loadSelectedMachine();
575
+ setProgress(null);
576
+ }
577
+ finally {
578
+ if (mountedRef.current) {
579
+ setAction('idle');
580
+ setReloadingId(null);
581
+ void refreshDevSnapshot();
582
+ }
583
+ }
584
+ }, [closeSoon, devSnapshot, loadSelectedMachine, refreshDevSnapshot, runWithReconnect]);
585
+ /** Kept for the legacy one-tap path (BlackBox command, chat Reload button). */
477
586
  const handleHotReload = (0, react_1.useCallback)(async () => {
478
587
  setAction('hot-reloading');
479
588
  setError(null);
@@ -579,19 +688,36 @@ const FeedbackModal = () => {
579
688
  try {
580
689
  const { Dimensions } = require('react-native');
581
690
  const { width, height } = Dimensions.get('window');
691
+ const cfg = YaverFeedback_1.YaverFeedback.getConfig();
692
+ const identity = (0, P2PClient_1.resolveReportIdentity)({
693
+ projectName: cfg?.projectName,
694
+ bundleId: cfg?.bundleId,
695
+ surface: cfg?.surface,
696
+ surfaces: cfg?.surfaces,
697
+ stack: cfg?.stack,
698
+ stacks: cfg?.stacks,
699
+ testSurfaces: cfg?.testSurfaces,
700
+ feedbackSdk: cfg?.feedbackSdk,
701
+ feedbackTransport: cfg?.feedbackTransport,
702
+ voiceCapabilities: cfg?.voiceCapabilities,
703
+ sttProvider: cfg?.sttProvider,
704
+ ttsProvider: cfg?.ttsProvider,
705
+ });
582
706
  const deviceInfo = {
583
707
  platform: react_native_1.Platform.OS,
584
708
  osVersion: String(react_native_1.Platform.Version),
585
709
  model: react_native_1.Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
586
710
  screenWidth: width,
587
711
  screenHeight: height,
712
+ appName: identity.appName,
588
713
  };
589
714
  const capturedErrors = YaverFeedback_1.YaverFeedback.getCapturedErrors();
590
715
  const bundle = {
591
716
  metadata: {
592
717
  timestamp: new Date().toISOString(),
593
- device: deviceInfo,
594
- app: {},
718
+ deviceInfo,
719
+ app: identity.app,
720
+ project: identity.project,
595
721
  userNote: '[Screenshot + Fix]',
596
722
  },
597
723
  screenshots: [path],
@@ -926,8 +1052,27 @@ const FeedbackModal = () => {
926
1052
  </react_native_1.View>
927
1053
  </react_native_1.View>
928
1054
 
929
- {/* 1. Hot Reload — the common path */}
930
- <ActionRow label={action === 'hot-reloading' ? 'Reloading…' : 'Hot Reload'} tint="#fbbf24" onPress={handleHotReload} disabled={busy} busy={action === 'hot-reloading'}/>
1055
+ {/* 1. Reload — Hot / Full / Rebuild Bundle.
1056
+ Rendered from the shared decision seam, so a production
1057
+ build (__DEV__ false) renders nothing here at all, and a
1058
+ blocked action shows greyed WITH its reason underneath
1059
+ rather than vanishing. */}
1060
+ {availableReloadActions.map((reloadAction) => (<react_native_1.View key={reloadAction.id} style={styles.reloadRow}>
1061
+ <ActionRow label={reloadingId === reloadAction.id
1062
+ ? `${reloadAction.label}…`
1063
+ : reloadAction.label} tint={reloadAction.id === 'rebuild' ? '#38bdf8' : '#fbbf24'} onPress={() => {
1064
+ void handleReloadAction(reloadAction);
1065
+ }}
1066
+ // Never `disabled` at the Pressable level for a blocked
1067
+ // action: we WANT the tap so we can say why. Only a
1068
+ // genuinely busy modal blocks the press.
1069
+ disabled={busy && reloadingId !== reloadAction.id} busy={reloadingId === reloadAction.id}/>
1070
+ <react_native_1.Text style={styles.reloadHint}>
1071
+ {reloadAction.enabled
1072
+ ? reloadAction.hint
1073
+ : reloadAction.disabledReason}
1074
+ </react_native_1.Text>
1075
+ </react_native_1.View>))}
931
1076
 
932
1077
  {/* 3. Vibing — expands to an input box on first tap
933
1078
  so the user says WHAT they want to vibe on, just
@@ -1004,6 +1149,15 @@ const ActionRow = ({ label, tint, onPress, disabled, busy, }) => (<react_native_
1004
1149
  {busy ? (<react_native_1.ActivityIndicator color={tint} size="small"/>) : (<react_native_1.Text style={[styles.actionText, { color: tint }]}>{label}</react_native_1.Text>)}
1005
1150
  </react_native_1.Pressable>);
1006
1151
  const styles = react_native_1.StyleSheet.create({
1152
+ reloadRow: {
1153
+ gap: 4,
1154
+ },
1155
+ reloadHint: {
1156
+ color: '#8b8b93',
1157
+ fontSize: 11,
1158
+ lineHeight: 15,
1159
+ paddingHorizontal: 4,
1160
+ },
1007
1161
  vibeInputRow: {
1008
1162
  backgroundColor: 'rgba(129,140,248,0.08)',
1009
1163
  borderColor: 'rgba(129,140,248,0.4)',
@@ -1,4 +1,5 @@
1
- import { CapabilitySnapshot, FeedbackBundle, IncidentEvent, OpenCodeConfigSummary, OperationState, RunnerBrowserAuthSession, RunnerAuthStatusRow, TestSession, VoiceCapability } from './types';
1
+ import { AppInfo, CapabilitySnapshot, FeedbackBundle, FeedbackProjectRef, IncidentEvent, OpenCodeConfigSummary, OperationState, RunnerBrowserAuthSession, RunnerAuthStatusRow, TestSession, VoiceCapability } from './types';
2
+ import type { DevServerSnapshot, ReloadWireMode } from './reloadActions';
2
3
  export interface FeedbackEvent {
3
4
  type: string;
4
5
  timestamp: string;
@@ -28,6 +29,45 @@ export declare function resolveAppIdentity(opts?: {
28
29
  bundleId?: string;
29
30
  projectPath?: string;
30
31
  };
32
+ /**
33
+ * Build the app-identity half of a feedback report's metadata.
34
+ *
35
+ * `resolveAppIdentity()` has always fed /vibing/execute and /dev/reload-app,
36
+ * so the agent could route a "vibe on THIS app" request to the right repo —
37
+ * but nothing fed /feedback. Reports arrived with no identity at all, so the
38
+ * agent's fix router fell through to its own working directory and edited
39
+ * whichever repo it happened to be sitting in. This closes that gap by
40
+ * reusing the same resolver for the feedback path.
41
+ *
42
+ * Precedence, most to least trustworthy:
43
+ * 1. What the host app declared (`FeedbackConfig.projectName`/`bundleId`).
44
+ * An app knows its own identity; nothing should override it.
45
+ * 2. The guest project Yaver's host shell pinned, when running as a Hermes
46
+ * guest. Ambient lookups describe Yaver there, not the guest.
47
+ * 3. Ambient expo-constants / native modules — correct for a standalone
48
+ * build, which is the common case.
49
+ *
50
+ * Every lookup is best-effort: a bare RN app with no expo-constants still
51
+ * produces a report, just one the agent resolves by its own means.
52
+ */
53
+ export declare function resolveReportIdentity(opts?: {
54
+ projectName?: string;
55
+ bundleId?: string;
56
+ surface?: FeedbackProjectRef['surface'];
57
+ surfaces?: FeedbackProjectRef['surfaces'];
58
+ stack?: string;
59
+ stacks?: string[];
60
+ testSurfaces?: string[];
61
+ feedbackSdk?: string;
62
+ feedbackTransport?: string;
63
+ voiceCapabilities?: string[];
64
+ sttProvider?: string;
65
+ ttsProvider?: string;
66
+ }): {
67
+ appName?: string;
68
+ app: AppInfo;
69
+ project?: FeedbackProjectRef;
70
+ };
31
71
  /**
32
72
  * Lightweight P2P HTTP client for communicating with a Yaver agent.
33
73
  *
@@ -163,6 +203,29 @@ export declare class P2PClient {
163
203
  * via the BlackBox command channel.
164
204
  * @param mode - "dev" for hot reload, "bundle" for native bundle rebuild
165
205
  */
206
+ /**
207
+ * Read the dev server's state so the overlay can decide WHICH reload
208
+ * actions to offer, and disable the rest with a reason.
209
+ *
210
+ * Returns null when the machine cannot be reached at all — which the
211
+ * caller must render as "not connected", never as "no dev server".
212
+ * Those are two different problems with two different fixes.
213
+ */
214
+ getDevServerStatus(): Promise<DevServerSnapshot | null>;
215
+ /**
216
+ * Trigger a reload with an EXPLICIT fast/full mode — no bundle fallback.
217
+ *
218
+ * `reloadApp('dev')` silently falls through to a bundle rebuild when the
219
+ * dev server is down, which is right for a one-button UX and wrong the
220
+ * moment the user picks between two named actions: someone who pressed
221
+ * "Full Reload" must not get a 60-second bundle rebuild without being
222
+ * told. So this method reports the failure instead, with a named cause.
223
+ *
224
+ * Auth: the SAME bearer used for the feedback POST. `/dev/reload` is
225
+ * registered under `authSDKOrGuest` in desktop/agent/httpserver.go and is
226
+ * already in the `guest-reload` SDK-token scope list — nothing widens.
227
+ */
228
+ reloadWithMode(mode: ReloadWireMode, snapshot?: DevServerSnapshot | null): Promise<ReloadAck>;
166
229
  reloadApp(mode?: 'dev' | 'bundle', opts?: {
167
230
  projectName?: string;
168
231
  bundleId?: string;