yaver-feedback-react-native 0.6.1 → 0.7.1
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/Discovery.d.ts +40 -27
- package/dist/Discovery.js +164 -70
- package/dist/FeedbackModal.d.ts +0 -9
- package/dist/FeedbackModal.js +292 -473
- package/dist/MachinePickerScreen.js +5 -1
- package/dist/P2PClient.d.ts +24 -0
- package/dist/P2PClient.js +65 -10
- package/dist/YaverFeedback.d.ts +10 -4
- package/dist/YaverFeedback.js +30 -10
- package/dist/__tests__/YaverFeedback.test.js +2 -35
- package/dist/__tests__/types.test.js +3 -35
- package/dist/auth.d.ts +14 -0
- package/dist/auth.js +36 -3
- package/dist/capture.d.ts +25 -12
- package/dist/capture.js +73 -34
- package/dist/deviceDedup.d.ts +44 -0
- package/dist/deviceDedup.js +184 -0
- package/dist/expo.d.ts +0 -1
- package/dist/expo.js +0 -2
- package/dist/index.d.ts +15 -10
- package/dist/index.js +17 -11
- package/dist/types.d.ts +2 -32
- package/dist/upload.d.ts +8 -3
- package/dist/upload.js +5 -12
- package/package.json +1 -1
- package/src/Discovery.ts +188 -101
- package/src/FeedbackModal.tsx +409 -564
- package/src/MachinePickerScreen.tsx +6 -1
- package/src/P2PClient.ts +66 -12
- package/src/YaverFeedback.ts +27 -12
- package/src/__tests__/YaverFeedback.test.ts +2 -41
- package/src/__tests__/types.test.ts +3 -39
- package/src/auth.ts +46 -3
- package/src/capture.ts +73 -37
- package/src/deviceDedup.ts +184 -0
- package/src/expo.ts +0 -2
- package/src/index.ts +19 -10
- package/src/types.ts +2 -33
- package/src/upload.ts +6 -15
|
@@ -79,7 +79,11 @@ const YaverMachinePickerScreen = ({ token, currentDeviceId, onPick, onCancel, })
|
|
|
79
79
|
};
|
|
80
80
|
const renderDevice = (device) => {
|
|
81
81
|
const selected = device.deviceId === currentDeviceId;
|
|
82
|
-
|
|
82
|
+
// Match the Yaver mobile app: HEARTBEAT_STALE_MS is 90 s. Using
|
|
83
|
+
// 60 s here flashed yellow on a single missed agent beat even
|
|
84
|
+
// though the Mac was up.
|
|
85
|
+
const stale = device.lastHeartbeat > 0 &&
|
|
86
|
+
Date.now() - device.lastHeartbeat > 90000;
|
|
83
87
|
const healthColor = !device.isOnline
|
|
84
88
|
? '#ef4444'
|
|
85
89
|
: device.needsAuth || device.runnerDown || stale
|
package/dist/P2PClient.d.ts
CHANGED
|
@@ -68,6 +68,30 @@ export declare class P2PClient {
|
|
|
68
68
|
reloadApp(mode?: 'dev' | 'bundle'): Promise<{
|
|
69
69
|
ok: boolean;
|
|
70
70
|
}>;
|
|
71
|
+
/**
|
|
72
|
+
* Open a vibing session on the connected agent. Vibing is the Yaver
|
|
73
|
+
* interactive coding-agent flow — `/vibing/execute` creates a task with
|
|
74
|
+
* the project context plus the user's prompt. Returns the task id the
|
|
75
|
+
* caller can poll via `/tasks/{id}` if needed.
|
|
76
|
+
*
|
|
77
|
+
* Requires an owner/CLI/paired token — the `/vibing*` routes do not
|
|
78
|
+
* currently accept SDK-minted tokens. Power users typically drive
|
|
79
|
+
* vibing from Claude Code / the Yaver mobile app; this method is a
|
|
80
|
+
* convenience for the SDK's one-tap bug-report-to-vibing path.
|
|
81
|
+
*/
|
|
82
|
+
vibing(prompt: string, projectPath?: string): Promise<{
|
|
83
|
+
taskId: string;
|
|
84
|
+
}>;
|
|
85
|
+
/**
|
|
86
|
+
* After uploading a feedback bundle with `uploadFeedback`, call this
|
|
87
|
+
* with the returned report id to create a fix task on the agent. The
|
|
88
|
+
* task includes the feedback's screenshots, errors, and (when available)
|
|
89
|
+
* the BlackBox context for the originating device.
|
|
90
|
+
*/
|
|
91
|
+
triggerFix(feedbackId: string): Promise<{
|
|
92
|
+
taskId: string;
|
|
93
|
+
prompt: string;
|
|
94
|
+
}>;
|
|
71
95
|
/** Get the download URL for a build artifact. */
|
|
72
96
|
getArtifactUrl(buildId: string): string;
|
|
73
97
|
/**
|
package/dist/P2PClient.js
CHANGED
|
@@ -62,13 +62,6 @@ class P2PClient {
|
|
|
62
62
|
name: `screenshot_${i}.png`,
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
|
-
if (bundle.audio) {
|
|
66
|
-
formData.append('audio', {
|
|
67
|
-
uri: react_native_1.Platform.OS === 'android' ? `file://${bundle.audio}` : bundle.audio,
|
|
68
|
-
type: 'audio/m4a',
|
|
69
|
-
name: 'voice_note.m4a',
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
65
|
if (bundle.video) {
|
|
73
66
|
formData.append('video', {
|
|
74
67
|
uri: react_native_1.Platform.OS === 'android' ? `file://${bundle.video}` : bundle.video,
|
|
@@ -188,17 +181,79 @@ class P2PClient {
|
|
|
188
181
|
* @param mode - "dev" for hot reload, "bundle" for native bundle rebuild
|
|
189
182
|
*/
|
|
190
183
|
async reloadApp(mode = 'dev') {
|
|
191
|
-
|
|
184
|
+
// Primary path: /dev/reload — same endpoint the Yaver mobile app uses.
|
|
185
|
+
// Triggers Metro/Expo HMR synchronously and emits an SSE `reload` event
|
|
186
|
+
// on /dev/events that the FeedbackModal can subscribe to for progress.
|
|
187
|
+
//
|
|
188
|
+
// Only fall back to /dev/reload-app (the BlackBox-SSE-broadcast path)
|
|
189
|
+
// when the primary path reports "no dev server running" — that mode is
|
|
190
|
+
// really for the mobile app remotely kicking a third-party app, not
|
|
191
|
+
// for the app kicking itself.
|
|
192
|
+
const primary = await fetch(`${this.baseUrl}/dev/reload`, {
|
|
193
|
+
method: 'POST',
|
|
194
|
+
headers: { Authorization: `Bearer ${this.authToken}` },
|
|
195
|
+
});
|
|
196
|
+
if (primary.ok) {
|
|
197
|
+
return primary.json().catch(() => ({ ok: true }));
|
|
198
|
+
}
|
|
199
|
+
if (primary.status >= 500 || primary.status === 404 || mode === 'bundle') {
|
|
200
|
+
const fallback = await fetch(`${this.baseUrl}/dev/reload-app`, {
|
|
201
|
+
method: 'POST',
|
|
202
|
+
headers: {
|
|
203
|
+
Authorization: `Bearer ${this.authToken}`,
|
|
204
|
+
'Content-Type': 'application/json',
|
|
205
|
+
},
|
|
206
|
+
body: JSON.stringify({ mode }),
|
|
207
|
+
});
|
|
208
|
+
if (!fallback.ok) {
|
|
209
|
+
const text = await fallback.text().catch(() => '');
|
|
210
|
+
throw new Error(`[P2PClient] Reload failed (${fallback.status}): ${text}`);
|
|
211
|
+
}
|
|
212
|
+
return fallback.json().catch(() => ({ ok: true }));
|
|
213
|
+
}
|
|
214
|
+
const text = await primary.text().catch(() => '');
|
|
215
|
+
throw new Error(`[P2PClient] Reload failed (${primary.status}): ${text}`);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Open a vibing session on the connected agent. Vibing is the Yaver
|
|
219
|
+
* interactive coding-agent flow — `/vibing/execute` creates a task with
|
|
220
|
+
* the project context plus the user's prompt. Returns the task id the
|
|
221
|
+
* caller can poll via `/tasks/{id}` if needed.
|
|
222
|
+
*
|
|
223
|
+
* Requires an owner/CLI/paired token — the `/vibing*` routes do not
|
|
224
|
+
* currently accept SDK-minted tokens. Power users typically drive
|
|
225
|
+
* vibing from Claude Code / the Yaver mobile app; this method is a
|
|
226
|
+
* convenience for the SDK's one-tap bug-report-to-vibing path.
|
|
227
|
+
*/
|
|
228
|
+
async vibing(prompt, projectPath) {
|
|
229
|
+
const response = await fetch(`${this.baseUrl}/vibing/execute`, {
|
|
192
230
|
method: 'POST',
|
|
193
231
|
headers: {
|
|
194
232
|
Authorization: `Bearer ${this.authToken}`,
|
|
195
233
|
'Content-Type': 'application/json',
|
|
196
234
|
},
|
|
197
|
-
body: JSON.stringify({
|
|
235
|
+
body: JSON.stringify({ prompt, projectPath: projectPath ?? '' }),
|
|
236
|
+
});
|
|
237
|
+
if (!response.ok) {
|
|
238
|
+
const text = await response.text().catch(() => '');
|
|
239
|
+
throw new Error(`[P2PClient] Vibing failed (${response.status}): ${text}`);
|
|
240
|
+
}
|
|
241
|
+
return response.json();
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* After uploading a feedback bundle with `uploadFeedback`, call this
|
|
245
|
+
* with the returned report id to create a fix task on the agent. The
|
|
246
|
+
* task includes the feedback's screenshots, errors, and (when available)
|
|
247
|
+
* the BlackBox context for the originating device.
|
|
248
|
+
*/
|
|
249
|
+
async triggerFix(feedbackId) {
|
|
250
|
+
const response = await fetch(`${this.baseUrl}/feedback/${encodeURIComponent(feedbackId)}/fix`, {
|
|
251
|
+
method: 'POST',
|
|
252
|
+
headers: { Authorization: `Bearer ${this.authToken}` },
|
|
198
253
|
});
|
|
199
254
|
if (!response.ok) {
|
|
200
255
|
const text = await response.text().catch(() => '');
|
|
201
|
-
throw new Error(`[P2PClient]
|
|
256
|
+
throw new Error(`[P2PClient] Fix trigger failed (${response.status}): ${text}`);
|
|
202
257
|
}
|
|
203
258
|
return response.json();
|
|
204
259
|
}
|
package/dist/YaverFeedback.d.ts
CHANGED
|
@@ -19,6 +19,16 @@ export declare class YaverFeedback {
|
|
|
19
19
|
* Sets config.agentUrl and creates P2PClient on success.
|
|
20
20
|
*/
|
|
21
21
|
static discoverAgent(): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Force a fresh Convex lookup for the agent URL — ignoring any
|
|
24
|
+
* cached URL. Callers use this after a P2P request fails
|
|
25
|
+
* (connection refused / timeout) because the most common cause is
|
|
26
|
+
* the Mac's LAN IP rotating. Convex has the fresh one, so we
|
|
27
|
+
* re-query and probe `[quicHost, ...localIps]` in parallel.
|
|
28
|
+
*
|
|
29
|
+
* Returns true when a new URL was adopted.
|
|
30
|
+
*/
|
|
31
|
+
static reconnect(): Promise<boolean>;
|
|
22
32
|
/**
|
|
23
33
|
* Pull a cached session token + selected device from AsyncStorage (populated
|
|
24
34
|
* by the in-SDK login + machine-picker screens). When present the SDK can
|
|
@@ -116,10 +126,6 @@ export declare class YaverFeedback {
|
|
|
116
126
|
* Available after init if agentUrl is set, or after first successful discovery.
|
|
117
127
|
*/
|
|
118
128
|
static getP2PClient(): P2PClient | null;
|
|
119
|
-
/** Returns the current feedback mode. */
|
|
120
|
-
static getFeedbackMode(): 'live' | 'narrated' | 'batch';
|
|
121
|
-
/** Returns the agent commentary level (0-10). */
|
|
122
|
-
static getCommentaryLevel(): number;
|
|
123
129
|
/**
|
|
124
130
|
* Record a business event. Routes through BlackBox so the agent
|
|
125
131
|
* persists it to the analytics ledger (no dashboards — export
|
package/dist/YaverFeedback.js
CHANGED
|
@@ -65,8 +65,6 @@ class YaverFeedback {
|
|
|
65
65
|
config = {
|
|
66
66
|
trigger: 'shake',
|
|
67
67
|
maxRecordingDuration: 120,
|
|
68
|
-
feedbackMode: 'batch',
|
|
69
|
-
agentCommentaryLevel: 0,
|
|
70
68
|
autoLogin: true,
|
|
71
69
|
...cfg,
|
|
72
70
|
};
|
|
@@ -190,6 +188,36 @@ class YaverFeedback {
|
|
|
190
188
|
// Discovery failed — FloatingButton will show disconnected, user can retry
|
|
191
189
|
}
|
|
192
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* Force a fresh Convex lookup for the agent URL — ignoring any
|
|
193
|
+
* cached URL. Callers use this after a P2P request fails
|
|
194
|
+
* (connection refused / timeout) because the most common cause is
|
|
195
|
+
* the Mac's LAN IP rotating. Convex has the fresh one, so we
|
|
196
|
+
* re-query and probe `[quicHost, ...localIps]` in parallel.
|
|
197
|
+
*
|
|
198
|
+
* Returns true when a new URL was adopted.
|
|
199
|
+
*/
|
|
200
|
+
static async reconnect() {
|
|
201
|
+
if (!config || !enabled)
|
|
202
|
+
return false;
|
|
203
|
+
if (!config.authToken || !config.convexUrl)
|
|
204
|
+
return false;
|
|
205
|
+
try {
|
|
206
|
+
const result = await Discovery_1.YaverDiscovery.refreshFromConvex({
|
|
207
|
+
convexUrl: config.convexUrl,
|
|
208
|
+
authToken: config.authToken,
|
|
209
|
+
preferredDeviceId: config.preferredDeviceId,
|
|
210
|
+
});
|
|
211
|
+
if (!result)
|
|
212
|
+
return false;
|
|
213
|
+
config.agentUrl = result.url;
|
|
214
|
+
p2pClient = new P2PClient_1.P2PClient(result.url, config.authToken ?? '');
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
193
221
|
/**
|
|
194
222
|
* Pull a cached session token + selected device from AsyncStorage (populated
|
|
195
223
|
* by the in-SDK login + machine-picker screens). When present the SDK can
|
|
@@ -464,14 +492,6 @@ class YaverFeedback {
|
|
|
464
492
|
static getP2PClient() {
|
|
465
493
|
return p2pClient;
|
|
466
494
|
}
|
|
467
|
-
/** Returns the current feedback mode. */
|
|
468
|
-
static getFeedbackMode() {
|
|
469
|
-
return config?.feedbackMode ?? 'batch';
|
|
470
|
-
}
|
|
471
|
-
/** Returns the agent commentary level (0-10). */
|
|
472
|
-
static getCommentaryLevel() {
|
|
473
|
-
return config?.agentCommentaryLevel ?? 0;
|
|
474
|
-
}
|
|
475
495
|
// ─── One-stop SaaS replacement methods ─────────────────────────
|
|
476
496
|
//
|
|
477
497
|
// These are the three solo-dev SaaS-replacement entry points
|
|
@@ -36,22 +36,18 @@ describe('YaverFeedback', () => {
|
|
|
36
36
|
expect(cfg.agentUrl).toBe('http://localhost:18080');
|
|
37
37
|
expect(cfg.trigger).toBe('shake');
|
|
38
38
|
expect(cfg.maxRecordingDuration).toBe(120);
|
|
39
|
-
expect(cfg.feedbackMode).toBe('batch');
|
|
40
|
-
expect(cfg.agentCommentaryLevel).toBe(0);
|
|
41
39
|
});
|
|
42
40
|
it('respects user-provided values over defaults', () => {
|
|
43
41
|
YaverFeedback_1.YaverFeedback.init({
|
|
44
42
|
authToken: 'tok',
|
|
45
43
|
trigger: 'floating-button',
|
|
46
44
|
maxRecordingDuration: 60,
|
|
47
|
-
|
|
48
|
-
agentCommentaryLevel: 7,
|
|
45
|
+
strictNativeAuth: true,
|
|
49
46
|
});
|
|
50
47
|
const cfg = YaverFeedback_1.YaverFeedback.getConfig();
|
|
51
48
|
expect(cfg.trigger).toBe('floating-button');
|
|
52
49
|
expect(cfg.maxRecordingDuration).toBe(60);
|
|
53
|
-
expect(cfg.
|
|
54
|
-
expect(cfg.agentCommentaryLevel).toBe(7);
|
|
50
|
+
expect(cfg.strictNativeAuth).toBe(true);
|
|
55
51
|
});
|
|
56
52
|
it('with enabled=false sets enabled to false', () => {
|
|
57
53
|
YaverFeedback_1.YaverFeedback.init({
|
|
@@ -110,35 +106,6 @@ describe('YaverFeedback', () => {
|
|
|
110
106
|
expect(cfg.agentUrl).toBe('http://10.0.0.1:18080');
|
|
111
107
|
});
|
|
112
108
|
});
|
|
113
|
-
describe('getFeedbackMode()', () => {
|
|
114
|
-
it('defaults to batch when no config', () => {
|
|
115
|
-
// After any init, feedbackMode defaults to 'batch'
|
|
116
|
-
YaverFeedback_1.YaverFeedback.init({ authToken: 'tok' });
|
|
117
|
-
expect(YaverFeedback_1.YaverFeedback.getFeedbackMode()).toBe('batch');
|
|
118
|
-
});
|
|
119
|
-
it('returns configured mode', () => {
|
|
120
|
-
YaverFeedback_1.YaverFeedback.init({ authToken: 'tok', feedbackMode: 'narrated' });
|
|
121
|
-
expect(YaverFeedback_1.YaverFeedback.getFeedbackMode()).toBe('narrated');
|
|
122
|
-
});
|
|
123
|
-
it('returns live when configured', () => {
|
|
124
|
-
YaverFeedback_1.YaverFeedback.init({ authToken: 'tok', feedbackMode: 'live' });
|
|
125
|
-
expect(YaverFeedback_1.YaverFeedback.getFeedbackMode()).toBe('live');
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
describe('getCommentaryLevel()', () => {
|
|
129
|
-
it('defaults to 0', () => {
|
|
130
|
-
YaverFeedback_1.YaverFeedback.init({ authToken: 'tok' });
|
|
131
|
-
expect(YaverFeedback_1.YaverFeedback.getCommentaryLevel()).toBe(0);
|
|
132
|
-
});
|
|
133
|
-
it('returns configured level', () => {
|
|
134
|
-
YaverFeedback_1.YaverFeedback.init({ authToken: 'tok', agentCommentaryLevel: 5 });
|
|
135
|
-
expect(YaverFeedback_1.YaverFeedback.getCommentaryLevel()).toBe(5);
|
|
136
|
-
});
|
|
137
|
-
it('returns max level when set to 10', () => {
|
|
138
|
-
YaverFeedback_1.YaverFeedback.init({ authToken: 'tok', agentCommentaryLevel: 10 });
|
|
139
|
-
expect(YaverFeedback_1.YaverFeedback.getCommentaryLevel()).toBe(10);
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
109
|
describe('startReport()', () => {
|
|
143
110
|
it('does nothing when not enabled', async () => {
|
|
144
111
|
YaverFeedback_1.YaverFeedback.init({ authToken: 'tok', enabled: false });
|
|
@@ -11,8 +11,6 @@ describe('React Native SDK types', () => {
|
|
|
11
11
|
expect(config.trigger).toBeUndefined();
|
|
12
12
|
expect(config.enabled).toBeUndefined();
|
|
13
13
|
expect(config.maxRecordingDuration).toBeUndefined();
|
|
14
|
-
expect(config.feedbackMode).toBeUndefined();
|
|
15
|
-
expect(config.agentCommentaryLevel).toBeUndefined();
|
|
16
14
|
});
|
|
17
15
|
it('can be constructed with all optional fields', () => {
|
|
18
16
|
const config = {
|
|
@@ -21,12 +19,10 @@ describe('React Native SDK types', () => {
|
|
|
21
19
|
trigger: 'shake',
|
|
22
20
|
enabled: true,
|
|
23
21
|
maxRecordingDuration: 60,
|
|
24
|
-
|
|
25
|
-
agentCommentaryLevel: 7,
|
|
22
|
+
strictNativeAuth: true,
|
|
26
23
|
};
|
|
27
24
|
expect(config.trigger).toBe('shake');
|
|
28
|
-
expect(config.
|
|
29
|
-
expect(config.agentCommentaryLevel).toBe(7);
|
|
25
|
+
expect(config.strictNativeAuth).toBe(true);
|
|
30
26
|
});
|
|
31
27
|
it('accepts all trigger types', () => {
|
|
32
28
|
const triggers = ['shake', 'floating-button', 'manual'];
|
|
@@ -35,13 +31,6 @@ describe('React Native SDK types', () => {
|
|
|
35
31
|
expect(config.trigger).toBe(trigger);
|
|
36
32
|
});
|
|
37
33
|
});
|
|
38
|
-
it('accepts all feedback modes', () => {
|
|
39
|
-
const modes = ['live', 'narrated', 'batch'];
|
|
40
|
-
modes.forEach((mode) => {
|
|
41
|
-
const config = { authToken: 'tok', feedbackMode: mode };
|
|
42
|
-
expect(config.feedbackMode).toBe(mode);
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
34
|
});
|
|
46
35
|
describe('FeedbackBundle', () => {
|
|
47
36
|
it('can be constructed with required fields', () => {
|
|
@@ -67,9 +56,8 @@ describe('React Native SDK types', () => {
|
|
|
67
56
|
expect(bundle.metadata.device.platform).toBe('ios');
|
|
68
57
|
expect(bundle.screenshots).toEqual([]);
|
|
69
58
|
expect(bundle.video).toBeUndefined();
|
|
70
|
-
expect(bundle.audio).toBeUndefined();
|
|
71
59
|
});
|
|
72
|
-
it('can include optional video
|
|
60
|
+
it('can include optional video + screenshots', () => {
|
|
73
61
|
const bundle = {
|
|
74
62
|
metadata: {
|
|
75
63
|
timestamp: '2026-03-24T12:00:00Z',
|
|
@@ -84,11 +72,9 @@ describe('React Native SDK types', () => {
|
|
|
84
72
|
userNote: 'This button does not work',
|
|
85
73
|
},
|
|
86
74
|
video: '/tmp/recording.mp4',
|
|
87
|
-
audio: '/tmp/voice.m4a',
|
|
88
75
|
screenshots: ['/tmp/ss1.png', '/tmp/ss2.png'],
|
|
89
76
|
};
|
|
90
77
|
expect(bundle.video).toBe('/tmp/recording.mp4');
|
|
91
|
-
expect(bundle.audio).toBe('/tmp/voice.m4a');
|
|
92
78
|
expect(bundle.screenshots).toHaveLength(2);
|
|
93
79
|
expect(bundle.metadata.userNote).toBe('This button does not work');
|
|
94
80
|
});
|
|
@@ -187,24 +173,6 @@ describe('React Native SDK types', () => {
|
|
|
187
173
|
});
|
|
188
174
|
});
|
|
189
175
|
});
|
|
190
|
-
describe('AgentCommentary', () => {
|
|
191
|
-
it('has correct structure', () => {
|
|
192
|
-
const commentary = {
|
|
193
|
-
id: 'cmt-1',
|
|
194
|
-
timestamp: '2026-03-24T12:00:00Z',
|
|
195
|
-
message: 'I see a layout issue on the login screen',
|
|
196
|
-
type: 'observation',
|
|
197
|
-
};
|
|
198
|
-
expect(commentary.type).toBe('observation');
|
|
199
|
-
});
|
|
200
|
-
it('accepts all commentary types', () => {
|
|
201
|
-
const types = ['observation', 'suggestion', 'question', 'action'];
|
|
202
|
-
types.forEach((type) => {
|
|
203
|
-
const c = { id: '1', timestamp: 'now', message: 'test', type };
|
|
204
|
-
expect(c.type).toBe(type);
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
});
|
|
208
176
|
describe('FeedbackStreamEvent', () => {
|
|
209
177
|
it('has correct structure', () => {
|
|
210
178
|
const event = {
|
package/dist/auth.d.ts
CHANGED
|
@@ -105,7 +105,17 @@ export interface RemoteDevice {
|
|
|
105
105
|
accessScope: 'owner' | 'shared-scoped' | 'shared-legacy';
|
|
106
106
|
quicHost: string;
|
|
107
107
|
quicPort: number;
|
|
108
|
+
/** Agent HTTP port — preferred over quicPort when present. */
|
|
109
|
+
httpPort?: number;
|
|
108
110
|
publicKey?: string;
|
|
111
|
+
/** Hardware identifier — used for dedup across re-pair events. */
|
|
112
|
+
hwid?: string;
|
|
113
|
+
/**
|
|
114
|
+
* Every LAN IP the agent reported in its last heartbeat. Useful on
|
|
115
|
+
* multi-homed hosts — probing all of them in parallel is the same
|
|
116
|
+
* trick the Yaver mobile app uses to "just work" on the same Wi-Fi.
|
|
117
|
+
*/
|
|
118
|
+
localIps?: string[];
|
|
109
119
|
}
|
|
110
120
|
export interface DeviceList {
|
|
111
121
|
owned: RemoteDevice[];
|
|
@@ -114,5 +124,9 @@ export interface DeviceList {
|
|
|
114
124
|
/**
|
|
115
125
|
* Fetch the set of remote dev machines this user can reach. Splits into
|
|
116
126
|
* owned (user is the host) vs shared (host invited them as a guest).
|
|
127
|
+
*
|
|
128
|
+
* Collapses duplicate rows before splitting — Convex can return multiple
|
|
129
|
+
* rows per physical machine after a re-pair or hostname change, and the
|
|
130
|
+
* raw list used to render as "Kvancs-MacBook-Air.local ×3" in the picker.
|
|
117
131
|
*/
|
|
118
132
|
export declare function listReachableDevices(token: string): Promise<DeviceList>;
|
package/dist/auth.js
CHANGED
|
@@ -346,6 +346,10 @@ async function loginWithEmail(email, password) {
|
|
|
346
346
|
/**
|
|
347
347
|
* Fetch the set of remote dev machines this user can reach. Splits into
|
|
348
348
|
* owned (user is the host) vs shared (host invited them as a guest).
|
|
349
|
+
*
|
|
350
|
+
* Collapses duplicate rows before splitting — Convex can return multiple
|
|
351
|
+
* rows per physical machine after a re-pair or hostname change, and the
|
|
352
|
+
* raw list used to render as "Kvancs-MacBook-Air.local ×3" in the picker.
|
|
349
353
|
*/
|
|
350
354
|
async function listReachableDevices(token) {
|
|
351
355
|
try {
|
|
@@ -355,10 +359,39 @@ async function listReachableDevices(token) {
|
|
|
355
359
|
if (!res.ok)
|
|
356
360
|
return { owned: [], shared: [] };
|
|
357
361
|
const data = await res.json();
|
|
358
|
-
const
|
|
362
|
+
const raw = (data.devices ?? []);
|
|
363
|
+
// Normalise Convex field names → SDK's RemoteDevice shape. The
|
|
364
|
+
// backend returns `localIps`, sometimes the mobile-side mapping
|
|
365
|
+
// surfaces `lanIps` — accept either so the field survives.
|
|
366
|
+
const normalised = raw.map((d) => ({
|
|
367
|
+
deviceId: d.deviceId ?? d.id,
|
|
368
|
+
name: d.name ?? '',
|
|
369
|
+
platform: d.platform ?? d.os ?? '',
|
|
370
|
+
isOnline: !!d.isOnline,
|
|
371
|
+
needsAuth: !!d.needsAuth,
|
|
372
|
+
runnerDown: !!d.runnerDown,
|
|
373
|
+
lastHeartbeat: d.lastHeartbeat ?? 0,
|
|
374
|
+
isGuest: !!d.isGuest,
|
|
375
|
+
hostName: d.hostName,
|
|
376
|
+
hostEmail: d.hostEmail,
|
|
377
|
+
accessScope: d.accessScope ?? 'owner',
|
|
378
|
+
quicHost: d.quicHost ?? d.host ?? '',
|
|
379
|
+
quicPort: d.quicPort ?? 0,
|
|
380
|
+
httpPort: d.httpPort ?? d.quicPort,
|
|
381
|
+
publicKey: d.publicKey,
|
|
382
|
+
hwid: d.hardwareId ?? d.hwid,
|
|
383
|
+
localIps: Array.isArray(d.localIps)
|
|
384
|
+
? d.localIps
|
|
385
|
+
: Array.isArray(d.lanIps)
|
|
386
|
+
? d.lanIps
|
|
387
|
+
: undefined,
|
|
388
|
+
}));
|
|
389
|
+
// Lazy require so Jest + tree-shakers don't choke on a circular import.
|
|
390
|
+
const { collapseRemoteDevices } = require('./deviceDedup');
|
|
391
|
+
const deduped = collapseRemoteDevices(normalised);
|
|
359
392
|
return {
|
|
360
|
-
owned:
|
|
361
|
-
shared:
|
|
393
|
+
owned: deduped.filter((d) => !d.isGuest),
|
|
394
|
+
shared: deduped.filter((d) => d.isGuest),
|
|
362
395
|
};
|
|
363
396
|
}
|
|
364
397
|
catch {
|
package/dist/capture.d.ts
CHANGED
|
@@ -1,27 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Screen capture
|
|
2
|
+
* Screen capture helpers — screenshot + video recording.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Peer deps (all optional — loaded lazily):
|
|
5
|
+
* - `react-native-view-shot` — screenshot
|
|
6
|
+
* - `react-native-record-screen` — video recording (iOS ReplayKit /
|
|
7
|
+
* Android MediaProjection)
|
|
8
|
+
*
|
|
9
|
+
* Each helper surfaces a clear error if the module is missing so a host
|
|
10
|
+
* app knows exactly which peer dep to add. Audio-note / voice-command
|
|
11
|
+
* recording was removed in 0.7.0 — see FeedbackModal for the new
|
|
12
|
+
* 5-button surface.
|
|
8
13
|
*/
|
|
9
14
|
/**
|
|
10
15
|
* Capture the current screen as a PNG image.
|
|
11
16
|
* Requires `react-native-view-shot` to be installed.
|
|
12
|
-
*
|
|
17
|
+
*
|
|
18
|
+
* Note: the feedback modal should hide itself *before* calling this so the
|
|
19
|
+
* screenshot contains the underlying app state (the actual bug), not the
|
|
20
|
+
* modal. See `FeedbackModal.handleScreenshotForFix`.
|
|
13
21
|
*/
|
|
14
22
|
export declare function captureScreenshot(): Promise<string>;
|
|
15
23
|
/**
|
|
16
|
-
* Start recording
|
|
17
|
-
*
|
|
24
|
+
* Start a screen-recording session. Requires
|
|
25
|
+
* `react-native-record-screen` as a peer dep.
|
|
26
|
+
*
|
|
27
|
+
* The user must grant the iOS ReplayKit / Android MediaProjection
|
|
28
|
+
* permission the first time; the prompt is shown by the native module,
|
|
29
|
+
* not the SDK.
|
|
18
30
|
*/
|
|
19
|
-
export declare function
|
|
31
|
+
export declare function startVideoRecording(): Promise<void>;
|
|
20
32
|
/**
|
|
21
|
-
* Stop the current
|
|
22
|
-
* @returns Object with the file path and duration in seconds.
|
|
33
|
+
* Stop the current video recording and return the on-device file path.
|
|
23
34
|
*/
|
|
24
|
-
export declare function
|
|
35
|
+
export declare function stopVideoRecording(): Promise<{
|
|
25
36
|
path: string;
|
|
26
37
|
duration: number;
|
|
27
38
|
}>;
|
|
39
|
+
/** Whether a video recording is currently active. */
|
|
40
|
+
export declare function isVideoRecording(): boolean;
|
package/dist/capture.js
CHANGED
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Screen capture
|
|
3
|
+
* Screen capture helpers — screenshot + video recording.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Peer deps (all optional — loaded lazily):
|
|
6
|
+
* - `react-native-view-shot` — screenshot
|
|
7
|
+
* - `react-native-record-screen` — video recording (iOS ReplayKit /
|
|
8
|
+
* Android MediaProjection)
|
|
9
|
+
*
|
|
10
|
+
* Each helper surfaces a clear error if the module is missing so a host
|
|
11
|
+
* app knows exactly which peer dep to add. Audio-note / voice-command
|
|
12
|
+
* recording was removed in 0.7.0 — see FeedbackModal for the new
|
|
13
|
+
* 5-button surface.
|
|
9
14
|
*/
|
|
10
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
16
|
exports.captureScreenshot = captureScreenshot;
|
|
12
|
-
exports.
|
|
13
|
-
exports.
|
|
14
|
-
|
|
17
|
+
exports.startVideoRecording = startVideoRecording;
|
|
18
|
+
exports.stopVideoRecording = stopVideoRecording;
|
|
19
|
+
exports.isVideoRecording = isVideoRecording;
|
|
15
20
|
/**
|
|
16
21
|
* Capture the current screen as a PNG image.
|
|
17
22
|
* Requires `react-native-view-shot` to be installed.
|
|
18
|
-
*
|
|
23
|
+
*
|
|
24
|
+
* Note: the feedback modal should hide itself *before* calling this so the
|
|
25
|
+
* screenshot contains the underlying app state (the actual bug), not the
|
|
26
|
+
* modal. See `FeedbackModal.handleScreenshotForFix`.
|
|
19
27
|
*/
|
|
20
28
|
async function captureScreenshot() {
|
|
21
29
|
try {
|
|
@@ -27,48 +35,79 @@ async function captureScreenshot() {
|
|
|
27
35
|
return uri;
|
|
28
36
|
}
|
|
29
37
|
catch (err) {
|
|
30
|
-
throw new Error('[YaverFeedback] Screenshot capture failed.
|
|
38
|
+
throw new Error('[YaverFeedback] Screenshot capture failed. Install react-native-view-shot as a peer dep. ' +
|
|
31
39
|
String(err));
|
|
32
40
|
}
|
|
33
41
|
}
|
|
42
|
+
let videoRecorderModule = null;
|
|
43
|
+
let videoRecordingActive = false;
|
|
34
44
|
/**
|
|
35
|
-
* Start recording
|
|
36
|
-
*
|
|
45
|
+
* Start a screen-recording session. Requires
|
|
46
|
+
* `react-native-record-screen` as a peer dep.
|
|
47
|
+
*
|
|
48
|
+
* The user must grant the iOS ReplayKit / Android MediaProjection
|
|
49
|
+
* permission the first time; the prompt is shown by the native module,
|
|
50
|
+
* not the SDK.
|
|
37
51
|
*/
|
|
38
|
-
async function
|
|
52
|
+
async function startVideoRecording() {
|
|
53
|
+
if (videoRecordingActive) {
|
|
54
|
+
throw new Error('[YaverFeedback] A video recording is already in progress.');
|
|
55
|
+
}
|
|
39
56
|
try {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
57
|
+
videoRecorderModule = require('react-native-record-screen').default ??
|
|
58
|
+
require('react-native-record-screen');
|
|
59
|
+
if (typeof videoRecorderModule.startRecording !== 'function') {
|
|
60
|
+
throw new Error('react-native-record-screen missing startRecording()');
|
|
61
|
+
}
|
|
62
|
+
const result = await videoRecorderModule.startRecording({
|
|
63
|
+
mic: false,
|
|
64
|
+
width: 720,
|
|
65
|
+
bitrate: 1024 * 1000,
|
|
66
|
+
});
|
|
67
|
+
if (result && result.status && result.status !== 'success') {
|
|
68
|
+
throw new Error(`startRecording returned ${result.status}`);
|
|
69
|
+
}
|
|
70
|
+
videoRecordingActive = true;
|
|
43
71
|
}
|
|
44
72
|
catch (err) {
|
|
45
|
-
|
|
46
|
-
|
|
73
|
+
videoRecorderModule = null;
|
|
74
|
+
videoRecordingActive = false;
|
|
75
|
+
throw new Error('[YaverFeedback] Could not start screen recording. Install react-native-record-screen. ' +
|
|
47
76
|
String(err));
|
|
48
77
|
}
|
|
49
78
|
}
|
|
50
79
|
/**
|
|
51
|
-
* Stop the current
|
|
52
|
-
* @returns Object with the file path and duration in seconds.
|
|
80
|
+
* Stop the current video recording and return the on-device file path.
|
|
53
81
|
*/
|
|
54
|
-
async function
|
|
55
|
-
if (!
|
|
56
|
-
throw new Error('[YaverFeedback] No
|
|
82
|
+
async function stopVideoRecording() {
|
|
83
|
+
if (!videoRecordingActive || !videoRecorderModule) {
|
|
84
|
+
throw new Error('[YaverFeedback] No video recording in progress.');
|
|
57
85
|
}
|
|
58
86
|
try {
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
87
|
+
const res = await videoRecorderModule.stopRecording();
|
|
88
|
+
videoRecordingActive = false;
|
|
89
|
+
const path = typeof res === 'string'
|
|
90
|
+
? res
|
|
91
|
+
: res?.result?.outputURL ??
|
|
92
|
+
res?.outputURL ??
|
|
93
|
+
res?.uri ??
|
|
94
|
+
'';
|
|
95
|
+
const durationMs = typeof res?.result?.duration === 'number'
|
|
96
|
+
? res.result.duration
|
|
97
|
+
: typeof res?.duration === 'number'
|
|
98
|
+
? res.duration
|
|
99
|
+
: 0;
|
|
100
|
+
if (!path) {
|
|
101
|
+
throw new Error('stopRecording() returned no file path');
|
|
102
|
+
}
|
|
68
103
|
return { path, duration: durationMs / 1000 };
|
|
69
104
|
}
|
|
70
105
|
catch (err) {
|
|
71
|
-
|
|
72
|
-
throw new Error('[YaverFeedback] Failed to stop
|
|
106
|
+
videoRecordingActive = false;
|
|
107
|
+
throw new Error('[YaverFeedback] Failed to stop screen recording. ' + String(err));
|
|
73
108
|
}
|
|
74
109
|
}
|
|
110
|
+
/** Whether a video recording is currently active. */
|
|
111
|
+
function isVideoRecording() {
|
|
112
|
+
return videoRecordingActive;
|
|
113
|
+
}
|