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/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BlackBoxConfig } from './BlackBox';
|
|
1
2
|
/**
|
|
2
3
|
* Remote browser-style sign-in session for a coding-agent CLI on the
|
|
3
4
|
* connected yaver host. Mirrors runnerBrowserAuthSession on the agent
|
|
@@ -121,9 +122,32 @@ export interface CapabilitySnapshot {
|
|
|
121
122
|
};
|
|
122
123
|
targets: Record<string, CapabilityTargetReadiness>;
|
|
123
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Opt-in config for the overlay's "App Store screenshots" action and the
|
|
127
|
+
* `capture_store_shots` remote command. When set, the SDK can walk the
|
|
128
|
+
* app's routes on-device, screenshot each, and upload them to the agent
|
|
129
|
+
* (which runs the App Store Connect backend).
|
|
130
|
+
*/
|
|
131
|
+
export interface StoreShotsConfig {
|
|
132
|
+
/** Ordered routes to visit + screenshot (e.g. ['/(tabs)/home', ...]). */
|
|
133
|
+
routes: string[];
|
|
134
|
+
/** Navigation handle: a react-navigation ref or expo-router `router`. */
|
|
135
|
+
navigationRef?: any;
|
|
136
|
+
/** Also set metadata + attempt submit-for-review after upload. */
|
|
137
|
+
submit?: boolean;
|
|
138
|
+
/** Optional per-route screenshot names (defaults to NN_<route>). */
|
|
139
|
+
screens?: string[];
|
|
140
|
+
}
|
|
124
141
|
export interface FeedbackConfig {
|
|
125
142
|
/** URL of the Yaver agent (e.g. "http://192.168.1.10:18080"). If omitted, auto-discovery is used. */
|
|
126
143
|
agentUrl?: string;
|
|
144
|
+
/**
|
|
145
|
+
* Enables the overlay's "App Store screenshots" action + the
|
|
146
|
+
* `capture_store_shots` remote command. The host supplies the route
|
|
147
|
+
* list (and a navigation ref) once; the SDK captures the real running
|
|
148
|
+
* app — no simulator needed.
|
|
149
|
+
*/
|
|
150
|
+
storeShots?: StoreShotsConfig;
|
|
127
151
|
/**
|
|
128
152
|
* Auth token for the Yaver agent. Optional in 0.5+: if omitted, the SDK
|
|
129
153
|
* will hydrate one from AsyncStorage or show its in-app login screen
|
|
@@ -163,6 +187,39 @@ export interface FeedbackConfig {
|
|
|
163
187
|
* If omitted with convexUrl, connects to the first online device.
|
|
164
188
|
*/
|
|
165
189
|
preferredDeviceId?: string;
|
|
190
|
+
/**
|
|
191
|
+
* Identity of the app this SDK instance reports for. Both fields are
|
|
192
|
+
* resolved automatically from expo-constants / native modules when
|
|
193
|
+
* omitted, so most apps never set them.
|
|
194
|
+
*
|
|
195
|
+
* Set them when the ambient answer would be wrong — above all when the
|
|
196
|
+
* app's JS is pushed into Yaver's container as a Hermes guest. There the
|
|
197
|
+
* runtime IS Yaver: `expo-constants` and `SettingsManager` describe the
|
|
198
|
+
* host (`Yaver` / `io.yaver.mobile`), not the guest, so auto-resolution
|
|
199
|
+
* would file every report against Yaver's own repo and the fix task would
|
|
200
|
+
* edit the SDK instead of the app. Declaring identity here is the only
|
|
201
|
+
* answer that holds in both a standalone build and a guest bundle.
|
|
202
|
+
*
|
|
203
|
+
* `bundleId` is the key that actually routes: the agent matches it against
|
|
204
|
+
* each project's pbxproj / build.gradle / app.json. `projectName` is a
|
|
205
|
+
* fallback for platforms with no bundle id.
|
|
206
|
+
*/
|
|
207
|
+
projectName?: string;
|
|
208
|
+
bundleId?: string;
|
|
209
|
+
/** Primary Yaver product surface for this app/package. */
|
|
210
|
+
surface?: YaverSurface;
|
|
211
|
+
/** All product surfaces this app/package owns. */
|
|
212
|
+
surfaces?: YaverSurface[];
|
|
213
|
+
/** Canonical development stack labels, e.g. react-native-expo, yaver-xml. */
|
|
214
|
+
stack?: string;
|
|
215
|
+
stacks?: string[];
|
|
216
|
+
/** Yaver preview/runtime targets that can exercise this app. */
|
|
217
|
+
testSurfaces?: string[];
|
|
218
|
+
feedbackSdk?: string;
|
|
219
|
+
feedbackTransport?: string;
|
|
220
|
+
voiceCapabilities?: string[];
|
|
221
|
+
sttProvider?: string;
|
|
222
|
+
ttsProvider?: string;
|
|
166
223
|
/** How feedback collection is triggered */
|
|
167
224
|
trigger?: 'shake' | 'floating-button' | 'manual';
|
|
168
225
|
/**
|
|
@@ -203,6 +260,22 @@ export interface FeedbackConfig {
|
|
|
203
260
|
* Calling `BlackBox.start()` manually is still safe — it's idempotent.
|
|
204
261
|
*/
|
|
205
262
|
autoStartBlackBox?: boolean;
|
|
263
|
+
/**
|
|
264
|
+
* Config handed to BlackBox when the SDK auto-starts it.
|
|
265
|
+
*
|
|
266
|
+
* Without this there was no way to configure the flight recorder at all.
|
|
267
|
+
* The obvious move — and the one the README's own snippet makes — is:
|
|
268
|
+
*
|
|
269
|
+
* YaverFeedback.init({ trigger: 'shake' });
|
|
270
|
+
* BlackBox.start({ appName: 'my-app' });
|
|
271
|
+
*
|
|
272
|
+
* but that start() early-returns, because a zero-config init has no
|
|
273
|
+
* agentUrl yet (discovery resolves it asynchronously). The auto-start then
|
|
274
|
+
* fires ~500ms later once the agent and token exist and calls `start()`
|
|
275
|
+
* with NO arguments — so the host's config is silently dropped and appName
|
|
276
|
+
* is ''. Pass it here instead and the auto-start will honour it.
|
|
277
|
+
*/
|
|
278
|
+
blackBox?: BlackBoxConfig;
|
|
206
279
|
/**
|
|
207
280
|
* Small tap-to-open icon that floats above the app so the user
|
|
208
281
|
* doesn't have to shake every time they want to open feedback.
|
|
@@ -291,6 +364,26 @@ export interface FeedbackConfig {
|
|
|
291
364
|
* Bugsnag, or any other error tracking tool.
|
|
292
365
|
*/
|
|
293
366
|
maxCapturedErrors?: number;
|
|
367
|
+
/**
|
|
368
|
+
* Crash-aware reporting/fixing. Defaults to disabled.
|
|
369
|
+
*
|
|
370
|
+
* - enabled: records fatal errors into the SDK error buffer and enables
|
|
371
|
+
* explicit `YaverFeedback.reportCrash(error)`.
|
|
372
|
+
* - installGlobalHandler: opt-in only. Wraps React Native's global ErrorUtils
|
|
373
|
+
* handler without suppressing the existing handler. Leave false when Sentry,
|
|
374
|
+
* Crashlytics, Bugsnag, or another crash tool owns the global handler.
|
|
375
|
+
* - autoFix: after uploading a crash report, ask the connected Yaver agent to
|
|
376
|
+
* create a fix task. The normal BlackBox command stream can then deliver a
|
|
377
|
+
* reload/reload_bundle command after the agent rebuilds.
|
|
378
|
+
* - captureScreenshot: attach a best-effort screenshot to the crash report.
|
|
379
|
+
*/
|
|
380
|
+
crashReporting?: {
|
|
381
|
+
enabled?: boolean;
|
|
382
|
+
installGlobalHandler?: boolean;
|
|
383
|
+
autoFix?: boolean;
|
|
384
|
+
captureScreenshot?: boolean;
|
|
385
|
+
metadata?: Record<string, unknown>;
|
|
386
|
+
};
|
|
294
387
|
/**
|
|
295
388
|
* Which platforms the Build button targets.
|
|
296
389
|
* - 'ios' — build iOS only
|
|
@@ -403,9 +496,36 @@ export interface CapturedError {
|
|
|
403
496
|
}
|
|
404
497
|
export interface FeedbackMetadata {
|
|
405
498
|
timestamp: string;
|
|
406
|
-
|
|
499
|
+
/**
|
|
500
|
+
* Wire key is `deviceInfo`, matching the Flutter + web SDKs and the
|
|
501
|
+
* agent's `FeedbackReport.DeviceInfo` (`json:"deviceInfo"`). This was
|
|
502
|
+
* `device` until 0.9.2 — a key the agent never read, so the whole block
|
|
503
|
+
* (platform, model, and the app name the fix router needs) was silently
|
|
504
|
+
* dropped on every React Native report. The agent still accepts the old
|
|
505
|
+
* key so builds already in the field keep working.
|
|
506
|
+
*/
|
|
507
|
+
deviceInfo: DeviceInfo;
|
|
407
508
|
app: AppInfo;
|
|
509
|
+
/**
|
|
510
|
+
* Which of the host's projects this report is about. The agent resolves
|
|
511
|
+
* a feedback→fix task's working directory from this; without it the task
|
|
512
|
+
* falls back to whatever directory the agent happens to be sitting in.
|
|
513
|
+
* Populated automatically by `resolveReportIdentity()`.
|
|
514
|
+
*/
|
|
515
|
+
project?: FeedbackProjectRef;
|
|
408
516
|
userNote?: string;
|
|
517
|
+
reportKind?: 'feedback' | 'auto-report' | 'crash';
|
|
518
|
+
crash?: {
|
|
519
|
+
message: string;
|
|
520
|
+
isFatal: boolean;
|
|
521
|
+
source: 'manual' | 'global-handler';
|
|
522
|
+
autoFixRequested?: boolean;
|
|
523
|
+
};
|
|
524
|
+
twin?: {
|
|
525
|
+
sessionId?: string;
|
|
526
|
+
surface?: string;
|
|
527
|
+
artifactDir?: string;
|
|
528
|
+
};
|
|
409
529
|
}
|
|
410
530
|
export interface DeviceInfo {
|
|
411
531
|
platform: string;
|
|
@@ -413,12 +533,49 @@ export interface DeviceInfo {
|
|
|
413
533
|
model: string;
|
|
414
534
|
screenWidth: number;
|
|
415
535
|
screenHeight: number;
|
|
536
|
+
/** Host app name. First key the agent's fix router checks. */
|
|
537
|
+
appName?: string;
|
|
416
538
|
}
|
|
417
539
|
export interface AppInfo {
|
|
418
540
|
bundleId?: string;
|
|
419
541
|
version?: string;
|
|
420
542
|
buildNumber?: string;
|
|
421
543
|
}
|
|
544
|
+
/**
|
|
545
|
+
* Identifies the host project a report belongs to. Mirrors the web SDK's
|
|
546
|
+
* `FeedbackProjectRef` and the agent's `FeedbackProject`.
|
|
547
|
+
*/
|
|
548
|
+
export interface FeedbackProjectRef {
|
|
549
|
+
appName?: string;
|
|
550
|
+
projectName?: string;
|
|
551
|
+
/**
|
|
552
|
+
* Bundle identifier / applicationId. The only unambiguous key: an app's
|
|
553
|
+
* display name ("Talos") does not match the agent's registry name for it
|
|
554
|
+
* ("talos / mobile"), and one repo can hold several mobile projects that
|
|
555
|
+
* all share a name prefix. The agent matches this against the project's
|
|
556
|
+
* pbxproj, build.gradle, and app.json.
|
|
557
|
+
*/
|
|
558
|
+
bundleId?: string;
|
|
559
|
+
/**
|
|
560
|
+
* Deliberately never set by this SDK. The agent ignores client-supplied
|
|
561
|
+
* paths for feedback reports (an untrusted guest could otherwise point the
|
|
562
|
+
* fix task's CWD at ~/.ssh) and resolves the path server-side. Present only
|
|
563
|
+
* so the type matches the wire format the agent parses.
|
|
564
|
+
*/
|
|
565
|
+
projectPath?: string;
|
|
566
|
+
surface?: YaverSurface;
|
|
567
|
+
surfaces?: YaverSurface[];
|
|
568
|
+
stack?: string;
|
|
569
|
+
stacks?: string[];
|
|
570
|
+
testSurfaces?: string[];
|
|
571
|
+
feedbackSdk?: string;
|
|
572
|
+
feedbackTransport?: string;
|
|
573
|
+
voiceCapabilities?: string[];
|
|
574
|
+
sttProvider?: string;
|
|
575
|
+
ttsProvider?: string;
|
|
576
|
+
releaseChannel?: 'production' | 'candidate' | 'development';
|
|
577
|
+
}
|
|
578
|
+
export type YaverSurface = 'web' | 'mobile' | 'backend' | 'watch' | 'tv' | 'car' | 'vision' | 'desktop' | 'cli';
|
|
422
579
|
export interface TimelineEvent {
|
|
423
580
|
type: 'screenshot' | 'audio' | 'video';
|
|
424
581
|
path: string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yaver-feedback-react-native",
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"description": "Visual feedback SDK for Yaver
|
|
3
|
+
"version": "0.9.2",
|
|
4
|
+
"description": "Visual feedback SDK for Yaver — bug reports, screen recording, voice vibe coding, and local-first developer workflows",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"files": [
|
package/src/BlackBox.ts
CHANGED
|
@@ -66,6 +66,13 @@ export interface BlackBoxCommand {
|
|
|
66
66
|
/** Callback type for handling agent commands. */
|
|
67
67
|
export type CommandHandler = (cmd: BlackBoxCommand) => void;
|
|
68
68
|
|
|
69
|
+
function unrefTimer(timer: ReturnType<typeof setTimeout> | ReturnType<typeof setInterval>): void {
|
|
70
|
+
const maybeNodeTimer = timer as unknown as { unref?: () => void };
|
|
71
|
+
if (typeof maybeNodeTimer.unref === 'function') {
|
|
72
|
+
maybeNodeTimer.unref();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
69
76
|
export class BlackBox {
|
|
70
77
|
private static baseUrl: string | null = null;
|
|
71
78
|
private static authToken: string | null = null;
|
|
@@ -109,7 +116,7 @@ export class BlackBox {
|
|
|
109
116
|
|
|
110
117
|
BlackBox.baseUrl = feedbackConfig.agentUrl.replace(/\/$/, '');
|
|
111
118
|
BlackBox.authToken = feedbackConfig.authToken ?? null;
|
|
112
|
-
BlackBox.relayPassword = (
|
|
119
|
+
BlackBox.relayPassword = YaverFeedback.getRelayPassword();
|
|
113
120
|
BlackBox.deviceId = config?.deviceId ?? BlackBox.generateDeviceId();
|
|
114
121
|
BlackBox.appName = config?.appName ?? '';
|
|
115
122
|
BlackBox.flushInterval = config?.flushInterval ?? 2000;
|
|
@@ -120,6 +127,7 @@ export class BlackBox {
|
|
|
120
127
|
// Start periodic flush
|
|
121
128
|
if (BlackBox.flushTimer) clearInterval(BlackBox.flushTimer);
|
|
122
129
|
BlackBox.flushTimer = setInterval(() => BlackBox.flush(), BlackBox.flushInterval);
|
|
130
|
+
unrefTimer(BlackBox.flushTimer);
|
|
123
131
|
|
|
124
132
|
// Log the session start
|
|
125
133
|
BlackBox.push({
|
|
@@ -471,6 +479,7 @@ export class BlackBox {
|
|
|
471
479
|
BlackBox.sseReconnectTimer = null;
|
|
472
480
|
if (BlackBox.started) BlackBox.connectSSE();
|
|
473
481
|
}, 5000);
|
|
482
|
+
unrefTimer(BlackBox.sseReconnectTimer);
|
|
474
483
|
}
|
|
475
484
|
|
|
476
485
|
// ─── Internal ────────────────────────────────────────────────────
|
package/src/DeployPanel.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useState } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
ActivityIndicator,
|
|
4
|
+
Alert,
|
|
4
5
|
Pressable,
|
|
5
6
|
ScrollView,
|
|
6
7
|
StyleSheet,
|
|
@@ -184,6 +185,47 @@ export const DeployPanel: React.FC<DeployPanelProps> = ({ onClose }) => {
|
|
|
184
185
|
);
|
|
185
186
|
};
|
|
186
187
|
|
|
188
|
+
// First-class App Store screenshots: walk the app's routes on THIS
|
|
189
|
+
// device, screenshot each, upload to the agent (which runs the ASC
|
|
190
|
+
// backend). Closes the overlay first so the captures are clean (no
|
|
191
|
+
// modal in frame), then reports via an alert.
|
|
192
|
+
const runStoreShots = () => {
|
|
193
|
+
const cfg = YaverFeedback.getConfig() as { storeShots?: any } | null;
|
|
194
|
+
const ss = cfg?.storeShots;
|
|
195
|
+
if (!ss?.routes?.length) {
|
|
196
|
+
setStatus('Set config.storeShots.routes (and a navigationRef) to enable.');
|
|
197
|
+
setStatusTone('error');
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const app = resolveAppSlug();
|
|
201
|
+
onClose();
|
|
202
|
+
// Defer so the overlay is fully dismissed before the first capture.
|
|
203
|
+
setTimeout(async () => {
|
|
204
|
+
try {
|
|
205
|
+
const res = await YaverFeedback.captureStoreScreenshots({
|
|
206
|
+
app,
|
|
207
|
+
routes: ss.routes,
|
|
208
|
+
navigationRef: ss.navigationRef,
|
|
209
|
+
screens: ss.screens,
|
|
210
|
+
submit: ss.submit,
|
|
211
|
+
});
|
|
212
|
+
const msg = res.ok
|
|
213
|
+
? res.submitted
|
|
214
|
+
? 'Submitted for App Store review 🎉'
|
|
215
|
+
: res.staged
|
|
216
|
+
? `Uploaded ${res.uploaded} screenshots — staged. One tap left in App Store Connect.`
|
|
217
|
+
: `Uploaded ${res.uploaded} App Store screenshots.`
|
|
218
|
+
: res.message || 'Capture failed.';
|
|
219
|
+
Alert.alert('App Store screenshots', msg);
|
|
220
|
+
} catch (e: any) {
|
|
221
|
+
Alert.alert('App Store screenshots', e?.message ?? 'Capture failed.');
|
|
222
|
+
}
|
|
223
|
+
}, 500);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const storeShotsEnabled =
|
|
227
|
+
((YaverFeedback.getConfig() as { storeShots?: any } | null)?.storeShots?.routes?.length ?? 0) > 0;
|
|
228
|
+
|
|
187
229
|
const triggerDeploy = async (machine: string) => {
|
|
188
230
|
if (!options) return;
|
|
189
231
|
setShipping(true);
|
|
@@ -277,6 +319,19 @@ export const DeployPanel: React.FC<DeployPanelProps> = ({ onClose }) => {
|
|
|
277
319
|
<ScrollView style={styles.list}>{options.devices.map(machineRow)}</ScrollView>
|
|
278
320
|
) : null}
|
|
279
321
|
|
|
322
|
+
{storeShotsEnabled && (
|
|
323
|
+
<Pressable
|
|
324
|
+
onPress={runStoreShots}
|
|
325
|
+
disabled={shipping}
|
|
326
|
+
style={({ pressed }) => [styles.shotsBtn, pressed && styles.rowPressed]}
|
|
327
|
+
>
|
|
328
|
+
<Text style={styles.shotsBtnText}>📸 App Store screenshots</Text>
|
|
329
|
+
<Text style={styles.shotsBtnSub}>
|
|
330
|
+
capture this app on-device + upload to App Store Connect
|
|
331
|
+
</Text>
|
|
332
|
+
</Pressable>
|
|
333
|
+
)}
|
|
334
|
+
|
|
280
335
|
{status && (
|
|
281
336
|
<Text
|
|
282
337
|
style={[
|
|
@@ -388,6 +443,25 @@ const styles = StyleSheet.create({
|
|
|
388
443
|
rowMetaWarning: {
|
|
389
444
|
color: 'rgb(255,178,115)',
|
|
390
445
|
},
|
|
446
|
+
shotsBtn: {
|
|
447
|
+
marginTop: 12,
|
|
448
|
+
paddingVertical: 12,
|
|
449
|
+
paddingHorizontal: 14,
|
|
450
|
+
borderRadius: 10,
|
|
451
|
+
backgroundColor: 'rgba(124,109,255,0.16)',
|
|
452
|
+
borderWidth: 1,
|
|
453
|
+
borderColor: 'rgba(124,109,255,0.4)',
|
|
454
|
+
},
|
|
455
|
+
shotsBtnText: {
|
|
456
|
+
color: '#fff',
|
|
457
|
+
fontSize: 14,
|
|
458
|
+
fontWeight: '600',
|
|
459
|
+
},
|
|
460
|
+
shotsBtnSub: {
|
|
461
|
+
color: 'rgba(255,255,255,0.55)',
|
|
462
|
+
fontSize: 12,
|
|
463
|
+
marginTop: 2,
|
|
464
|
+
},
|
|
391
465
|
status: {
|
|
392
466
|
color: 'rgba(255,255,255,0.55)',
|
|
393
467
|
fontSize: 12,
|
package/src/Discovery.ts
CHANGED
|
@@ -7,6 +7,14 @@ type AsyncStorageLike = {
|
|
|
7
7
|
setItem: (key: string, value: string) => Promise<void>;
|
|
8
8
|
removeItem: (key: string) => Promise<void>;
|
|
9
9
|
};
|
|
10
|
+
|
|
11
|
+
function unrefTimer(timer: ReturnType<typeof setTimeout>): void {
|
|
12
|
+
const maybeNodeTimer = timer as unknown as { unref?: () => void };
|
|
13
|
+
if (typeof maybeNodeTimer.unref === 'function') {
|
|
14
|
+
maybeNodeTimer.unref();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
10
18
|
function getAsyncStorage(): AsyncStorageLike | null {
|
|
11
19
|
try {
|
|
12
20
|
const mod = require('@react-native-async-storage/async-storage');
|
|
@@ -362,14 +370,15 @@ export class YaverDiscovery {
|
|
|
362
370
|
static async probe(url: string): Promise<DiscoveryResult | null> {
|
|
363
371
|
const base = url.replace(/\/$/, '');
|
|
364
372
|
const start = Date.now();
|
|
373
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
365
374
|
try {
|
|
366
375
|
const controller = new AbortController();
|
|
367
|
-
|
|
376
|
+
timeoutId = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
|
|
377
|
+
unrefTimer(timeoutId);
|
|
368
378
|
const response = await fetch(`${base}/health`, {
|
|
369
379
|
method: 'GET',
|
|
370
380
|
signal: controller.signal,
|
|
371
381
|
});
|
|
372
|
-
clearTimeout(timeoutId);
|
|
373
382
|
if (!response.ok) return null;
|
|
374
383
|
const latency = Date.now() - start;
|
|
375
384
|
let hostname = 'Unknown';
|
|
@@ -384,6 +393,8 @@ export class YaverDiscovery {
|
|
|
384
393
|
return { url: base, hostname, version, latency };
|
|
385
394
|
} catch {
|
|
386
395
|
return null;
|
|
396
|
+
} finally {
|
|
397
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
387
398
|
}
|
|
388
399
|
}
|
|
389
400
|
|
package/src/FeedbackModal.tsx
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
// stopVideoRecording,
|
|
26
26
|
} from './capture';
|
|
27
27
|
import { uploadFeedback } from './upload';
|
|
28
|
+
import { resolveReportIdentity } from './P2PClient';
|
|
28
29
|
import {
|
|
29
30
|
DeviceInfo,
|
|
30
31
|
FeedbackBundle,
|
|
@@ -37,6 +38,8 @@ import { QuickActionIcon } from './QuickActionIcon';
|
|
|
37
38
|
import { VibeChatScreen } from './VibeChatScreen';
|
|
38
39
|
import { DeployPanel } from './DeployPanel';
|
|
39
40
|
import { listReachableDevices, RemoteDevice } from './auth';
|
|
41
|
+
import { reloadActions } from './reloadActions';
|
|
42
|
+
import type { DevServerSnapshot, ReloadAction } from './reloadActions';
|
|
40
43
|
import {
|
|
41
44
|
QUICK_ICON_COLOR_PRESETS,
|
|
42
45
|
QuickIconColorPreset,
|
|
@@ -195,6 +198,11 @@ export const FeedbackModal: React.FC = () => {
|
|
|
195
198
|
// is our guaranteed UI for bringing the icon back — we surface a
|
|
196
199
|
// small "Show quick icon" row when this is true.
|
|
197
200
|
const [quickIconHidden, setQuickIconHidden] = useState(false);
|
|
201
|
+
// Dev-server snapshot behind the reload buttons. `null` means "we have not
|
|
202
|
+
// been able to ask" — rendered as "not connected", never as "no dev server".
|
|
203
|
+
const [devSnapshot, setDevSnapshot] = useState<DevServerSnapshot | null>(null);
|
|
204
|
+
// Which reload action is in flight, so only that button spins.
|
|
205
|
+
const [reloadingId, setReloadingId] = useState<string | null>(null);
|
|
198
206
|
const [runnerAuthModal, setRunnerAuthModal] = useState<string | null>(null);
|
|
199
207
|
// Vibing-input mode: same expand-on-tap pattern as email login.
|
|
200
208
|
// Tap "Vibing" once → the button reveals an input + Send; that lets
|
|
@@ -226,6 +234,28 @@ export const FeedbackModal: React.FC = () => {
|
|
|
226
234
|
const [showOpenCodeConfig, setShowOpenCodeConfig] = useState(false);
|
|
227
235
|
const mountedRef = useRef(true);
|
|
228
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Ask the machine what its dev server is doing, so the reload actions can
|
|
239
|
+
* be enabled/disabled against reality rather than against a guess.
|
|
240
|
+
*
|
|
241
|
+
* Best-effort and deliberately null-on-failure: null means "we could not
|
|
242
|
+
* ask", which the seam renders as "not connected to a machine yet" — a
|
|
243
|
+
* different sentence from "no dev server is running", because they have
|
|
244
|
+
* different fixes.
|
|
245
|
+
*/
|
|
246
|
+
const refreshDevSnapshot = useCallback(async () => {
|
|
247
|
+
try {
|
|
248
|
+
const client = YaverFeedback.getP2PClient();
|
|
249
|
+
if (!client) {
|
|
250
|
+
setDevSnapshot(null);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
setDevSnapshot(await client.getDevServerStatus());
|
|
254
|
+
} catch {
|
|
255
|
+
setDevSnapshot(null);
|
|
256
|
+
}
|
|
257
|
+
}, []);
|
|
258
|
+
|
|
229
259
|
const loadSelectedMachine = useCallback(async () => {
|
|
230
260
|
const cfg = YaverFeedback.getConfig();
|
|
231
261
|
if (!cfg?.authToken) {
|
|
@@ -448,12 +478,18 @@ export const FeedbackModal: React.FC = () => {
|
|
|
448
478
|
|
|
449
479
|
useEffect(() => {
|
|
450
480
|
if (!visible) return;
|
|
481
|
+
// Poll the dev server alongside the machine + runners. A reload button
|
|
482
|
+
// whose enabled state was decided once, when the sheet opened, goes
|
|
483
|
+
// stale the moment the user starts Metro from another surface — and a
|
|
484
|
+
// stale "no dev server is running" reads as the product being broken.
|
|
485
|
+
void refreshDevSnapshot();
|
|
451
486
|
const interval = setInterval(() => {
|
|
452
487
|
void loadSelectedMachine();
|
|
453
488
|
void loadRunnerStatuses();
|
|
489
|
+
void refreshDevSnapshot();
|
|
454
490
|
}, 5000);
|
|
455
491
|
return () => clearInterval(interval);
|
|
456
|
-
}, [loadRunnerStatuses, loadSelectedMachine, visible]);
|
|
492
|
+
}, [loadRunnerStatuses, loadSelectedMachine, refreshDevSnapshot, visible]);
|
|
457
493
|
|
|
458
494
|
useEffect(() => {
|
|
459
495
|
if (!visible) {
|
|
@@ -542,7 +578,87 @@ export const FeedbackModal: React.FC = () => {
|
|
|
542
578
|
[],
|
|
543
579
|
);
|
|
544
580
|
|
|
545
|
-
// ─── 1.
|
|
581
|
+
// ─── 1. Reload ─────────────────────────────────────────────────────
|
|
582
|
+
//
|
|
583
|
+
// Three actions now, not one: Hot Reload (mode=fast), Full Reload
|
|
584
|
+
// (mode=full — Flutter's hot RESTART), and Rebuild Bundle
|
|
585
|
+
// (/dev/reload-app, the only one that works with Metro down).
|
|
586
|
+
//
|
|
587
|
+
// WHICH of them render, and which are disabled with what reason, is
|
|
588
|
+
// decided by the pure `reloadActions()` seam — never inline here, so the
|
|
589
|
+
// same policy holds on web, Flutter, Unity, Swift and Kotlin. In
|
|
590
|
+
// particular: a production build (`__DEV__ === false`) gets NONE of them.
|
|
591
|
+
|
|
592
|
+
const availableReloadActions = reloadActions(devSnapshot, {
|
|
593
|
+
// __DEV__ is React Native's own build flag. A release bundle sets it
|
|
594
|
+
// false, so a shipped app renders no reload UI at all — which is the
|
|
595
|
+
// point, and is what reloadActions.test.ts pins.
|
|
596
|
+
isDevBuild: typeof __DEV__ !== 'undefined' && __DEV__ === true,
|
|
597
|
+
connected: devSnapshot !== null,
|
|
598
|
+
machineLabel: machineCard.device?.name || undefined,
|
|
599
|
+
includeRebuild: true,
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
const handleReloadAction = useCallback(
|
|
603
|
+
async (reloadAction: ReloadAction) => {
|
|
604
|
+
if (!reloadAction.enabled) {
|
|
605
|
+
// Pressing a disabled action must SAY why. A row that does nothing
|
|
606
|
+
// is the same defect as a spinner that never resolves.
|
|
607
|
+
setToast(reloadAction.disabledReason || 'Reload is unavailable right now.');
|
|
608
|
+
setError(reloadAction.disabledReason || null);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
setReloadingId(reloadAction.id);
|
|
612
|
+
setAction('hot-reloading');
|
|
613
|
+
setError(null);
|
|
614
|
+
setProgress(0);
|
|
615
|
+
setToast(`${reloadAction.label}…`);
|
|
616
|
+
try {
|
|
617
|
+
await loadSelectedMachine();
|
|
618
|
+
const selected = await YaverFeedback.getSelectedRemoteDevice();
|
|
619
|
+
if (!selected) {
|
|
620
|
+
YaverFeedback.showMachinePicker();
|
|
621
|
+
throw new Error('No machine selected. Pick a machine and try again.');
|
|
622
|
+
}
|
|
623
|
+
if (selected.needsAuth) {
|
|
624
|
+
YaverFeedback.showMachinePicker();
|
|
625
|
+
throw new Error('Selected machine needs pairing again.');
|
|
626
|
+
}
|
|
627
|
+
if (!selected.isOnline) {
|
|
628
|
+
throw new Error('Selected machine is offline. Start `yaver serve` on it first.');
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
let ackMessage = `${reloadAction.label} requested.`;
|
|
632
|
+
await runWithReconnect(async (client) => {
|
|
633
|
+
const ack = await client.reloadWithMode(reloadAction.mode, devSnapshot);
|
|
634
|
+
ackMessage = ack.message;
|
|
635
|
+
setToast(ack.message);
|
|
636
|
+
setProgress(0.2);
|
|
637
|
+
});
|
|
638
|
+
setToast(ackMessage);
|
|
639
|
+
if (reloadAction.mode === 'bundle') closeSoon(2500);
|
|
640
|
+
} catch (err: unknown) {
|
|
641
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
642
|
+
setError(message);
|
|
643
|
+
setToast(
|
|
644
|
+
message.toLowerCase().indexOf('session expired') >= 0
|
|
645
|
+
? 'Session expired. Sign in again.'
|
|
646
|
+
: message,
|
|
647
|
+
);
|
|
648
|
+
await loadSelectedMachine();
|
|
649
|
+
setProgress(null);
|
|
650
|
+
} finally {
|
|
651
|
+
if (mountedRef.current) {
|
|
652
|
+
setAction('idle');
|
|
653
|
+
setReloadingId(null);
|
|
654
|
+
void refreshDevSnapshot();
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
},
|
|
658
|
+
[closeSoon, devSnapshot, loadSelectedMachine, refreshDevSnapshot, runWithReconnect],
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
/** Kept for the legacy one-tap path (BlackBox command, chat Reload button). */
|
|
546
662
|
const handleHotReload = useCallback(async () => {
|
|
547
663
|
setAction('hot-reloading');
|
|
548
664
|
setError(null);
|
|
@@ -661,19 +777,36 @@ export const FeedbackModal: React.FC = () => {
|
|
|
661
777
|
try {
|
|
662
778
|
const { Dimensions } = require('react-native');
|
|
663
779
|
const { width, height } = Dimensions.get('window');
|
|
780
|
+
const cfg = YaverFeedback.getConfig();
|
|
781
|
+
const identity = resolveReportIdentity({
|
|
782
|
+
projectName: cfg?.projectName,
|
|
783
|
+
bundleId: cfg?.bundleId,
|
|
784
|
+
surface: cfg?.surface,
|
|
785
|
+
surfaces: cfg?.surfaces,
|
|
786
|
+
stack: cfg?.stack,
|
|
787
|
+
stacks: cfg?.stacks,
|
|
788
|
+
testSurfaces: cfg?.testSurfaces,
|
|
789
|
+
feedbackSdk: cfg?.feedbackSdk,
|
|
790
|
+
feedbackTransport: cfg?.feedbackTransport,
|
|
791
|
+
voiceCapabilities: cfg?.voiceCapabilities,
|
|
792
|
+
sttProvider: cfg?.sttProvider,
|
|
793
|
+
ttsProvider: cfg?.ttsProvider,
|
|
794
|
+
});
|
|
664
795
|
const deviceInfo: DeviceInfo = {
|
|
665
796
|
platform: Platform.OS,
|
|
666
797
|
osVersion: String(Platform.Version),
|
|
667
798
|
model: Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
|
|
668
799
|
screenWidth: width,
|
|
669
800
|
screenHeight: height,
|
|
801
|
+
appName: identity.appName,
|
|
670
802
|
};
|
|
671
803
|
const capturedErrors = YaverFeedback.getCapturedErrors();
|
|
672
804
|
const bundle: FeedbackBundle = {
|
|
673
805
|
metadata: {
|
|
674
806
|
timestamp: new Date().toISOString(),
|
|
675
|
-
|
|
676
|
-
app:
|
|
807
|
+
deviceInfo,
|
|
808
|
+
app: identity.app,
|
|
809
|
+
project: identity.project,
|
|
677
810
|
userNote: '[Screenshot + Fix]',
|
|
678
811
|
},
|
|
679
812
|
screenshots: [path],
|
|
@@ -1115,16 +1248,36 @@ export const FeedbackModal: React.FC = () => {
|
|
|
1115
1248
|
</View>
|
|
1116
1249
|
</View>
|
|
1117
1250
|
|
|
1118
|
-
{/* 1.
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1251
|
+
{/* 1. Reload — Hot / Full / Rebuild Bundle.
|
|
1252
|
+
Rendered from the shared decision seam, so a production
|
|
1253
|
+
build (__DEV__ false) renders nothing here at all, and a
|
|
1254
|
+
blocked action shows greyed WITH its reason underneath
|
|
1255
|
+
rather than vanishing. */}
|
|
1256
|
+
{availableReloadActions.map((reloadAction) => (
|
|
1257
|
+
<View key={reloadAction.id} style={styles.reloadRow}>
|
|
1258
|
+
<ActionRow
|
|
1259
|
+
label={
|
|
1260
|
+
reloadingId === reloadAction.id
|
|
1261
|
+
? `${reloadAction.label}…`
|
|
1262
|
+
: reloadAction.label
|
|
1263
|
+
}
|
|
1264
|
+
tint={reloadAction.id === 'rebuild' ? '#38bdf8' : '#fbbf24'}
|
|
1265
|
+
onPress={() => {
|
|
1266
|
+
void handleReloadAction(reloadAction);
|
|
1267
|
+
}}
|
|
1268
|
+
// Never `disabled` at the Pressable level for a blocked
|
|
1269
|
+
// action: we WANT the tap so we can say why. Only a
|
|
1270
|
+
// genuinely busy modal blocks the press.
|
|
1271
|
+
disabled={busy && reloadingId !== reloadAction.id}
|
|
1272
|
+
busy={reloadingId === reloadAction.id}
|
|
1273
|
+
/>
|
|
1274
|
+
<Text style={styles.reloadHint}>
|
|
1275
|
+
{reloadAction.enabled
|
|
1276
|
+
? reloadAction.hint
|
|
1277
|
+
: reloadAction.disabledReason}
|
|
1278
|
+
</Text>
|
|
1279
|
+
</View>
|
|
1280
|
+
))}
|
|
1128
1281
|
|
|
1129
1282
|
{/* 3. Vibing — expands to an input box on first tap
|
|
1130
1283
|
so the user says WHAT they want to vibe on, just
|
|
@@ -1293,6 +1446,15 @@ const ActionRow: React.FC<ActionRowProps> = ({
|
|
|
1293
1446
|
);
|
|
1294
1447
|
|
|
1295
1448
|
const styles = StyleSheet.create({
|
|
1449
|
+
reloadRow: {
|
|
1450
|
+
gap: 4,
|
|
1451
|
+
},
|
|
1452
|
+
reloadHint: {
|
|
1453
|
+
color: '#8b8b93',
|
|
1454
|
+
fontSize: 11,
|
|
1455
|
+
lineHeight: 15,
|
|
1456
|
+
paddingHorizontal: 4,
|
|
1457
|
+
},
|
|
1296
1458
|
vibeInputRow: {
|
|
1297
1459
|
backgroundColor: 'rgba(129,140,248,0.08)',
|
|
1298
1460
|
borderColor: 'rgba(129,140,248,0.4)',
|