yaver-feedback-react-native 0.7.5 → 0.7.7
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/dist/FeedbackModal.js +29 -7
- package/dist/MachinePickerScreen.js +23 -5
- package/dist/P2PClient.js +50 -49
- package/dist/YaverFeedback.js +36 -3
- package/package.json +1 -1
- package/src/FeedbackModal.tsx +32 -7
- package/src/MachinePickerScreen.tsx +19 -5
- package/src/P2PClient.ts +56 -56
- package/src/YaverFeedback.ts +37 -3
package/dist/FeedbackModal.js
CHANGED
|
@@ -58,9 +58,22 @@ const FeedbackModal = () => {
|
|
|
58
58
|
setAction('idle');
|
|
59
59
|
}
|
|
60
60
|
});
|
|
61
|
+
// Agent streams build / compile progress through the BlackBox
|
|
62
|
+
// SSE command channel as `command: "status"`; YaverFeedback re-emits
|
|
63
|
+
// it as `yaverFeedback:status`. Show the most recent message in the
|
|
64
|
+
// toast so a multi-second rebuild feels like "working" instead of
|
|
65
|
+
// "stuck".
|
|
66
|
+
const statusSub = react_native_1.DeviceEventEmitter.addListener('yaverFeedback:status', (payload) => {
|
|
67
|
+
if (!mountedRef.current)
|
|
68
|
+
return;
|
|
69
|
+
const msg = payload?.message || payload?.phase || '';
|
|
70
|
+
if (msg)
|
|
71
|
+
setToast(msg);
|
|
72
|
+
});
|
|
61
73
|
return () => {
|
|
62
74
|
mountedRef.current = false;
|
|
63
75
|
sub.remove();
|
|
76
|
+
statusSub.remove();
|
|
64
77
|
};
|
|
65
78
|
}, []);
|
|
66
79
|
const closeSoon = (0, react_1.useCallback)((delayMs = 1200) => {
|
|
@@ -92,18 +105,27 @@ const FeedbackModal = () => {
|
|
|
92
105
|
await fn(client);
|
|
93
106
|
}
|
|
94
107
|
catch (err) {
|
|
95
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
const
|
|
108
|
+
const msg = (err instanceof Error ? err.message : String(err)) || '';
|
|
109
|
+
// Avoid unbounded `.*` in regex — on RN 0.81 / Hermes rope
|
|
110
|
+
// strings plus a background SSE reconnect, that pattern has
|
|
111
|
+
// reliably SIGSEGV'd Hermes's string-view flattening path.
|
|
112
|
+
// Split into short, literal-only alternations.
|
|
113
|
+
const lower = msg.toLowerCase();
|
|
114
|
+
const authFailed = lower.indexOf('invalid token') >= 0 ||
|
|
115
|
+
lower.indexOf('unauthor') >= 0 ||
|
|
116
|
+
lower.indexOf(' 401') >= 0 ||
|
|
117
|
+
lower.indexOf(' 403') >= 0;
|
|
101
118
|
if (authFailed) {
|
|
102
119
|
await YaverFeedback_1.YaverFeedback.signOut();
|
|
103
120
|
YaverFeedback_1.YaverFeedback.showLogin();
|
|
104
121
|
throw new Error('Session expired — please sign in again.');
|
|
105
122
|
}
|
|
106
|
-
const transient =
|
|
123
|
+
const transient = lower.indexOf('network request failed') >= 0 ||
|
|
124
|
+
lower.indexOf('econnrefused') >= 0 ||
|
|
125
|
+
lower.indexOf('failed to fetch') >= 0 ||
|
|
126
|
+
lower.indexOf('fetch failed') >= 0 ||
|
|
127
|
+
lower.indexOf('aborted') >= 0 ||
|
|
128
|
+
lower.indexOf('timeout') >= 0;
|
|
107
129
|
if (!transient)
|
|
108
130
|
throw err;
|
|
109
131
|
const ok = await YaverFeedback_1.YaverFeedback.reconnect();
|
|
@@ -94,15 +94,33 @@ const YaverMachinePickerScreen = ({ token, currentDeviceId, onPick, onCancel, })
|
|
|
94
94
|
: device.needsAuth
|
|
95
95
|
? '#f59e0b'
|
|
96
96
|
: '#22c55e';
|
|
97
|
+
// Derive a single short status phrase the user can act on.
|
|
98
|
+
let statusLine = device.platform;
|
|
99
|
+
if (!device.isOnline) {
|
|
100
|
+
statusLine = 'Offline — start `yaver serve` on the Mac';
|
|
101
|
+
}
|
|
102
|
+
else if (device.needsAuth) {
|
|
103
|
+
statusLine =
|
|
104
|
+
'Needs pairing — open the Yaver app to adopt this machine';
|
|
105
|
+
}
|
|
106
|
+
else if (device.runnerDown) {
|
|
107
|
+
statusLine = 'Runner down — restart the coding agent on the Mac';
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
// Happy-path subtitle: platform + optional host/share hint.
|
|
111
|
+
statusLine = device.platform;
|
|
112
|
+
if (device.isGuest && device.hostEmail) {
|
|
113
|
+
statusLine = `${statusLine} • ${device.hostEmail}`;
|
|
114
|
+
}
|
|
115
|
+
else if (device.accessScope === 'shared-scoped') {
|
|
116
|
+
statusLine = `${statusLine} • paylaşılan`;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
97
119
|
return (<react_native_1.TouchableOpacity key={device.deviceId} style={[styles.deviceRow, selected && styles.deviceSelected]} onPress={() => handlePick(device)}>
|
|
98
120
|
<react_native_1.View style={[styles.health, { backgroundColor: healthColor }]}/>
|
|
99
121
|
<react_native_1.View style={{ flex: 1 }}>
|
|
100
122
|
<react_native_1.Text style={styles.deviceName}>{device.name || device.deviceId}</react_native_1.Text>
|
|
101
|
-
<react_native_1.Text style={styles.deviceMeta}>
|
|
102
|
-
{device.platform}
|
|
103
|
-
{device.isGuest && device.hostEmail ? ` • ${device.hostEmail}` : ''}
|
|
104
|
-
{device.accessScope === 'shared-scoped' ? ' • paylaşılan' : ''}
|
|
105
|
-
</react_native_1.Text>
|
|
123
|
+
<react_native_1.Text style={styles.deviceMeta}>{statusLine}</react_native_1.Text>
|
|
106
124
|
</react_native_1.View>
|
|
107
125
|
{selected && <react_native_1.Text style={styles.selectedBadge}>seçili</react_native_1.Text>}
|
|
108
126
|
</react_native_1.TouchableOpacity>);
|
package/dist/P2PClient.js
CHANGED
|
@@ -13,21 +13,28 @@ const react_native_1 = require("react-native");
|
|
|
13
13
|
* running on the host machine".
|
|
14
14
|
*/
|
|
15
15
|
function friendlyReloadError(status, body) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
// Plain .indexOf instead of regex — short literal tests sidestep
|
|
17
|
+
// a Hermes rope-flatten SIGSEGV we saw on RN 0.81 / iOS 18.3.1
|
|
18
|
+
// when the same body was already being processed by a concurrent
|
|
19
|
+
// SSE reconnect loop.
|
|
20
|
+
const lower = (body || '').toLowerCase();
|
|
21
|
+
const hasRefused = lower.indexOf('connection refused') >= 0 ||
|
|
22
|
+
lower.indexOf('econnrefused') >= 0;
|
|
23
|
+
const hasLoopback = lower.indexOf('127.0.0.1') >= 0 || lower.indexOf('localhost') >= 0;
|
|
24
|
+
if (hasRefused && hasLoopback) {
|
|
19
25
|
return ('No dev server running on your machine. ' +
|
|
20
26
|
'Start Metro with `yaver dev start` or use "Screenshot & Fix" instead.');
|
|
21
27
|
}
|
|
22
|
-
if (
|
|
28
|
+
if (lower.indexOf('no dev server') >= 0 ||
|
|
29
|
+
lower.indexOf('not running') >= 0) {
|
|
23
30
|
return 'No dev server running on your machine. Start Metro first.';
|
|
24
31
|
}
|
|
25
|
-
if (status === 403)
|
|
32
|
+
if (status === 401 || status === 403) {
|
|
26
33
|
return 'Agent rejected the session — please sign in again.';
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (status >= 500)
|
|
34
|
+
}
|
|
35
|
+
if (status >= 500) {
|
|
30
36
|
return 'Agent hit an internal error while reloading. Check `yaver logs`.';
|
|
37
|
+
}
|
|
31
38
|
return `Reload failed (${status}).`;
|
|
32
39
|
}
|
|
33
40
|
/**
|
|
@@ -208,52 +215,46 @@ class P2PClient {
|
|
|
208
215
|
* via the BlackBox command channel.
|
|
209
216
|
* @param mode - "dev" for hot reload, "bundle" for native bundle rebuild
|
|
210
217
|
*/
|
|
211
|
-
async reloadApp(mode = '
|
|
212
|
-
//
|
|
213
|
-
// Triggers Metro/Expo HMR synchronously and emits an SSE `reload` event
|
|
214
|
-
// on /dev/events that the FeedbackModal can subscribe to for progress.
|
|
218
|
+
async reloadApp(mode = 'bundle') {
|
|
219
|
+
// Default path: always rebuild a fresh Hermes bundle.
|
|
215
220
|
//
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
// In that case a JS hot-reload is simply not possible — pivot to
|
|
231
|
-
// `/dev/reload-app` in bundle mode, which rebuilds + pushes a fresh
|
|
232
|
-
// Hermes bundle without needing Metro to be alive.
|
|
233
|
-
const primaryText = await primary.text().catch(() => '');
|
|
234
|
-
const noDevServer = primary.status === 400 ||
|
|
235
|
-
primary.status === 404 ||
|
|
236
|
-
primary.status >= 500 ||
|
|
237
|
-
/connection refused|no dev server|not running|ECONNREFUSED/i.test(primaryText);
|
|
238
|
-
if (noDevServer || mode === 'bundle') {
|
|
239
|
-
const nextMode = mode === 'bundle' ? 'bundle' : 'bundle';
|
|
240
|
-
const fallback = await fetch(`${this.baseUrl}/dev/reload-app`, {
|
|
221
|
+
// Rationale: the SDK's common caller is a phone user who's not
|
|
222
|
+
// sitting at their Mac — they're doing vibe coding with an AI agent
|
|
223
|
+
// editing files on the Mac remotely, or they installed the app via
|
|
224
|
+
// TestFlight and there's no Metro running at all. A Metro-based
|
|
225
|
+
// reload would either fail (Metro offline) or — worse — serve a
|
|
226
|
+
// stale bundle because Metro can be slow to re-index fresh edits.
|
|
227
|
+
// Bundle mode always produces the correct bytecode from the current
|
|
228
|
+
// filesystem state, taking ~30–60 s, and hits /dev/reload-app which
|
|
229
|
+
// uses BlackBox SSE to push the fresh bundle URL to the device.
|
|
230
|
+
//
|
|
231
|
+
// Callers who know they want Metro HMR pass `mode='dev'` explicitly;
|
|
232
|
+
// we still honour that. Everything else defaults to bundle.
|
|
233
|
+
if (mode === 'dev') {
|
|
234
|
+
const primary = await fetch(`${this.baseUrl}/dev/reload`, {
|
|
241
235
|
method: 'POST',
|
|
242
|
-
headers: {
|
|
243
|
-
Authorization: `Bearer ${this.authToken}`,
|
|
244
|
-
'Content-Type': 'application/json',
|
|
245
|
-
},
|
|
246
|
-
body: JSON.stringify({ mode: nextMode }),
|
|
236
|
+
headers: { Authorization: `Bearer ${this.authToken}` },
|
|
247
237
|
});
|
|
248
|
-
if (
|
|
249
|
-
|
|
250
|
-
// Give the user something actionable instead of a raw Go error.
|
|
251
|
-
const friendlyFromFallback = friendlyReloadError(fallback.status, fallbackText);
|
|
252
|
-
throw new Error(friendlyFromFallback);
|
|
238
|
+
if (primary.ok) {
|
|
239
|
+
return primary.json().catch(() => ({ ok: true }));
|
|
253
240
|
}
|
|
254
|
-
|
|
241
|
+
// Dev mode failed — fall through to bundle rebuild below rather
|
|
242
|
+
// than surfacing the raw error, so the user never has to know
|
|
243
|
+
// Metro wasn't running.
|
|
244
|
+
}
|
|
245
|
+
const res = await fetch(`${this.baseUrl}/dev/reload-app`, {
|
|
246
|
+
method: 'POST',
|
|
247
|
+
headers: {
|
|
248
|
+
Authorization: `Bearer ${this.authToken}`,
|
|
249
|
+
'Content-Type': 'application/json',
|
|
250
|
+
},
|
|
251
|
+
body: JSON.stringify({ mode: 'bundle' }),
|
|
252
|
+
});
|
|
253
|
+
if (!res.ok) {
|
|
254
|
+
const text = await res.text().catch(() => '');
|
|
255
|
+
throw new Error(friendlyReloadError(res.status, text));
|
|
255
256
|
}
|
|
256
|
-
|
|
257
|
+
return res.json().catch(() => ({ ok: true }));
|
|
257
258
|
}
|
|
258
259
|
/**
|
|
259
260
|
* Open a vibing session on the connected agent. Vibing is the Yaver
|
package/dist/YaverFeedback.js
CHANGED
|
@@ -126,9 +126,13 @@ class YaverFeedback {
|
|
|
126
126
|
}
|
|
127
127
|
});
|
|
128
128
|
}
|
|
129
|
-
// Wire up BlackBox command handlers for reload signals from
|
|
130
|
-
//
|
|
131
|
-
//
|
|
129
|
+
// Wire up BlackBox command handlers for reload + status signals from
|
|
130
|
+
// the agent. Opens an SSE channel the agent uses to:
|
|
131
|
+
// - broadcast reload_bundle so the phone picks up a fresh Hermes
|
|
132
|
+
// bundle after a vibe-coding edit;
|
|
133
|
+
// - stream build/compile progress ("Compiling bundle…", "Pushing
|
|
134
|
+
// assets…", "Done") so the SDK can surface it like a normal
|
|
135
|
+
// "Working…" spinner instead of a silent 60-second freeze.
|
|
132
136
|
if (enabled) {
|
|
133
137
|
BlackBox_1.BlackBox.onCommand((cmd) => {
|
|
134
138
|
if (cmd.command === 'reload') {
|
|
@@ -149,7 +153,36 @@ class YaverFeedback {
|
|
|
149
153
|
YaverFeedback.defaultReloadBundle(bundleUrl, assetsUrl);
|
|
150
154
|
}
|
|
151
155
|
}
|
|
156
|
+
else if (cmd.command === 'status') {
|
|
157
|
+
// Pipe agent progress pings to the UI. The FeedbackModal
|
|
158
|
+
// subscribes to this event and renders the message while a
|
|
159
|
+
// reload / build is in flight.
|
|
160
|
+
const message = typeof cmd.data?.message === 'string'
|
|
161
|
+
? cmd.data.message
|
|
162
|
+
: '';
|
|
163
|
+
const phase = typeof cmd.data?.phase === 'string'
|
|
164
|
+
? cmd.data.phase
|
|
165
|
+
: '';
|
|
166
|
+
const { DeviceEventEmitter } = require('react-native');
|
|
167
|
+
DeviceEventEmitter.emit('yaverFeedback:status', {
|
|
168
|
+
message,
|
|
169
|
+
phase,
|
|
170
|
+
at: Date.now(),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
152
173
|
});
|
|
174
|
+
// NOTE: BlackBox.start() is intentionally NOT auto-called here.
|
|
175
|
+
// An earlier version (0.7.6) did auto-start it, and when the
|
|
176
|
+
// agent was in bootstrap / needs-auth mode — which can happen
|
|
177
|
+
// any time after `yaver serve` restarts before the user pairs —
|
|
178
|
+
// the SSE channel retried with exponential backoff on 401s,
|
|
179
|
+
// producing a tight loop of string concatenation + JSON parse
|
|
180
|
+
// that tripped a Hermes rope-string SIGSEGV on iOS 18.3.1
|
|
181
|
+
// during any other JS-thread regex work (e.g. react-native-
|
|
182
|
+
// view-shot's internal string handling during Screenshot &
|
|
183
|
+
// Fix). Host apps call BlackBox.start() explicitly once they
|
|
184
|
+
// know the agent URL + token are valid (SFMG does this inside
|
|
185
|
+
// its YaverFeedbackWidget after auth).
|
|
153
186
|
}
|
|
154
187
|
// NOTE: We intentionally do NOT hook ErrorUtils.setGlobalHandler().
|
|
155
188
|
// Sentry, Crashlytics, Bugsnag, and other tools all compete for that
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yaver-feedback-react-native",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.7",
|
|
4
4
|
"description": "Visual feedback SDK for Yaver — bug reports, screen recording, voice annotations, and local-first developer workflows",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
package/src/FeedbackModal.tsx
CHANGED
|
@@ -67,9 +67,23 @@ export const FeedbackModal: React.FC = () => {
|
|
|
67
67
|
setAction('idle');
|
|
68
68
|
}
|
|
69
69
|
});
|
|
70
|
+
// Agent streams build / compile progress through the BlackBox
|
|
71
|
+
// SSE command channel as `command: "status"`; YaverFeedback re-emits
|
|
72
|
+
// it as `yaverFeedback:status`. Show the most recent message in the
|
|
73
|
+
// toast so a multi-second rebuild feels like "working" instead of
|
|
74
|
+
// "stuck".
|
|
75
|
+
const statusSub = DeviceEventEmitter.addListener(
|
|
76
|
+
'yaverFeedback:status',
|
|
77
|
+
(payload: { message?: string; phase?: string }) => {
|
|
78
|
+
if (!mountedRef.current) return;
|
|
79
|
+
const msg = payload?.message || payload?.phase || '';
|
|
80
|
+
if (msg) setToast(msg);
|
|
81
|
+
},
|
|
82
|
+
);
|
|
70
83
|
return () => {
|
|
71
84
|
mountedRef.current = false;
|
|
72
85
|
sub.remove();
|
|
86
|
+
statusSub.remove();
|
|
73
87
|
};
|
|
74
88
|
}, []);
|
|
75
89
|
|
|
@@ -102,18 +116,29 @@ export const FeedbackModal: React.FC = () => {
|
|
|
102
116
|
try {
|
|
103
117
|
await fn(client);
|
|
104
118
|
} catch (err) {
|
|
105
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
const
|
|
119
|
+
const msg = (err instanceof Error ? err.message : String(err)) || '';
|
|
120
|
+
// Avoid unbounded `.*` in regex — on RN 0.81 / Hermes rope
|
|
121
|
+
// strings plus a background SSE reconnect, that pattern has
|
|
122
|
+
// reliably SIGSEGV'd Hermes's string-view flattening path.
|
|
123
|
+
// Split into short, literal-only alternations.
|
|
124
|
+
const lower = msg.toLowerCase();
|
|
125
|
+
const authFailed =
|
|
126
|
+
lower.indexOf('invalid token') >= 0 ||
|
|
127
|
+
lower.indexOf('unauthor') >= 0 ||
|
|
128
|
+
lower.indexOf(' 401') >= 0 ||
|
|
129
|
+
lower.indexOf(' 403') >= 0;
|
|
111
130
|
if (authFailed) {
|
|
112
131
|
await YaverFeedback.signOut();
|
|
113
132
|
YaverFeedback.showLogin();
|
|
114
133
|
throw new Error('Session expired — please sign in again.');
|
|
115
134
|
}
|
|
116
|
-
const transient =
|
|
135
|
+
const transient =
|
|
136
|
+
lower.indexOf('network request failed') >= 0 ||
|
|
137
|
+
lower.indexOf('econnrefused') >= 0 ||
|
|
138
|
+
lower.indexOf('failed to fetch') >= 0 ||
|
|
139
|
+
lower.indexOf('fetch failed') >= 0 ||
|
|
140
|
+
lower.indexOf('aborted') >= 0 ||
|
|
141
|
+
lower.indexOf('timeout') >= 0;
|
|
117
142
|
if (!transient) throw err;
|
|
118
143
|
const ok = await YaverFeedback.reconnect();
|
|
119
144
|
if (!ok) throw err;
|
|
@@ -87,6 +87,24 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
|
|
|
87
87
|
: device.needsAuth
|
|
88
88
|
? '#f59e0b'
|
|
89
89
|
: '#22c55e';
|
|
90
|
+
// Derive a single short status phrase the user can act on.
|
|
91
|
+
let statusLine = device.platform;
|
|
92
|
+
if (!device.isOnline) {
|
|
93
|
+
statusLine = 'Offline — start `yaver serve` on the Mac';
|
|
94
|
+
} else if (device.needsAuth) {
|
|
95
|
+
statusLine =
|
|
96
|
+
'Needs pairing — open the Yaver app to adopt this machine';
|
|
97
|
+
} else if (device.runnerDown) {
|
|
98
|
+
statusLine = 'Runner down — restart the coding agent on the Mac';
|
|
99
|
+
} else {
|
|
100
|
+
// Happy-path subtitle: platform + optional host/share hint.
|
|
101
|
+
statusLine = device.platform;
|
|
102
|
+
if (device.isGuest && device.hostEmail) {
|
|
103
|
+
statusLine = `${statusLine} • ${device.hostEmail}`;
|
|
104
|
+
} else if (device.accessScope === 'shared-scoped') {
|
|
105
|
+
statusLine = `${statusLine} • paylaşılan`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
90
108
|
return (
|
|
91
109
|
<TouchableOpacity
|
|
92
110
|
key={device.deviceId}
|
|
@@ -96,11 +114,7 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
|
|
|
96
114
|
<View style={[styles.health, { backgroundColor: healthColor }]} />
|
|
97
115
|
<View style={{ flex: 1 }}>
|
|
98
116
|
<Text style={styles.deviceName}>{device.name || device.deviceId}</Text>
|
|
99
|
-
<Text style={styles.deviceMeta}>
|
|
100
|
-
{device.platform}
|
|
101
|
-
{device.isGuest && device.hostEmail ? ` • ${device.hostEmail}` : ''}
|
|
102
|
-
{device.accessScope === 'shared-scoped' ? ' • paylaşılan' : ''}
|
|
103
|
-
</Text>
|
|
117
|
+
<Text style={styles.deviceMeta}>{statusLine}</Text>
|
|
104
118
|
</View>
|
|
105
119
|
{selected && <Text style={styles.selectedBadge}>seçili</Text>}
|
|
106
120
|
</TouchableOpacity>
|
package/src/P2PClient.ts
CHANGED
|
@@ -18,22 +18,34 @@ export interface FeedbackEvent {
|
|
|
18
18
|
* running on the host machine".
|
|
19
19
|
*/
|
|
20
20
|
function friendlyReloadError(status: number, body: string): string {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
)
|
|
21
|
+
// Plain .indexOf instead of regex — short literal tests sidestep
|
|
22
|
+
// a Hermes rope-flatten SIGSEGV we saw on RN 0.81 / iOS 18.3.1
|
|
23
|
+
// when the same body was already being processed by a concurrent
|
|
24
|
+
// SSE reconnect loop.
|
|
25
|
+
const lower = (body || '').toLowerCase();
|
|
26
|
+
const hasRefused =
|
|
27
|
+
lower.indexOf('connection refused') >= 0 ||
|
|
28
|
+
lower.indexOf('econnrefused') >= 0;
|
|
29
|
+
const hasLoopback =
|
|
30
|
+
lower.indexOf('127.0.0.1') >= 0 || lower.indexOf('localhost') >= 0;
|
|
31
|
+
if (hasRefused && hasLoopback) {
|
|
26
32
|
return (
|
|
27
33
|
'No dev server running on your machine. ' +
|
|
28
34
|
'Start Metro with `yaver dev start` or use "Screenshot & Fix" instead.'
|
|
29
35
|
);
|
|
30
36
|
}
|
|
31
|
-
if (
|
|
37
|
+
if (
|
|
38
|
+
lower.indexOf('no dev server') >= 0 ||
|
|
39
|
+
lower.indexOf('not running') >= 0
|
|
40
|
+
) {
|
|
32
41
|
return 'No dev server running on your machine. Start Metro first.';
|
|
33
42
|
}
|
|
34
|
-
if (status ===
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
if (status === 401 || status === 403) {
|
|
44
|
+
return 'Agent rejected the session — please sign in again.';
|
|
45
|
+
}
|
|
46
|
+
if (status >= 500) {
|
|
47
|
+
return 'Agent hit an internal error while reloading. Check `yaver logs`.';
|
|
48
|
+
}
|
|
37
49
|
return `Reload failed (${status}).`;
|
|
38
50
|
}
|
|
39
51
|
|
|
@@ -244,59 +256,47 @@ export class P2PClient {
|
|
|
244
256
|
* via the BlackBox command channel.
|
|
245
257
|
* @param mode - "dev" for hot reload, "bundle" for native bundle rebuild
|
|
246
258
|
*/
|
|
247
|
-
async reloadApp(mode: 'dev' | 'bundle' = '
|
|
248
|
-
//
|
|
249
|
-
// Triggers Metro/Expo HMR synchronously and emits an SSE `reload` event
|
|
250
|
-
// on /dev/events that the FeedbackModal can subscribe to for progress.
|
|
259
|
+
async reloadApp(mode: 'dev' | 'bundle' = 'bundle'): Promise<{ ok: boolean }> {
|
|
260
|
+
// Default path: always rebuild a fresh Hermes bundle.
|
|
251
261
|
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
// `http://127.0.0.1:<metroPort>/reload` and that connection refuses.
|
|
267
|
-
// In that case a JS hot-reload is simply not possible — pivot to
|
|
268
|
-
// `/dev/reload-app` in bundle mode, which rebuilds + pushes a fresh
|
|
269
|
-
// Hermes bundle without needing Metro to be alive.
|
|
270
|
-
const primaryText = await primary.text().catch(() => '');
|
|
271
|
-
const noDevServer =
|
|
272
|
-
primary.status === 400 ||
|
|
273
|
-
primary.status === 404 ||
|
|
274
|
-
primary.status >= 500 ||
|
|
275
|
-
/connection refused|no dev server|not running|ECONNREFUSED/i.test(primaryText);
|
|
276
|
-
|
|
277
|
-
if (noDevServer || mode === 'bundle') {
|
|
278
|
-
const nextMode = mode === 'bundle' ? 'bundle' : 'bundle';
|
|
279
|
-
const fallback = await fetch(`${this.baseUrl}/dev/reload-app`, {
|
|
262
|
+
// Rationale: the SDK's common caller is a phone user who's not
|
|
263
|
+
// sitting at their Mac — they're doing vibe coding with an AI agent
|
|
264
|
+
// editing files on the Mac remotely, or they installed the app via
|
|
265
|
+
// TestFlight and there's no Metro running at all. A Metro-based
|
|
266
|
+
// reload would either fail (Metro offline) or — worse — serve a
|
|
267
|
+
// stale bundle because Metro can be slow to re-index fresh edits.
|
|
268
|
+
// Bundle mode always produces the correct bytecode from the current
|
|
269
|
+
// filesystem state, taking ~30–60 s, and hits /dev/reload-app which
|
|
270
|
+
// uses BlackBox SSE to push the fresh bundle URL to the device.
|
|
271
|
+
//
|
|
272
|
+
// Callers who know they want Metro HMR pass `mode='dev'` explicitly;
|
|
273
|
+
// we still honour that. Everything else defaults to bundle.
|
|
274
|
+
if (mode === 'dev') {
|
|
275
|
+
const primary = await fetch(`${this.baseUrl}/dev/reload`, {
|
|
280
276
|
method: 'POST',
|
|
281
|
-
headers: {
|
|
282
|
-
Authorization: `Bearer ${this.authToken}`,
|
|
283
|
-
'Content-Type': 'application/json',
|
|
284
|
-
},
|
|
285
|
-
body: JSON.stringify({ mode: nextMode }),
|
|
277
|
+
headers: { Authorization: `Bearer ${this.authToken}` },
|
|
286
278
|
});
|
|
287
|
-
if (
|
|
288
|
-
|
|
289
|
-
// Give the user something actionable instead of a raw Go error.
|
|
290
|
-
const friendlyFromFallback = friendlyReloadError(
|
|
291
|
-
fallback.status,
|
|
292
|
-
fallbackText,
|
|
293
|
-
);
|
|
294
|
-
throw new Error(friendlyFromFallback);
|
|
279
|
+
if (primary.ok) {
|
|
280
|
+
return primary.json().catch(() => ({ ok: true }));
|
|
295
281
|
}
|
|
296
|
-
|
|
282
|
+
// Dev mode failed — fall through to bundle rebuild below rather
|
|
283
|
+
// than surfacing the raw error, so the user never has to know
|
|
284
|
+
// Metro wasn't running.
|
|
297
285
|
}
|
|
298
286
|
|
|
299
|
-
|
|
287
|
+
const res = await fetch(`${this.baseUrl}/dev/reload-app`, {
|
|
288
|
+
method: 'POST',
|
|
289
|
+
headers: {
|
|
290
|
+
Authorization: `Bearer ${this.authToken}`,
|
|
291
|
+
'Content-Type': 'application/json',
|
|
292
|
+
},
|
|
293
|
+
body: JSON.stringify({ mode: 'bundle' }),
|
|
294
|
+
});
|
|
295
|
+
if (!res.ok) {
|
|
296
|
+
const text = await res.text().catch(() => '');
|
|
297
|
+
throw new Error(friendlyReloadError(res.status, text));
|
|
298
|
+
}
|
|
299
|
+
return res.json().catch(() => ({ ok: true }));
|
|
300
300
|
}
|
|
301
301
|
|
|
302
302
|
/**
|
package/src/YaverFeedback.ts
CHANGED
|
@@ -142,9 +142,13 @@ export class YaverFeedback {
|
|
|
142
142
|
});
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
// Wire up BlackBox command handlers for reload signals from
|
|
146
|
-
//
|
|
147
|
-
//
|
|
145
|
+
// Wire up BlackBox command handlers for reload + status signals from
|
|
146
|
+
// the agent. Opens an SSE channel the agent uses to:
|
|
147
|
+
// - broadcast reload_bundle so the phone picks up a fresh Hermes
|
|
148
|
+
// bundle after a vibe-coding edit;
|
|
149
|
+
// - stream build/compile progress ("Compiling bundle…", "Pushing
|
|
150
|
+
// assets…", "Done") so the SDK can surface it like a normal
|
|
151
|
+
// "Working…" spinner instead of a silent 60-second freeze.
|
|
148
152
|
if (enabled) {
|
|
149
153
|
BlackBox.onCommand((cmd) => {
|
|
150
154
|
if (cmd.command === 'reload') {
|
|
@@ -161,8 +165,38 @@ export class YaverFeedback {
|
|
|
161
165
|
} else {
|
|
162
166
|
YaverFeedback.defaultReloadBundle(bundleUrl, assetsUrl);
|
|
163
167
|
}
|
|
168
|
+
} else if (cmd.command === 'status') {
|
|
169
|
+
// Pipe agent progress pings to the UI. The FeedbackModal
|
|
170
|
+
// subscribes to this event and renders the message while a
|
|
171
|
+
// reload / build is in flight.
|
|
172
|
+
const message =
|
|
173
|
+
typeof cmd.data?.message === 'string'
|
|
174
|
+
? (cmd.data.message as string)
|
|
175
|
+
: '';
|
|
176
|
+
const phase =
|
|
177
|
+
typeof cmd.data?.phase === 'string'
|
|
178
|
+
? (cmd.data.phase as string)
|
|
179
|
+
: '';
|
|
180
|
+
const { DeviceEventEmitter } = require('react-native');
|
|
181
|
+
DeviceEventEmitter.emit('yaverFeedback:status', {
|
|
182
|
+
message,
|
|
183
|
+
phase,
|
|
184
|
+
at: Date.now(),
|
|
185
|
+
});
|
|
164
186
|
}
|
|
165
187
|
});
|
|
188
|
+
// NOTE: BlackBox.start() is intentionally NOT auto-called here.
|
|
189
|
+
// An earlier version (0.7.6) did auto-start it, and when the
|
|
190
|
+
// agent was in bootstrap / needs-auth mode — which can happen
|
|
191
|
+
// any time after `yaver serve` restarts before the user pairs —
|
|
192
|
+
// the SSE channel retried with exponential backoff on 401s,
|
|
193
|
+
// producing a tight loop of string concatenation + JSON parse
|
|
194
|
+
// that tripped a Hermes rope-string SIGSEGV on iOS 18.3.1
|
|
195
|
+
// during any other JS-thread regex work (e.g. react-native-
|
|
196
|
+
// view-shot's internal string handling during Screenshot &
|
|
197
|
+
// Fix). Host apps call BlackBox.start() explicitly once they
|
|
198
|
+
// know the agent URL + token are valid (SFMG does this inside
|
|
199
|
+
// its YaverFeedbackWidget after auth).
|
|
166
200
|
}
|
|
167
201
|
|
|
168
202
|
// NOTE: We intentionally do NOT hook ErrorUtils.setGlobalHandler().
|