yaver-feedback-react-native 0.8.13 → 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 +477 -66
- package/dist/MachinePickerScreen.js +14 -5
- package/dist/P2PClient.d.ts +94 -1
- package/dist/P2PClient.js +281 -2
- package/dist/ShakeDetector.js +2 -0
- package/dist/VibeChatScreen.d.ts +6 -1
- package/dist/VibeChatScreen.js +204 -1
- 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/capture.d.ts +6 -0
- package/dist/capture.js +53 -0
- 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 +215 -3
- package/dist/voice.d.ts +61 -0
- package/dist/voice.js +246 -0
- package/package.json +14 -3
- package/src/BlackBox.ts +10 -1
- package/src/DeployPanel.tsx +74 -0
- package/src/Discovery.ts +13 -2
- package/src/FeedbackModal.tsx +563 -87
- package/src/MachinePickerScreen.tsx +12 -3
- package/src/P2PClient.ts +314 -2
- package/src/ShakeDetector.ts +1 -0
- package/src/VibeChatScreen.tsx +219 -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/capture.ts +51 -0
- package/src/index.ts +21 -0
- package/src/reloadActions.ts +273 -0
- package/src/storeShots.ts +189 -0
- package/src/types.ts +217 -3
- package/src/voice.ts +270 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Runtime shake toggling + init() idempotency.
|
|
3
|
+
//
|
|
4
|
+
// Host apps put the feedback SDK behind a settings switch, which means init /
|
|
5
|
+
// enable / disable run repeatedly in one process. Two things used to break
|
|
6
|
+
// there:
|
|
7
|
+
//
|
|
8
|
+
// 1. There was no way to turn the shake catcher off short of
|
|
9
|
+
// setEnabled(false) (which also stops the flight recorder and the agent
|
|
10
|
+
// command channel) or a full re-init. `trigger` is only read inside
|
|
11
|
+
// init(). setShakeEnabled() fills that gap.
|
|
12
|
+
//
|
|
13
|
+
// 2. init() registered a BlackBox command handler and threw away the
|
|
14
|
+
// unsubscribe, so each re-init stacked another. Two inits meant two
|
|
15
|
+
// reloads per agent command; destroy() never cleared them either.
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
const YaverFeedback_1 = require("../YaverFeedback");
|
|
18
|
+
const BlackBox_1 = require("../BlackBox");
|
|
19
|
+
const mockShakeStart = jest.fn();
|
|
20
|
+
const mockShakeStop = jest.fn();
|
|
21
|
+
jest.mock('../ShakeDetector', () => ({
|
|
22
|
+
ShakeDetector: jest.fn().mockImplementation(() => ({
|
|
23
|
+
start: mockShakeStart,
|
|
24
|
+
stop: mockShakeStop,
|
|
25
|
+
})),
|
|
26
|
+
}));
|
|
27
|
+
jest.mock('react-native', () => ({
|
|
28
|
+
Platform: { OS: 'ios' },
|
|
29
|
+
DeviceEventEmitter: { emit: jest.fn(), addListener: jest.fn() },
|
|
30
|
+
NativeModules: {},
|
|
31
|
+
}));
|
|
32
|
+
const mockFetch = jest.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve({}) }));
|
|
33
|
+
global.fetch = mockFetch;
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
jest.clearAllMocks();
|
|
36
|
+
YaverFeedback_1.YaverFeedback.destroy();
|
|
37
|
+
});
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
YaverFeedback_1.YaverFeedback.destroy();
|
|
40
|
+
});
|
|
41
|
+
describe('setShakeEnabled', () => {
|
|
42
|
+
it('arms the shake listener on init when trigger is shake', () => {
|
|
43
|
+
YaverFeedback_1.YaverFeedback.init({ trigger: 'shake', agentUrl: 'http://x:18080', authToken: 't' });
|
|
44
|
+
expect(YaverFeedback_1.YaverFeedback.isShakeEnabled()).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
it('disarms the listener without disabling the SDK', () => {
|
|
47
|
+
YaverFeedback_1.YaverFeedback.init({ trigger: 'shake', agentUrl: 'http://x:18080', authToken: 't' });
|
|
48
|
+
YaverFeedback_1.YaverFeedback.setShakeEnabled(false);
|
|
49
|
+
expect(YaverFeedback_1.YaverFeedback.isShakeEnabled()).toBe(false);
|
|
50
|
+
expect(mockShakeStop).toHaveBeenCalled();
|
|
51
|
+
// The point of a separate toggle: the rest of the SDK stays up.
|
|
52
|
+
expect(YaverFeedback_1.YaverFeedback.isEnabled()).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
it('re-arms the listener', () => {
|
|
55
|
+
YaverFeedback_1.YaverFeedback.init({ trigger: 'shake', agentUrl: 'http://x:18080', authToken: 't' });
|
|
56
|
+
YaverFeedback_1.YaverFeedback.setShakeEnabled(false);
|
|
57
|
+
YaverFeedback_1.YaverFeedback.setShakeEnabled(true);
|
|
58
|
+
expect(YaverFeedback_1.YaverFeedback.isShakeEnabled()).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
it('is idempotent — repeated calls do not stack detectors', () => {
|
|
61
|
+
YaverFeedback_1.YaverFeedback.init({ trigger: 'shake', agentUrl: 'http://x:18080', authToken: 't' });
|
|
62
|
+
mockShakeStart.mockClear();
|
|
63
|
+
YaverFeedback_1.YaverFeedback.setShakeEnabled(true);
|
|
64
|
+
YaverFeedback_1.YaverFeedback.setShakeEnabled(true);
|
|
65
|
+
expect(mockShakeStart).not.toHaveBeenCalled();
|
|
66
|
+
expect(YaverFeedback_1.YaverFeedback.isShakeEnabled()).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
it('persists onto config so a later enable does not resurrect the listener', () => {
|
|
69
|
+
YaverFeedback_1.YaverFeedback.init({ trigger: 'shake', agentUrl: 'http://x:18080', authToken: 't' });
|
|
70
|
+
YaverFeedback_1.YaverFeedback.setShakeEnabled(false);
|
|
71
|
+
YaverFeedback_1.YaverFeedback.setEnabled(false);
|
|
72
|
+
YaverFeedback_1.YaverFeedback.setEnabled(true);
|
|
73
|
+
// The user said no shake. Cycling the master switch must not undo that.
|
|
74
|
+
expect(YaverFeedback_1.YaverFeedback.isShakeEnabled()).toBe(false);
|
|
75
|
+
expect(YaverFeedback_1.YaverFeedback.getConfig()?.disableShakeGesture).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
it('does not arm the listener for a non-shake trigger', () => {
|
|
78
|
+
YaverFeedback_1.YaverFeedback.init({ trigger: 'manual', agentUrl: 'http://x:18080', authToken: 't' });
|
|
79
|
+
expect(YaverFeedback_1.YaverFeedback.isShakeEnabled()).toBe(false);
|
|
80
|
+
YaverFeedback_1.YaverFeedback.setShakeEnabled(true);
|
|
81
|
+
expect(YaverFeedback_1.YaverFeedback.isShakeEnabled()).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
it('no-ops before init rather than throwing', () => {
|
|
84
|
+
expect(() => YaverFeedback_1.YaverFeedback.setShakeEnabled(true)).not.toThrow();
|
|
85
|
+
expect(YaverFeedback_1.YaverFeedback.isShakeEnabled()).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
describe('init() command-handler registration', () => {
|
|
89
|
+
it('does not stack command handlers across re-inits', () => {
|
|
90
|
+
const cfg = { trigger: 'shake', agentUrl: 'http://x:18080', authToken: 't' };
|
|
91
|
+
YaverFeedback_1.YaverFeedback.init(cfg);
|
|
92
|
+
YaverFeedback_1.YaverFeedback.init(cfg);
|
|
93
|
+
YaverFeedback_1.YaverFeedback.init(cfg);
|
|
94
|
+
// Three inits, one live handler. Before the fix this array grew by one
|
|
95
|
+
// each time, so a single agent 'reload' fired three reloads.
|
|
96
|
+
expect(BlackBox_1.BlackBox.commandHandlers.length).toBe(1);
|
|
97
|
+
});
|
|
98
|
+
it('fires a reload exactly once per command after repeated inits', () => {
|
|
99
|
+
// The user-visible symptom the count above stands in for.
|
|
100
|
+
const onReload = jest.fn();
|
|
101
|
+
const cfg = { trigger: 'shake', agentUrl: 'http://x:18080', authToken: 't', onReload };
|
|
102
|
+
YaverFeedback_1.YaverFeedback.init(cfg);
|
|
103
|
+
YaverFeedback_1.YaverFeedback.init(cfg);
|
|
104
|
+
for (const h of BlackBox_1.BlackBox.commandHandlers)
|
|
105
|
+
h({ command: 'reload' });
|
|
106
|
+
expect(onReload).toHaveBeenCalledTimes(1);
|
|
107
|
+
});
|
|
108
|
+
it('destroy() unregisters the command handler', () => {
|
|
109
|
+
YaverFeedback_1.YaverFeedback.init({ trigger: 'shake', agentUrl: 'http://x:18080', authToken: 't' });
|
|
110
|
+
const before = BlackBox_1.BlackBox.commandHandlers.length;
|
|
111
|
+
expect(before).toBeGreaterThan(0);
|
|
112
|
+
YaverFeedback_1.YaverFeedback.destroy();
|
|
113
|
+
expect(BlackBox_1.BlackBox.commandHandlers.length).toBe(before - 1);
|
|
114
|
+
});
|
|
115
|
+
it('destroy() then init() leaves exactly one handler', () => {
|
|
116
|
+
YaverFeedback_1.YaverFeedback.init({ trigger: 'shake', agentUrl: 'http://x:18080', authToken: 't' });
|
|
117
|
+
YaverFeedback_1.YaverFeedback.destroy();
|
|
118
|
+
YaverFeedback_1.YaverFeedback.init({ trigger: 'shake', agentUrl: 'http://x:18080', authToken: 't' });
|
|
119
|
+
expect(BlackBox_1.BlackBox.commandHandlers.length).toBe(1);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Target-device selection contract.
|
|
3
|
+
//
|
|
4
|
+
// This exists because pickTargetDevice used to silently reroute: when the
|
|
5
|
+
// user's `preferredDeviceId` was absent from the list — or present but
|
|
6
|
+
// carrying an empty `quicHost` — the preference block fell through to the
|
|
7
|
+
// generic "first fresh machine" search. The user picked their Mac mini and
|
|
8
|
+
// the fix landed on their laptop, with no error on either end.
|
|
9
|
+
//
|
|
10
|
+
// The contract now: an explicit preference is honoured by id alone, or the
|
|
11
|
+
// pick fails. Not connecting is a better failure than connecting to the
|
|
12
|
+
// wrong host.
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
const device_1 = require("../_core/device");
|
|
15
|
+
const constants_1 = require("../_core/constants");
|
|
16
|
+
const NOW = 1700000000000;
|
|
17
|
+
function device(over) {
|
|
18
|
+
return {
|
|
19
|
+
name: over.deviceId,
|
|
20
|
+
platform: 'darwin',
|
|
21
|
+
isOnline: true,
|
|
22
|
+
lastHeartbeat: NOW,
|
|
23
|
+
quicHost: '100.64.0.1',
|
|
24
|
+
quicPort: 18080,
|
|
25
|
+
...over,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
jest.spyOn(Date, 'now').mockReturnValue(NOW);
|
|
30
|
+
});
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
jest.restoreAllMocks();
|
|
33
|
+
});
|
|
34
|
+
describe('pickTargetDevice — explicit preference', () => {
|
|
35
|
+
it('returns the preferred device when it is fresh', () => {
|
|
36
|
+
const macmini = device({ deviceId: 'macmini' });
|
|
37
|
+
const laptop = device({ deviceId: 'laptop' });
|
|
38
|
+
expect((0, device_1.pickTargetDevice)([laptop, macmini], 'macmini')).toBe(macmini);
|
|
39
|
+
});
|
|
40
|
+
it('returns the preferred device even when it is STALE, rather than rerouting', () => {
|
|
41
|
+
const macmini = device({
|
|
42
|
+
deviceId: 'macmini',
|
|
43
|
+
lastHeartbeat: NOW - (constants_1.HEARTBEAT_STALE_MS + 1),
|
|
44
|
+
});
|
|
45
|
+
const laptop = device({ deviceId: 'laptop' });
|
|
46
|
+
expect((0, device_1.isDeviceFresh)(macmini)).toBe(false);
|
|
47
|
+
expect((0, device_1.pickTargetDevice)([laptop, macmini], 'macmini')).toBe(macmini);
|
|
48
|
+
});
|
|
49
|
+
it('returns the preferred device even with NO quicHost — relay addresses it by id', () => {
|
|
50
|
+
// This is the off-LAN shape: no LAN address is published, and the relay
|
|
51
|
+
// route is `<relay>/d/<deviceId>`. An empty quicHost is not a reason to
|
|
52
|
+
// reroute; it is the normal remote case.
|
|
53
|
+
const macmini = device({ deviceId: 'macmini', quicHost: '' });
|
|
54
|
+
const laptop = device({ deviceId: 'laptop' });
|
|
55
|
+
expect((0, device_1.pickTargetDevice)([laptop, macmini], 'macmini')).toBe(macmini);
|
|
56
|
+
});
|
|
57
|
+
it('returns null when the preferred id is absent — never falls through to another machine', () => {
|
|
58
|
+
const laptop = device({ deviceId: 'laptop' });
|
|
59
|
+
const desktop = device({ deviceId: 'desktop' });
|
|
60
|
+
expect((0, device_1.pickTargetDevice)([laptop, desktop], 'macmini')).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
it('returns null for an unknown preference even when exactly one healthy machine exists', () => {
|
|
63
|
+
// The tempting "well, there's only one, just use it" case. Still wrong:
|
|
64
|
+
// the user asked for a specific host.
|
|
65
|
+
expect((0, device_1.pickTargetDevice)([device({ deviceId: 'laptop' })], 'macmini')).toBeNull();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe('pickTargetDevice — no preference', () => {
|
|
69
|
+
it('prefers a fresh device with a quicHost over a stale one', () => {
|
|
70
|
+
const stale = device({
|
|
71
|
+
deviceId: 'stale',
|
|
72
|
+
lastHeartbeat: NOW - (constants_1.HEARTBEAT_STALE_MS + 1),
|
|
73
|
+
});
|
|
74
|
+
const fresh = device({ deviceId: 'fresh' });
|
|
75
|
+
expect((0, device_1.pickTargetDevice)([stale, fresh])).toBe(fresh);
|
|
76
|
+
});
|
|
77
|
+
it('falls back to an online device with a quicHost when none are fresh', () => {
|
|
78
|
+
const offline = device({ deviceId: 'offline', isOnline: false });
|
|
79
|
+
const online = device({
|
|
80
|
+
deviceId: 'online',
|
|
81
|
+
lastHeartbeat: NOW - (constants_1.HEARTBEAT_STALE_MS + 1),
|
|
82
|
+
});
|
|
83
|
+
expect((0, device_1.pickTargetDevice)([offline, online])).toBe(online);
|
|
84
|
+
});
|
|
85
|
+
it('returns null for an empty list', () => {
|
|
86
|
+
expect((0, device_1.pickTargetDevice)([], 'macmini')).toBeNull();
|
|
87
|
+
expect((0, device_1.pickTargetDevice)([])).toBeNull();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const reloadActions_1 = require("../reloadActions");
|
|
4
|
+
const DEV = { isDevBuild: true, connected: true };
|
|
5
|
+
describe('reloadActions — the production guard', () => {
|
|
6
|
+
// THIS IS THE GUARD. Prove it by breaking it: flip `if (!opts.isDevBuild)`
|
|
7
|
+
// in reloadActions.ts to `if (opts.isDevBuild)` and this test fails while
|
|
8
|
+
// every other test in this file still passes.
|
|
9
|
+
it('returns NOTHING in a production build, even with a healthy dev server', () => {
|
|
10
|
+
const actions = (0, reloadActions_1.reloadActions)({ running: true, framework: 'vite' }, { isDevBuild: false, connected: true, includeRebuild: true });
|
|
11
|
+
expect(actions).toEqual([]);
|
|
12
|
+
});
|
|
13
|
+
it('returns actions in a dev build', () => {
|
|
14
|
+
const actions = (0, reloadActions_1.reloadActions)({ running: true, framework: 'vite' }, DEV);
|
|
15
|
+
expect(actions.map((a) => a.id)).toEqual(['hot', 'full']);
|
|
16
|
+
expect(actions.every((a) => a.enabled)).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
describe('reloadFrameworkFamily', () => {
|
|
20
|
+
it.each([
|
|
21
|
+
['flutter', 'flutter'],
|
|
22
|
+
['expo', 'react-native'],
|
|
23
|
+
['react-native', 'react-native'],
|
|
24
|
+
['vite', 'web'],
|
|
25
|
+
['nextjs', 'web'],
|
|
26
|
+
['', 'unknown'],
|
|
27
|
+
['godot', 'unknown'],
|
|
28
|
+
])('maps %s → %s', (framework, family) => {
|
|
29
|
+
expect((0, reloadActions_1.reloadFrameworkFamily)(framework)).toBe(family);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe('per-stack labels', () => {
|
|
33
|
+
it('calls the full action a Hot Restart on Flutter (stdin R), not a Full Reload', () => {
|
|
34
|
+
const actions = (0, reloadActions_1.reloadActions)({ running: true, framework: 'flutter' }, DEV);
|
|
35
|
+
expect(actions.find((a) => a.id === 'hot').label).toBe('Hot Reload');
|
|
36
|
+
expect(actions.find((a) => a.id === 'full').label).toBe('Hot Restart');
|
|
37
|
+
expect(actions.find((a) => a.id === 'full').hint).toContain('(R)');
|
|
38
|
+
});
|
|
39
|
+
it('calls it a Full Reload everywhere else', () => {
|
|
40
|
+
for (const framework of ['expo', 'vite', 'nextjs']) {
|
|
41
|
+
const actions = (0, reloadActions_1.reloadActions)({ running: true, framework }, DEV);
|
|
42
|
+
expect(actions.find((a) => a.id === 'full').label).toBe('Full Reload');
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
describe('URL / payload construction', () => {
|
|
47
|
+
it('sends fast for hot and full for full, both to /dev/reload', () => {
|
|
48
|
+
const actions = (0, reloadActions_1.reloadActions)({ running: true, framework: 'flutter' }, DEV);
|
|
49
|
+
expect((0, reloadActions_1.reloadRequest)(actions[0])).toEqual({
|
|
50
|
+
method: 'POST',
|
|
51
|
+
path: reloadActions_1.RELOAD_PATH,
|
|
52
|
+
body: { mode: 'fast' },
|
|
53
|
+
});
|
|
54
|
+
expect((0, reloadActions_1.reloadRequest)(actions[1])).toEqual({
|
|
55
|
+
method: 'POST',
|
|
56
|
+
path: reloadActions_1.RELOAD_PATH,
|
|
57
|
+
body: { mode: 'full' },
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
it('routes the RN bundle rebuild to /dev/reload-app', () => {
|
|
61
|
+
const actions = (0, reloadActions_1.reloadActions)({ running: false }, { ...DEV, includeRebuild: true });
|
|
62
|
+
const rebuild = actions.find((a) => a.id === 'rebuild');
|
|
63
|
+
expect((0, reloadActions_1.reloadRequest)(rebuild)).toEqual({
|
|
64
|
+
method: 'POST',
|
|
65
|
+
path: reloadActions_1.RELOAD_APP_PATH,
|
|
66
|
+
body: { mode: 'bundle' },
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe('a blocked action NAMES the blocker', () => {
|
|
71
|
+
it('names the missing dev server and the command that starts it', () => {
|
|
72
|
+
const actions = (0, reloadActions_1.reloadActions)({ running: false }, { ...DEV, machineLabel: 'primary' });
|
|
73
|
+
expect(actions).toHaveLength(2);
|
|
74
|
+
for (const action of actions) {
|
|
75
|
+
expect(action.enabled).toBe(false);
|
|
76
|
+
expect(action.disabledReason).toContain('primary');
|
|
77
|
+
expect(action.disabledReason).toContain('yaver dev start');
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
it('names "still building" rather than pretending nothing is running', () => {
|
|
81
|
+
const actions = (0, reloadActions_1.reloadActions)({ running: true, building: true, framework: 'expo' }, DEV);
|
|
82
|
+
expect(actions[0].disabledReason).toContain('still building');
|
|
83
|
+
});
|
|
84
|
+
it('names the missing machine when disconnected', () => {
|
|
85
|
+
const actions = (0, reloadActions_1.reloadActions)({ running: true, framework: 'expo' }, { isDevBuild: true, connected: false });
|
|
86
|
+
expect(actions[0].disabledReason).toContain('Not connected');
|
|
87
|
+
});
|
|
88
|
+
it('keeps Rebuild Bundle ENABLED with no dev server — that is its whole point', () => {
|
|
89
|
+
const actions = (0, reloadActions_1.reloadActions)({ running: false }, { ...DEV, includeRebuild: true });
|
|
90
|
+
const rebuild = actions.find((a) => a.id === 'rebuild');
|
|
91
|
+
expect(rebuild.enabled).toBe(true);
|
|
92
|
+
expect(rebuild.disabledReason).toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
it('disables Rebuild Bundle when there is no machine', () => {
|
|
95
|
+
const actions = (0, reloadActions_1.reloadActions)({ running: true }, { isDevBuild: true, connected: false, includeRebuild: true });
|
|
96
|
+
expect(actions.find((a) => a.id === 'rebuild').enabled).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
describe('describeReloadFailure names a cause, never "failed"', () => {
|
|
100
|
+
it('503 → no dev server', () => {
|
|
101
|
+
expect((0, reloadActions_1.describeReloadFailure)(503, 'dev server not available')).toContain('No dev server is running');
|
|
102
|
+
});
|
|
103
|
+
it('framework cannot hot reload → says so and points at the alternative', () => {
|
|
104
|
+
const msg = (0, reloadActions_1.describeReloadFailure)(500, 'unity does not support hot reload', {
|
|
105
|
+
running: true,
|
|
106
|
+
framework: 'unity',
|
|
107
|
+
});
|
|
108
|
+
expect(msg).toContain('unity');
|
|
109
|
+
expect(msg).toContain('Rebuild Bundle');
|
|
110
|
+
});
|
|
111
|
+
it('loopback connection refused → the dev server is not listening', () => {
|
|
112
|
+
const msg = (0, reloadActions_1.describeReloadFailure)(502, 'Get "http://127.0.0.1:8081/reload": dial tcp 127.0.0.1:8081: connect: connection refused');
|
|
113
|
+
expect(msg).toContain('not listening');
|
|
114
|
+
expect(msg).toContain('yaver dev start');
|
|
115
|
+
});
|
|
116
|
+
it('401/403 → session, not server', () => {
|
|
117
|
+
expect((0, reloadActions_1.describeReloadFailure)(401, '')).toContain('sign in again');
|
|
118
|
+
expect((0, reloadActions_1.describeReloadFailure)(403, '')).toContain('sign in again');
|
|
119
|
+
});
|
|
120
|
+
it('404 → the agent is too old, and says how to update it', () => {
|
|
121
|
+
expect((0, reloadActions_1.describeReloadFailure)(404, 'not found')).toContain('yaver-cli@latest');
|
|
122
|
+
});
|
|
123
|
+
it('5xx → points at the agent log', () => {
|
|
124
|
+
expect((0, reloadActions_1.describeReloadFailure)(500, 'boom')).toContain('yaver logs');
|
|
125
|
+
});
|
|
126
|
+
it('status 0 (transport never answered) → machine reachability', () => {
|
|
127
|
+
expect((0, reloadActions_1.describeReloadFailure)(0, '')).toContain('yaver serve');
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const fs_1 = require("fs");
|
|
4
|
+
const path_1 = require("path");
|
|
5
|
+
/**
|
|
6
|
+
* `reloadActions.ts` is duplicated into yaver-feedback-react-native and
|
|
7
|
+
* yaver-feedback-web on purpose — they are two independently published npm
|
|
8
|
+
* packages, and neither may depend on the other.
|
|
9
|
+
*
|
|
10
|
+
* Duplication without a guard is DRIFT, and drift in this file is the worst
|
|
11
|
+
* kind: it is a *policy* file. If the web copy stops returning [] for a
|
|
12
|
+
* production build and the RN copy still does, the two SDKs disagree about
|
|
13
|
+
* whether a shipped app may show a reload button — and nothing in `tsc`
|
|
14
|
+
* notices, because they are separate compilation units.
|
|
15
|
+
*
|
|
16
|
+
* This is the same shape as beaconParity.test.ts in mobile/: read both
|
|
17
|
+
* sources, assert they agree. Prove it by breaking it — change one word in
|
|
18
|
+
* either copy and this test fails.
|
|
19
|
+
*/
|
|
20
|
+
const RN_COPY = (0, path_1.join)(__dirname, '..', 'reloadActions.ts');
|
|
21
|
+
const WEB_COPY = (0, path_1.join)(__dirname, '..', '..', '..', // sdk/feedback/react-native
|
|
22
|
+
'web', 'src', 'reloadActions.ts');
|
|
23
|
+
describe('reloadActions parity between the RN and web SDKs', () => {
|
|
24
|
+
it('both copies exist', () => {
|
|
25
|
+
expect(() => (0, fs_1.readFileSync)(RN_COPY, 'utf8')).not.toThrow();
|
|
26
|
+
expect(() => (0, fs_1.readFileSync)(WEB_COPY, 'utf8')).not.toThrow();
|
|
27
|
+
});
|
|
28
|
+
it('is byte-identical across the two packages', () => {
|
|
29
|
+
const rn = (0, fs_1.readFileSync)(RN_COPY, 'utf8');
|
|
30
|
+
const web = (0, fs_1.readFileSync)(WEB_COPY, 'utf8');
|
|
31
|
+
expect(rn).toEqual(web);
|
|
32
|
+
});
|
|
33
|
+
it('still carries the production guard in BOTH copies', () => {
|
|
34
|
+
// Named separately from the byte comparison so that if someone
|
|
35
|
+
// legitimately reformats both files the failure still points at the one
|
|
36
|
+
// line that actually matters.
|
|
37
|
+
for (const [name, path] of [['react-native', RN_COPY], ['web', WEB_COPY]]) {
|
|
38
|
+
const source = (0, fs_1.readFileSync)(path, 'utf8');
|
|
39
|
+
expect(`${name}: ${source.includes('if (!opts.isDevBuild) return [];')}`).toEqual(`${name}: true`);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -39,7 +39,7 @@ describe('React Native SDK types', () => {
|
|
|
39
39
|
const bundle = {
|
|
40
40
|
metadata: {
|
|
41
41
|
timestamp: '2026-03-24T12:00:00Z',
|
|
42
|
-
|
|
42
|
+
deviceInfo: {
|
|
43
43
|
platform: 'ios',
|
|
44
44
|
osVersion: '18.0',
|
|
45
45
|
model: 'iPhone 16 Pro',
|
|
@@ -55,7 +55,7 @@ describe('React Native SDK types', () => {
|
|
|
55
55
|
screenshots: [],
|
|
56
56
|
};
|
|
57
57
|
expect(bundle.metadata.timestamp).toBe('2026-03-24T12:00:00Z');
|
|
58
|
-
expect(bundle.metadata.
|
|
58
|
+
expect(bundle.metadata.deviceInfo.platform).toBe('ios');
|
|
59
59
|
expect(bundle.screenshots).toEqual([]);
|
|
60
60
|
expect(bundle.video).toBeUndefined();
|
|
61
61
|
});
|
|
@@ -63,7 +63,7 @@ describe('React Native SDK types', () => {
|
|
|
63
63
|
const bundle = {
|
|
64
64
|
metadata: {
|
|
65
65
|
timestamp: '2026-03-24T12:00:00Z',
|
|
66
|
-
|
|
66
|
+
deviceInfo: {
|
|
67
67
|
platform: 'android',
|
|
68
68
|
osVersion: '15',
|
|
69
69
|
model: 'Pixel 9',
|
|
@@ -146,7 +146,7 @@ describe('React Native SDK types', () => {
|
|
|
146
146
|
bundle: {
|
|
147
147
|
metadata: {
|
|
148
148
|
timestamp: 'now',
|
|
149
|
-
|
|
149
|
+
deviceInfo: { platform: 'ios', osVersion: '18', model: 'iPhone', screenWidth: 393, screenHeight: 852 },
|
|
150
150
|
app: {},
|
|
151
151
|
},
|
|
152
152
|
screenshots: [],
|
|
@@ -164,7 +164,7 @@ describe('React Native SDK types', () => {
|
|
|
164
164
|
bundle: {
|
|
165
165
|
metadata: {
|
|
166
166
|
timestamp: 'now',
|
|
167
|
-
|
|
167
|
+
deviceInfo: { platform: 'ios', osVersion: '18', model: 'iPhone', screenWidth: 393, screenHeight: 852 },
|
|
168
168
|
app: {},
|
|
169
169
|
},
|
|
170
170
|
screenshots: [],
|
package/dist/_core/device.d.ts
CHANGED
|
@@ -39,18 +39,28 @@ export declare function mergeDeviceEntries(a: CoreDevice, b: CoreDevice): CoreDe
|
|
|
39
39
|
*/
|
|
40
40
|
export declare function collapseDevices(devices: CoreDevice[]): CoreDevice[];
|
|
41
41
|
/**
|
|
42
|
-
* "Fresh" matches the mobile app: online + heartbeat
|
|
43
|
-
* read Convex's `isOnline` first (backend
|
|
44
|
-
* gate from the server clock), then use this helper when
|
|
45
|
-
* phone-side freshness opinion too — e.g. for auto-connect
|
|
42
|
+
* "Fresh" matches the mobile app: online + heartbeat within
|
|
43
|
+
* HEARTBEAT_STALE_MS. Clients read Convex's `isOnline` first (the backend
|
|
44
|
+
* applies the same gate from the server clock), then use this helper when
|
|
45
|
+
* they need the phone-side freshness opinion too — e.g. for auto-connect
|
|
46
|
+
* picks.
|
|
46
47
|
*/
|
|
47
48
|
export declare function isDeviceFresh(d: CoreDevice, now?: number): boolean;
|
|
48
49
|
/**
|
|
49
|
-
* Choose the best candidate for an auto-connect attempt.
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
50
|
+
* Choose the best candidate for an auto-connect attempt.
|
|
51
|
+
*
|
|
52
|
+
* An explicit `preferredDeviceId` is honoured by id ALONE, or not at all:
|
|
53
|
+
* - A missing `quicHost` is not grounds to reroute. Relay transport
|
|
54
|
+
* addresses a device by id (`<relay>/d/<deviceId>`), so the entry is
|
|
55
|
+
* still reachable off-LAN — which is precisely when quicHost is absent.
|
|
56
|
+
* - If the id is not in the list, return null. Falling through to another
|
|
57
|
+
* machine silently lands the user's fix on the wrong host: they pick the
|
|
58
|
+
* Mac mini, the commit shows up on the laptop, and nothing reports an
|
|
59
|
+
* error. Not connecting is the better failure — the caller surfaces
|
|
60
|
+
* "selected machine missing, re-select it".
|
|
61
|
+
*
|
|
62
|
+
* With no preference, fall back: fresh + quicHost → online + quicHost →
|
|
63
|
+
* first with a quicHost.
|
|
54
64
|
*/
|
|
55
65
|
export declare function pickTargetDevice(devices: CoreDevice[], preferredDeviceId?: string): CoreDevice | null;
|
|
56
66
|
/**
|
package/dist/_core/device.js
CHANGED
|
@@ -182,10 +182,11 @@ function collapseDevices(devices) {
|
|
|
182
182
|
}
|
|
183
183
|
// ── Freshness + target pick ───────────────────────────────────────────
|
|
184
184
|
/**
|
|
185
|
-
* "Fresh" matches the mobile app: online + heartbeat
|
|
186
|
-
* read Convex's `isOnline` first (backend
|
|
187
|
-
* gate from the server clock), then use this helper when
|
|
188
|
-
* phone-side freshness opinion too — e.g. for auto-connect
|
|
185
|
+
* "Fresh" matches the mobile app: online + heartbeat within
|
|
186
|
+
* HEARTBEAT_STALE_MS. Clients read Convex's `isOnline` first (the backend
|
|
187
|
+
* applies the same gate from the server clock), then use this helper when
|
|
188
|
+
* they need the phone-side freshness opinion too — e.g. for auto-connect
|
|
189
|
+
* picks.
|
|
189
190
|
*/
|
|
190
191
|
function isDeviceFresh(d, now = Date.now()) {
|
|
191
192
|
if (!d.isOnline)
|
|
@@ -195,21 +196,26 @@ function isDeviceFresh(d, now = Date.now()) {
|
|
|
195
196
|
return now - d.lastHeartbeat < constants_1.HEARTBEAT_STALE_MS;
|
|
196
197
|
}
|
|
197
198
|
/**
|
|
198
|
-
* Choose the best candidate for an auto-connect attempt.
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
199
|
+
* Choose the best candidate for an auto-connect attempt.
|
|
200
|
+
*
|
|
201
|
+
* An explicit `preferredDeviceId` is honoured by id ALONE, or not at all:
|
|
202
|
+
* - A missing `quicHost` is not grounds to reroute. Relay transport
|
|
203
|
+
* addresses a device by id (`<relay>/d/<deviceId>`), so the entry is
|
|
204
|
+
* still reachable off-LAN — which is precisely when quicHost is absent.
|
|
205
|
+
* - If the id is not in the list, return null. Falling through to another
|
|
206
|
+
* machine silently lands the user's fix on the wrong host: they pick the
|
|
207
|
+
* Mac mini, the commit shows up on the laptop, and nothing reports an
|
|
208
|
+
* error. Not connecting is the better failure — the caller surfaces
|
|
209
|
+
* "selected machine missing, re-select it".
|
|
210
|
+
*
|
|
211
|
+
* With no preference, fall back: fresh + quicHost → online + quicHost →
|
|
212
|
+
* first with a quicHost.
|
|
203
213
|
*/
|
|
204
214
|
function pickTargetDevice(devices, preferredDeviceId) {
|
|
205
215
|
if (!devices.length)
|
|
206
216
|
return null;
|
|
207
217
|
if (preferredDeviceId) {
|
|
208
|
-
|
|
209
|
-
if (preferred && isDeviceFresh(preferred))
|
|
210
|
-
return preferred;
|
|
211
|
-
if (preferred)
|
|
212
|
-
return preferred;
|
|
218
|
+
return devices.find((d) => d.deviceId === preferredDeviceId) ?? null;
|
|
213
219
|
}
|
|
214
220
|
const fresh = devices.find((d) => isDeviceFresh(d) && d.quicHost);
|
|
215
221
|
if (fresh)
|
package/dist/capture.d.ts
CHANGED
|
@@ -88,3 +88,9 @@ export declare function stopAudioRecording(): Promise<{
|
|
|
88
88
|
} | null>;
|
|
89
89
|
/** Whether a voice-note recording is currently active. */
|
|
90
90
|
export declare function isAudioRecording(): boolean;
|
|
91
|
+
/** Begin a raw-PCM recording for the voice stream. */
|
|
92
|
+
export declare function startPcmRecording(): Promise<void>;
|
|
93
|
+
/** Stop the voice recording; returns the WAV file:// URI (or null). */
|
|
94
|
+
export declare function stopPcmRecording(): Promise<string | null>;
|
|
95
|
+
/** Whether a voice-stream recording is currently active. */
|
|
96
|
+
export declare function isPcmRecording(): boolean;
|
package/dist/capture.js
CHANGED
|
@@ -25,6 +25,9 @@ exports.isVoiceCaptureSupported = isVoiceCaptureSupported;
|
|
|
25
25
|
exports.startAudioRecording = startAudioRecording;
|
|
26
26
|
exports.stopAudioRecording = stopAudioRecording;
|
|
27
27
|
exports.isAudioRecording = isAudioRecording;
|
|
28
|
+
exports.startPcmRecording = startPcmRecording;
|
|
29
|
+
exports.stopPcmRecording = stopPcmRecording;
|
|
30
|
+
exports.isPcmRecording = isPcmRecording;
|
|
28
31
|
/**
|
|
29
32
|
* Capture the current screen as a PNG image.
|
|
30
33
|
* Requires `react-native-view-shot` to be installed.
|
|
@@ -307,3 +310,53 @@ async function stopAudioRecording() {
|
|
|
307
310
|
function isAudioRecording() {
|
|
308
311
|
return audioRecorderActive;
|
|
309
312
|
}
|
|
313
|
+
// ── Voice-stream recording (raw LPCM WAV) ──────────────────────────────
|
|
314
|
+
// The voice vibe-coding path streams audio to the agent's STT WS, which
|
|
315
|
+
// expects raw 16-bit / 16 kHz mono PCM (we strip the WAV header on the
|
|
316
|
+
// way out). That's a different format from the HIGH_QUALITY m4a recorder
|
|
317
|
+
// above — a compressed .m4a can't be streamed to Deepgram/whisper — so
|
|
318
|
+
// this uses its own recording options, mirroring the Yaver app's
|
|
319
|
+
// AgentVoiceButton.
|
|
320
|
+
let pcmRecorderRef = null;
|
|
321
|
+
let pcmRecorderActive = false;
|
|
322
|
+
// Raw LPCM 16-bit LE, 16 kHz mono. iOS uses lpcm; Android records WAV.
|
|
323
|
+
const PCM_RECORDING_OPTIONS = {
|
|
324
|
+
android: { extension: '.wav', outputFormat: 2, audioEncoder: 3, sampleRate: 16000, numberOfChannels: 1, bitRate: 256000 },
|
|
325
|
+
ios: {
|
|
326
|
+
extension: '.wav', outputFormat: 'lpcm', audioQuality: 0x40, sampleRate: 16000,
|
|
327
|
+
numberOfChannels: 1, bitRate: 256000, linearPCMBitDepth: 16, linearPCMIsBigEndian: false, linearPCMIsFloat: false,
|
|
328
|
+
},
|
|
329
|
+
web: { mimeType: 'audio/wav', bitsPerSecond: 256000 },
|
|
330
|
+
};
|
|
331
|
+
/** Begin a raw-PCM recording for the voice stream. */
|
|
332
|
+
async function startPcmRecording() {
|
|
333
|
+
if (pcmRecorderActive)
|
|
334
|
+
throw new Error('[YaverFeedback] A voice recording is already in progress.');
|
|
335
|
+
const ExpoAv = loadExpoAvOrThrow();
|
|
336
|
+
const { Audio } = ExpoAv;
|
|
337
|
+
const perm = await Audio.requestPermissionsAsync();
|
|
338
|
+
if (!perm.granted) {
|
|
339
|
+
throw new Error('[YaverFeedback] Microphone permission denied. Enable it in Settings ▸ Your App ▸ Microphone.');
|
|
340
|
+
}
|
|
341
|
+
await Audio.setAudioModeAsync({ allowsRecordingIOS: true, playsInSilentModeIOS: true, staysActiveInBackground: false });
|
|
342
|
+
const { recording } = await Audio.Recording.createAsync(PCM_RECORDING_OPTIONS);
|
|
343
|
+
pcmRecorderRef = recording;
|
|
344
|
+
pcmRecorderActive = true;
|
|
345
|
+
}
|
|
346
|
+
/** Stop the voice recording; returns the WAV file:// URI (or null). */
|
|
347
|
+
async function stopPcmRecording() {
|
|
348
|
+
if (!pcmRecorderActive || !pcmRecorderRef)
|
|
349
|
+
return null;
|
|
350
|
+
const recording = pcmRecorderRef;
|
|
351
|
+
pcmRecorderRef = null;
|
|
352
|
+
pcmRecorderActive = false;
|
|
353
|
+
try {
|
|
354
|
+
await recording.stopAndUnloadAsync();
|
|
355
|
+
}
|
|
356
|
+
catch { /* already stopped */ }
|
|
357
|
+
return typeof recording.getURI === 'function' ? recording.getURI() : null;
|
|
358
|
+
}
|
|
359
|
+
/** Whether a voice-stream recording is currently active. */
|
|
360
|
+
function isPcmRecording() {
|
|
361
|
+
return pcmRecorderActive;
|
|
362
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -28,12 +28,16 @@
|
|
|
28
28
|
* ```
|
|
29
29
|
*/
|
|
30
30
|
export { YaverFeedback } from './YaverFeedback';
|
|
31
|
+
export { captureStoreScreenshots } from './storeShots';
|
|
32
|
+
export type { CaptureStoreScreenshotsOptions, CaptureStoreScreenshotsResult, StoreShotFrame, } from './storeShots';
|
|
31
33
|
export { BlackBox } from './BlackBox';
|
|
32
34
|
export { YaverUpdates } from './YaverUpdates';
|
|
33
35
|
export type { YaverUpdatesConfig, PendingUpdate } from './YaverUpdates';
|
|
34
36
|
export { initExpo } from './expo';
|
|
35
37
|
export { YaverDiscovery } from './Discovery';
|
|
36
38
|
export { P2PClient } from './P2PClient';
|
|
39
|
+
export { reloadActions, reloadRequest, reloadFrameworkFamily, describeReloadFailure, RELOAD_PATH, RELOAD_APP_PATH, } from './reloadActions';
|
|
40
|
+
export type { ReloadAction, ReloadActionId, ReloadActionsOptions, ReloadWireMode, DevServerSnapshot, } from './reloadActions';
|
|
37
41
|
export { YaverConnectionScreen } from './ConnectionScreen';
|
|
38
42
|
export { YaverLoginScreen } from './LoginScreen';
|
|
39
43
|
export type { YaverLoginScreenProps } from './LoginScreen';
|