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.
- package/app.plugin.js +126 -9
- package/dist/BlackBox.js +9 -1
- package/dist/DeployPanel.js +65 -0
- package/dist/Discovery.js +13 -2
- package/dist/FeedbackModal.js +160 -6
- package/dist/P2PClient.d.ts +64 -1
- package/dist/P2PClient.js +222 -2
- package/dist/ShakeDetector.js +2 -0
- package/dist/YaverFeedback.d.ts +98 -0
- package/dist/YaverFeedback.js +509 -46
- package/dist/__tests__/BlackBox.relayPassword.test.d.ts +1 -0
- package/dist/__tests__/BlackBox.relayPassword.test.js +105 -0
- package/dist/__tests__/BlackBoxAutoStart.test.d.ts +1 -0
- package/dist/__tests__/BlackBoxAutoStart.test.js +91 -0
- package/dist/__tests__/BlackBoxAutoStartColdStart.test.d.ts +1 -0
- package/dist/__tests__/BlackBoxAutoStartColdStart.test.js +156 -0
- package/dist/__tests__/BrowserLaneIcon.test.d.ts +3 -0
- package/dist/__tests__/BrowserLaneIcon.test.js +80 -0
- package/dist/__tests__/P2PClient.test.js +2 -2
- package/dist/__tests__/ReportIdentity.test.d.ts +1 -0
- package/dist/__tests__/ReportIdentity.test.js +168 -0
- package/dist/__tests__/SDKToken.test.js +1 -1
- package/dist/__tests__/ShakeToggle.test.d.ts +1 -0
- package/dist/__tests__/ShakeToggle.test.js +121 -0
- package/dist/__tests__/pickTargetDevice.test.d.ts +1 -0
- package/dist/__tests__/pickTargetDevice.test.js +89 -0
- package/dist/__tests__/reloadActions.test.d.ts +1 -0
- package/dist/__tests__/reloadActions.test.js +129 -0
- package/dist/__tests__/reloadActionsParity.test.d.ts +1 -0
- package/dist/__tests__/reloadActionsParity.test.js +42 -0
- package/dist/__tests__/types.test.js +5 -5
- package/dist/_core/device.d.ts +19 -9
- package/dist/_core/device.js +20 -14
- package/dist/index.d.ts +4 -0
- package/dist/index.js +11 -1
- package/dist/reloadActions.d.ts +88 -0
- package/dist/reloadActions.js +200 -0
- package/dist/storeShots.d.ts +67 -0
- package/dist/storeShots.js +137 -0
- package/dist/types.d.ts +158 -1
- package/package.json +2 -2
- package/src/BlackBox.ts +10 -1
- package/src/DeployPanel.tsx +74 -0
- package/src/Discovery.ts +13 -2
- package/src/FeedbackModal.tsx +176 -14
- package/src/P2PClient.ts +235 -2
- package/src/ShakeDetector.ts +1 -0
- package/src/YaverFeedback.ts +498 -46
- package/src/__tests__/BlackBox.relayPassword.test.ts +129 -0
- package/src/__tests__/BlackBoxAutoStart.test.ts +111 -0
- package/src/__tests__/BlackBoxAutoStartColdStart.test.ts +191 -0
- package/src/__tests__/BrowserLaneIcon.test.ts +85 -0
- package/src/__tests__/P2PClient.test.ts +2 -2
- package/src/__tests__/ReportIdentity.test.ts +203 -0
- package/src/__tests__/SDKToken.test.ts +1 -1
- package/src/__tests__/ShakeToggle.test.ts +153 -0
- package/src/__tests__/pickTargetDevice.test.ts +101 -0
- package/src/__tests__/reloadActions.test.ts +171 -0
- package/src/__tests__/reloadActionsParity.test.ts +49 -0
- package/src/__tests__/types.test.ts +5 -5
- package/src/_core/device.ts +20 -14
- package/src/index.ts +21 -0
- package/src/reloadActions.ts +273 -0
- package/src/storeShots.ts +189 -0
- 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
|
-
|
|
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
|
-
|
|
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 =
|
|
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) {
|
package/dist/DeployPanel.js
CHANGED
|
@@ -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
|
-
|
|
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);
|
package/dist/FeedbackModal.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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.
|
|
930
|
-
|
|
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)',
|
package/dist/P2PClient.d.ts
CHANGED
|
@@ -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;
|