yaver-feedback-react-native 0.9.8 → 0.9.10

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.
Files changed (37) hide show
  1. package/app.plugin.js +24 -10
  2. package/dist/DogfoodQuickControls.js +7 -0
  3. package/dist/DogfoodRuntime.d.ts +6 -1
  4. package/dist/DogfoodRuntime.js +9 -3
  5. package/dist/DogfoodSessionUi.d.ts +1 -0
  6. package/dist/DogfoodSessionUi.js +5 -2
  7. package/dist/FeedbackModal.js +249 -278
  8. package/dist/FeedbackModalTheme.d.ts +5 -0
  9. package/dist/FeedbackModalTheme.js +29 -0
  10. package/dist/P2PClient.d.ts +2 -0
  11. package/dist/P2PClient.js +1 -1
  12. package/dist/P2PDogfoodDriver.js +17 -0
  13. package/dist/YaverFeedback.js +10 -0
  14. package/dist/__tests__/DogfoodRuntime.test.js +14 -0
  15. package/dist/__tests__/FeedbackModalContract.test.d.ts +1 -0
  16. package/dist/__tests__/FeedbackModalContract.test.js +70 -0
  17. package/dist/__tests__/NativeDogfoodShortcut.test.js +23 -0
  18. package/dist/__tests__/P2PDogfoodDriver.test.js +4 -1
  19. package/dist/__tests__/YaverFeedback.test.js +37 -12
  20. package/dist/index.d.ts +4 -5
  21. package/dist/index.js +4 -5
  22. package/ios/YaverHotReload.swift +3 -4
  23. package/package.json +1 -1
  24. package/src/DogfoodQuickControls.tsx +7 -0
  25. package/src/DogfoodRuntime.ts +21 -4
  26. package/src/DogfoodSessionUi.tsx +6 -2
  27. package/src/FeedbackModal.tsx +319 -372
  28. package/src/FeedbackModalTheme.ts +29 -0
  29. package/src/P2PClient.ts +3 -1
  30. package/src/P2PDogfoodDriver.ts +17 -0
  31. package/src/YaverFeedback.ts +10 -0
  32. package/src/__tests__/DogfoodRuntime.test.ts +15 -0
  33. package/src/__tests__/FeedbackModalContract.test.ts +80 -0
  34. package/src/__tests__/NativeDogfoodShortcut.test.ts +25 -0
  35. package/src/__tests__/P2PDogfoodDriver.test.ts +4 -0
  36. package/src/__tests__/YaverFeedback.test.ts +40 -12
  37. package/src/index.ts +4 -5
@@ -0,0 +1,5 @@
1
+ import type { DogfoodUiColors } from './DogfoodSessionUi';
2
+ /** High-contrast colors for shared Dogfood controls inside the light SDK sheet. */
3
+ export declare const FEEDBACK_DOGFOOD_LIGHT_COLORS: DogfoodUiColors;
4
+ /** The log surface stays dark, but its foregrounds are explicit and readable. */
5
+ export declare const FEEDBACK_DOGFOOD_CONSOLE_COLORS: DogfoodUiColors;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FEEDBACK_DOGFOOD_CONSOLE_COLORS = exports.FEEDBACK_DOGFOOD_LIGHT_COLORS = void 0;
4
+ /** High-contrast colors for shared Dogfood controls inside the light SDK sheet. */
5
+ exports.FEEDBACK_DOGFOOD_LIGHT_COLORS = {
6
+ background: '#ffffff',
7
+ border: '#c9c9d5',
8
+ text: '#222229',
9
+ muted: '#5f5f6b',
10
+ accent: '#5645d8',
11
+ accentSoft: '#e7e5ff',
12
+ ready: '#137a3f',
13
+ attention: '#9a5700',
14
+ blocked: '#b42318',
15
+ console: '#ffffff',
16
+ };
17
+ /** The log surface stays dark, but its foregrounds are explicit and readable. */
18
+ exports.FEEDBACK_DOGFOOD_CONSOLE_COLORS = {
19
+ ...exports.FEEDBACK_DOGFOOD_LIGHT_COLORS,
20
+ background: '#15151b',
21
+ border: '#454552',
22
+ text: '#f8fafc',
23
+ muted: '#cbd5e1',
24
+ accent: '#a5b4fc',
25
+ ready: '#4ade80',
26
+ attention: '#fbbf24',
27
+ blocked: '#fca5a5',
28
+ console: '#15151b',
29
+ };
@@ -178,6 +178,8 @@ export declare class P2PClient {
178
178
  framework?: string;
179
179
  frameworks?: string[];
180
180
  surfaces?: string[];
181
+ branch?: string;
182
+ gitRemote?: string;
181
183
  }>>;
182
184
  getOpenCodeConfig(): Promise<OpenCodeConfigSummary | null>;
183
185
  saveOpenCodeConfig(patch: {
package/dist/P2PClient.js CHANGED
@@ -259,7 +259,7 @@ function friendlyReloadError(status, body) {
259
259
  const hasLoopback = lower.indexOf('127.0.0.1') >= 0 || lower.indexOf('localhost') >= 0;
260
260
  if (hasRefused && hasLoopback) {
261
261
  return ('No dev server running on your machine. ' +
262
- 'Start Metro with `yaver dev start` or use "Screenshot & Fix" instead.');
262
+ 'Start Metro with `yaver dev start`, or describe the issue in Chat so the agent can inspect it.');
263
263
  }
264
264
  if (lower.indexOf('no dev server') >= 0 ||
265
265
  lower.indexOf('not running') >= 0) {
@@ -41,8 +41,18 @@ function createP2PDogfoodDriver(client, options = {}) {
41
41
  retryable: true,
42
42
  });
43
43
  }
44
+ context.log({
45
+ text: `[runtime] source · ${target.label}${target.platform ? ` · ${target.platform}` : ''}`,
46
+ at: Date.now(),
47
+ stream: 'system',
48
+ });
44
49
  context.setPhase('starting', `Starting ${target.label} over WebRTC…`);
45
50
  const session = await client.startDogfoodRemoteRuntime(project.workDir, project.framework, target.id);
51
+ context.log({
52
+ text: `[runtime] ${session.status || 'starting'}${session.note ? ` · ${session.note}` : ''}`,
53
+ at: Date.now(),
54
+ stream: 'system',
55
+ });
46
56
  context.registerCleanup(() => client.stopDogfoodRemoteRuntime(session.id), 'session');
47
57
  return {
48
58
  lane: 'webrtc',
@@ -51,6 +61,13 @@ function createP2PDogfoodDriver(client, options = {}) {
51
61
  };
52
62
  }
53
63
  context.setPhase(project.lane === 'hermes' ? 'compiling' : 'starting', project.lane === 'hermes' ? `Compiling ${project.name} with Hermes…` : `Starting ${project.name} in the browser…`);
64
+ context.log({
65
+ text: project.lane === 'hermes'
66
+ ? `[runtime] source · Hermes build on ${project.workDir}`
67
+ : `[runtime] source · browser build on ${project.workDir}`,
68
+ at: Date.now(),
69
+ stream: 'system',
70
+ });
54
71
  const status = await client.startDogfoodDevServer({
55
72
  framework: project.framework,
56
73
  workDir: project.workDir,
@@ -1090,6 +1090,16 @@ class YaverFeedback {
1090
1090
  }
1091
1091
  return base;
1092
1092
  }
1093
+ // Backend approval makes the installation eligible; it does not mean the
1094
+ // tester is currently in Dogfood. Keep all in-app controls absent until
1095
+ // enableDeviceDogfood() has minted a live scoped session, and make Exit
1096
+ // Dogfood actually remove both the gesture and fallback Y.
1097
+ if (!YaverFeedback.getDogfoodStatus().active) {
1098
+ if (typeof native?.setEnabled === 'function') {
1099
+ await native.setEnabled(false, 900).catch(() => undefined);
1100
+ }
1101
+ return { ...base, reason: 'dogfood-session-inactive' };
1102
+ }
1093
1103
  if (IS_HOST_MODE || isRunningInsideYaverHost()) {
1094
1104
  if (typeof native?.setEnabled === 'function') {
1095
1105
  await native.setEnabled(false, 900).catch(() => undefined);
@@ -106,6 +106,20 @@ describe('Dogfood lanes and console events', () => {
106
106
  expect(swift.map((option) => [option.lane, option.supported])).toEqual([
107
107
  ['browser', false], ['hermes', false], ['webrtc', true],
108
108
  ]);
109
+ expect((0, DogfoodRuntime_1.dogfoodLaneOptions)('kotlin', { nativeRuntimeAvailable: true })
110
+ .map((option) => [option.lane, option.supported])).toEqual([
111
+ ['browser', false], ['hermes', false], ['webrtc', true],
112
+ ]);
113
+ expect((0, DogfoodRuntime_1.defaultDogfoodLane)('swift', { nativeRuntimeAvailable: true })).toBe('webrtc');
114
+ });
115
+ test('a detected browser target adds real framework exceptions such as SwiftWasm', () => {
116
+ const swiftWasm = (0, DogfoodRuntime_1.dogfoodLaneOptions)('swift', {
117
+ nativeRuntimeAvailable: false,
118
+ browserRuntimeAvailable: true,
119
+ });
120
+ expect(swiftWasm.map((option) => [option.lane, option.supported])).toEqual([
121
+ ['browser', true], ['hermes', false], ['webrtc', false],
122
+ ]);
109
123
  });
110
124
  test('reads raw and replayed package-manager logs from /dev/events', () => {
111
125
  expect((0, DogfoodRuntime_1.dogfoodLogLinesFromDevEvent)({ type: 'log', logLine: '$ npm ci\u001b[0m' }))
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const fs_1 = require("fs");
4
+ const path_1 = require("path");
5
+ const FeedbackModalTheme_1 = require("../FeedbackModalTheme");
6
+ const SOURCE_PATH = (0, path_1.join)(__dirname, '..', 'FeedbackModal.tsx');
7
+ describe('FeedbackModal authenticated chat contract', () => {
8
+ const source = (0, fs_1.readFileSync)(SOURCE_PATH, 'utf8');
9
+ it('themes every shared Dogfood surface for the light feedback sheet', () => {
10
+ expect(source).toMatch(/<DogfoodStatusRail[\s\S]*?colors=\{FEEDBACK_DOGFOOD_LIGHT_COLORS\}/);
11
+ expect(source).toMatch(/<DogfoodLanePicker[\s\S]*?colors=\{FEEDBACK_DOGFOOD_LIGHT_COLORS\}/);
12
+ expect(source).toMatch(/<DogfoodLiveConsole[\s\S]*?colors=\{FEEDBACK_DOGFOOD_CONSOLE_COLORS\}/);
13
+ });
14
+ it('keeps normal light-sheet copy at WCAG AA contrast', () => {
15
+ const rgb = (hex) => hex.match(/[a-f\d]{2}/gi).map((part) => parseInt(part, 16) / 255);
16
+ const luminance = (hex) => {
17
+ const channels = rgb(hex).map((channel) => channel <= 0.03928
18
+ ? channel / 12.92
19
+ : ((channel + 0.055) / 1.055) ** 2.4);
20
+ return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2];
21
+ };
22
+ const ratio = (foreground, background) => {
23
+ const [lighter, darker] = [luminance(foreground), luminance(background)].sort((a, b) => b - a);
24
+ return (lighter + 0.05) / (darker + 0.05);
25
+ };
26
+ for (const foreground of [
27
+ FeedbackModalTheme_1.FEEDBACK_DOGFOOD_LIGHT_COLORS.text,
28
+ FeedbackModalTheme_1.FEEDBACK_DOGFOOD_LIGHT_COLORS.muted,
29
+ FeedbackModalTheme_1.FEEDBACK_DOGFOOD_LIGHT_COLORS.accent,
30
+ FeedbackModalTheme_1.FEEDBACK_DOGFOOD_LIGHT_COLORS.ready,
31
+ FeedbackModalTheme_1.FEEDBACK_DOGFOOD_LIGHT_COLORS.attention,
32
+ FeedbackModalTheme_1.FEEDBACK_DOGFOOD_LIGHT_COLORS.blocked,
33
+ ]) {
34
+ expect(ratio(foreground, FeedbackModalTheme_1.FEEDBACK_DOGFOOD_LIGHT_COLORS.background)).toBeGreaterThanOrEqual(4.5);
35
+ }
36
+ expect(ratio(FeedbackModalTheme_1.FEEDBACK_DOGFOOD_CONSOLE_COLORS.text, FeedbackModalTheme_1.FEEDBACK_DOGFOOD_CONSOLE_COLORS.console))
37
+ .toBeGreaterThanOrEqual(4.5);
38
+ expect(ratio(FeedbackModalTheme_1.FEEDBACK_DOGFOOD_CONSOLE_COLORS.muted, FeedbackModalTheme_1.FEEDBACK_DOGFOOD_CONSOLE_COLORS.console))
39
+ .toBeGreaterThanOrEqual(4.5);
40
+ });
41
+ it('uses Chat as the authenticated entry surface without legacy command buttons', () => {
42
+ expect(source).toContain("setActiveTab(authenticated ? 'chat' : 'settings')");
43
+ expect(source).toContain('setShowVibeInput(authenticated || directDogfood)');
44
+ expect(source).not.toContain('Screenshot & Fix');
45
+ expect(source).not.toContain('<DeployPanel');
46
+ });
47
+ it('keeps Dogfood setup to box, runner, and checkout before asking for a runtime', () => {
48
+ const setupSteps = source.match(/const dogfoodSetupSteps = \[([\s\S]*?)\n \];/)?.[1] || '';
49
+ expect([...setupSteps.matchAll(/key: '([^']+)'/g)].map((match) => match[1]))
50
+ .toEqual(['box', 'runner', 'checkout']);
51
+ expect(setupSteps).not.toContain("key: 'oauth'");
52
+ expect(setupSteps).not.toContain("key: 'installation'");
53
+ expect(setupSteps).not.toContain("key: 'model'");
54
+ expect(setupSteps).not.toContain("key: 'lane'");
55
+ expect(source).toContain("type DogfoodSetupStage = 'setup' | 'lane' | 'runtime'");
56
+ expect(source).toContain('label="Choose runtime"');
57
+ });
58
+ it('passes the selected native target and labels the live log source', () => {
59
+ expect(source).toContain("nativeTargetId: dogfoodLane === 'webrtc' ? dogfoodNativeTargetId : undefined");
60
+ expect(source).toMatch(/<DogfoodLiveConsole[\s\S]*?sourceLabel=\{dogfoodSourceLabel\}/);
61
+ expect(source).toContain('Simulator, emulator, or device');
62
+ });
63
+ it('has one iOS keyboard inset owner and no gesture-stealing sheet Pressable', () => {
64
+ expect(source).toContain('automaticallyAdjustKeyboardInsets={Platform.OS === \'ios\'}');
65
+ expect(source).not.toContain('<KeyboardAvoidingView');
66
+ expect(source).not.toContain('keyboardInset');
67
+ expect(source).toContain('<Pressable style={styles.backdrop} onPress={handleClose}');
68
+ expect(source).toMatch(/<View[\s\S]{0,300}?style=\{\[\s*styles\.modal/);
69
+ });
70
+ });
@@ -4,6 +4,29 @@ const fs_1 = require("fs");
4
4
  const path_1 = require("path");
5
5
  const plugin = require('../../app.plugin.js');
6
6
  describe('native Dogfood shortcut contract', () => {
7
+ it('never treats process uptime as a successful React render', () => {
8
+ const staleHook = `
9
+ // Crash-revert safety net: clear the boot-attempt counter once
10
+ // RN renders its first frame, OR after 10 s of uptime — whichever
11
+ // fires first. If neither fires (bundle crashes before render),
12
+ // YaverHotReload.bundleURL() will eventually revert to the
13
+ // TestFlight-installed bundle after 3 failed boots. See
14
+ // YaverHotReload.swift for the full state machine.
15
+ NotificationCenter.default.addObserver(
16
+ forName: NSNotification.Name(rawValue: "RCTContentDidAppearNotification"),
17
+ object: nil,
18
+ queue: .main
19
+ ) { _ in YaverHotReload.markBootSuccessful() }
20
+ DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
21
+ YaverHotReload.markBootSuccessful()
22
+ }
23
+ `;
24
+ const patched = plugin.__test.patchHotReloadBootConfirmation(staleHook);
25
+ expect(patched).toContain('RCTContentDidAppearNotification');
26
+ expect(patched).toContain('only a real first React frame confirms boot');
27
+ expect(patched).not.toContain('asyncAfter(deadline: .now() + 10)');
28
+ expect(plugin.__test.patchHotReloadBootConfirmation(patched)).toBe(patched);
29
+ });
7
30
  it('patches cold and warm iOS shortcut delivery idempotently', () => {
8
31
  const source = `
9
32
  public class AppDelegate: ExpoAppDelegate {
@@ -43,8 +43,11 @@ describe('createP2PDogfoodDriver', () => {
43
43
  })),
44
44
  stopDogfoodRemoteRuntime: closeRuntime,
45
45
  };
46
- const controller = new DogfoodRuntime_1.DogfoodController({ name: 'Native app', framework: 'flutter', workDir: '/workspace/app', lane: 'webrtc' }, (0, P2PDogfoodDriver_1.createP2PDogfoodDriver)(client));
46
+ const snapshots = [];
47
+ const controller = new DogfoodRuntime_1.DogfoodController({ name: 'Native app', framework: 'flutter', workDir: '/workspace/app', lane: 'webrtc' }, (0, P2PDogfoodDriver_1.createP2PDogfoodDriver)(client), { onChange: (snapshot) => snapshots.push(snapshot.logs.map((line) => line.text)) });
47
48
  await expect(controller.trigger()).resolves.toMatchObject({ lane: 'webrtc', sessionId: 'runtime-1' });
49
+ expect(snapshots.flat()).toContain('[runtime] source · iPhone simulator');
50
+ expect(snapshots.flat()).toContain('[runtime] starting');
48
51
  expect(closeRuntime).not.toHaveBeenCalled();
49
52
  await controller.stop();
50
53
  expect(closeRuntime).toHaveBeenCalledWith('runtime-1');
@@ -69,6 +69,25 @@ beforeEach(() => {
69
69
  jest.restoreAllMocks();
70
70
  jest.clearAllMocks();
71
71
  });
72
+ function activeDogfood(controlGesture = true) {
73
+ return {
74
+ enabled: true,
75
+ appId: 'io.example.app',
76
+ installationId: 'phone-1',
77
+ installationStatus: 'active',
78
+ controlGesture,
79
+ };
80
+ }
81
+ function initActiveDogfood(options = {}) {
82
+ const controlGesture = options.controlGesture ?? true;
83
+ YaverFeedback_1.YaverFeedback.init({
84
+ authToken: options.authToken,
85
+ bundleId: 'io.example.app',
86
+ dogfood: { controlGesture },
87
+ });
88
+ Object.assign(YaverFeedback_1.YaverFeedback.getConfig().dogfood, activeDogfood(controlGesture));
89
+ jest.clearAllMocks();
90
+ }
72
91
  describe('YaverFeedback', () => {
73
92
  describe('Dogfood onboarding', () => {
74
93
  it('starts with Yaver OAuth when the host has no session', async () => {
@@ -175,8 +194,7 @@ describe('YaverFeedback', () => {
175
194
  .toHaveBeenCalledWith(false, 'Dogfood');
176
195
  });
177
196
  it('keeps the Y visible until the authorized phone has completed onboarding', async () => {
178
- YaverFeedback_1.YaverFeedback.init({ bundleId: 'io.example.app', dogfood: {} });
179
- YaverFeedback_1.YaverFeedback.getConfig().dogfood.controlGesture = true;
197
+ initActiveDogfood();
180
198
  jest.spyOn(YaverFeedback_1.YaverFeedback, 'getDogfoodAccess').mockResolvedValueOnce({
181
199
  appId: 'io.example.app',
182
200
  yaverAuthenticated: true,
@@ -197,8 +215,7 @@ describe('YaverFeedback', () => {
197
215
  expect(react_native_1.NativeModules.YaverDogfoodGesture.setEnabled).toHaveBeenCalledWith(false, 900);
198
216
  });
199
217
  it('uses the invisible three-finger hold only after onboarding selects it', async () => {
200
- YaverFeedback_1.YaverFeedback.init({ bundleId: 'io.example.app', dogfood: {} });
201
- YaverFeedback_1.YaverFeedback.getConfig().dogfood.controlGesture = true;
218
+ initActiveDogfood();
202
219
  jest.spyOn(YaverFeedback_1.YaverFeedback, 'getDogfoodAccess').mockResolvedValueOnce({
203
220
  appId: 'io.example.app',
204
221
  yaverAuthenticated: true,
@@ -221,8 +238,7 @@ describe('YaverFeedback', () => {
221
238
  expect(react_native_1.NativeModules.YaverDogfoodGesture.setEnabled).toHaveBeenCalledWith(true, 900);
222
239
  });
223
240
  it('keeps the Y when native capability reports support but enabling the gesture fails', async () => {
224
- YaverFeedback_1.YaverFeedback.init({ bundleId: 'io.example.app', dogfood: {} });
225
- YaverFeedback_1.YaverFeedback.getConfig().dogfood.controlGesture = true;
241
+ initActiveDogfood();
226
242
  jest.spyOn(YaverFeedback_1.YaverFeedback, 'getDogfoodAccess').mockResolvedValueOnce({
227
243
  appId: 'io.example.app',
228
244
  yaverAuthenticated: true,
@@ -249,8 +265,7 @@ describe('YaverFeedback', () => {
249
265
  });
250
266
  });
251
267
  it('persists first-run completion for the exact account app installation', async () => {
252
- YaverFeedback_1.YaverFeedback.init({ authToken: 'owner-token', bundleId: 'io.example.app', dogfood: {} });
253
- YaverFeedback_1.YaverFeedback.getConfig().dogfood.controlGesture = true;
268
+ initActiveDogfood({ authToken: 'owner-token' });
254
269
  jest.spyOn(YaverFeedback_1.YaverFeedback, 'getDogfoodAccess').mockResolvedValueOnce({
255
270
  appId: 'io.example.app',
256
271
  yaverAuthenticated: true,
@@ -272,8 +287,7 @@ describe('YaverFeedback', () => {
272
287
  }));
273
288
  });
274
289
  it('falls back to the minimized Y when accessibility owns multi-touch', async () => {
275
- YaverFeedback_1.YaverFeedback.init({ bundleId: 'io.example.app', dogfood: {} });
276
- YaverFeedback_1.YaverFeedback.getConfig().dogfood.controlGesture = { durationMs: 1200 };
290
+ initActiveDogfood({ controlGesture: { durationMs: 1200 } });
277
291
  jest.spyOn(YaverFeedback_1.YaverFeedback, 'getDogfoodAccess').mockResolvedValueOnce({
278
292
  appId: 'io.example.app',
279
293
  yaverAuthenticated: true,
@@ -294,13 +308,24 @@ describe('YaverFeedback', () => {
294
308
  expect(react_native_1.NativeModules.YaverDogfoodGesture.setEnabled).toHaveBeenCalledWith(false, 1200);
295
309
  });
296
310
  it('suppresses guest controls inside the Yaver split-view container', async () => {
297
- YaverFeedback_1.YaverFeedback.init({ bundleId: 'io.example.app', dogfood: {} });
298
- YaverFeedback_1.YaverFeedback.getConfig().dogfood.controlGesture = true;
311
+ initActiveDogfood();
299
312
  react_native_1.NativeModules.YaverInfo = { isYaver: true };
300
313
  const state = await YaverFeedback_1.YaverFeedback.syncDogfoodControlGesture();
301
314
  delete react_native_1.NativeModules.YaverInfo;
302
315
  expect(state).toMatchObject({ gestureEnabled: false, fallbackVisible: false, reason: 'yaver-host-owns-controls' });
303
316
  });
317
+ it('removes the gesture and fallback Y when Dogfood exits', async () => {
318
+ initActiveDogfood();
319
+ await YaverFeedback_1.YaverFeedback.exitDogfoodMode();
320
+ const state = await YaverFeedback_1.YaverFeedback.syncDogfoodControlGesture();
321
+ expect(state).toMatchObject({
322
+ authorized: false,
323
+ gestureEnabled: false,
324
+ fallbackVisible: false,
325
+ reason: 'dogfood-session-inactive',
326
+ });
327
+ expect(react_native_1.NativeModules.YaverDogfoodGesture.setEnabled).toHaveBeenCalledWith(false, 900);
328
+ });
304
329
  });
305
330
  describe('init()', () => {
306
331
  it('sets config correctly with defaults', () => {
package/dist/index.d.ts CHANGED
@@ -1,11 +1,10 @@
1
1
  /**
2
2
  * yaver-feedback-react-native — Visual feedback SDK for Yaver.
3
3
  *
4
- * Shake-to-report surface with three launch actions:
5
- * 1. Hot Reload — instant JS reload
6
- * 2. Vibing — open a vibing session on the agent
7
- * 3. Screenshot & Fix — capture the current screen and trigger
8
- * the fix loop
4
+ * Shake-to-report surface with one primary action: Chat. The connected agent
5
+ * receives the current-screen context and can use its MCP tools for fixes,
6
+ * reloads, and deploys without duplicating those commands as modal buttons.
7
+ * Machine, runner, model, and Dogfood configuration remain under Settings.
9
8
  *
10
9
  * The small quick-access icon stays hidden until the first shake by
11
10
  * default on mobile, then remains available unless the user hides it.
package/dist/index.js CHANGED
@@ -2,11 +2,10 @@
2
2
  /**
3
3
  * yaver-feedback-react-native — Visual feedback SDK for Yaver.
4
4
  *
5
- * Shake-to-report surface with three launch actions:
6
- * 1. Hot Reload — instant JS reload
7
- * 2. Vibing — open a vibing session on the agent
8
- * 3. Screenshot & Fix — capture the current screen and trigger
9
- * the fix loop
5
+ * Shake-to-report surface with one primary action: Chat. The connected agent
6
+ * receives the current-screen context and can use its MCP tools for fixes,
7
+ * reloads, and deploys without duplicating those commands as modal buttons.
8
+ * Machine, runner, model, and Dogfood configuration remain under Settings.
10
9
  *
11
10
  * The small quick-access icon stays hidden until the first shake by
12
11
  * default on mobile, then remains available unless the user hides it.
@@ -157,8 +157,8 @@ class YaverHotReload: NSObject {
157
157
  /// bricks the app — user has to delete + reinstall from TestFlight.
158
158
  ///
159
159
  /// Guard: on each cold-start bundleURL() call, increment a boot
160
- /// counter. Reset it on first successful render (RCTContentDidAppear)
161
- /// or after 10 s of uptime. If the counter hits kMaxBootAttempts
160
+ /// counter. Reset it only on first successful render
161
+ /// (RCTContentDidAppear). If the counter hits kMaxBootAttempts
162
162
  /// without being reset, the bundle has crashed every boot so far —
163
163
  /// delete it and return nil so the app falls back to the
164
164
  /// TestFlight-installed bundle.
@@ -198,8 +198,7 @@ class YaverHotReload: NSObject {
198
198
  }
199
199
 
200
200
  /// Clear the boot-attempt counter. Call this from AppDelegate after
201
- /// first successful RN render (RCTContentDidAppearNotification) or
202
- /// after a short uptime safety timer — whichever fires first.
201
+ /// first successful RN render (RCTContentDidAppearNotification).
203
202
  @objc static func markBootSuccessful() {
204
203
  let defaults = UserDefaults.standard
205
204
  if defaults.integer(forKey: kKeyBootAttempts) > 0 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yaver-feedback-react-native",
3
- "version": "0.9.8",
3
+ "version": "0.9.10",
4
4
  "description": "Visual feedback SDK for Yaver — bug reports, screen recording, voice vibe coding, and local-first developer workflows",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -95,9 +95,16 @@ export const DogfoodQuickControls: React.FC = () => {
95
95
  const capability = DeviceEventEmitter.addListener('yaverDogfoodControlCapability', () => {
96
96
  void refresh();
97
97
  });
98
+ // Enrollment approval and Exit Dogfood both change SDK mode while this
99
+ // component remains mounted. Refresh React state immediately; native
100
+ // setEnabled() alone cannot make the fallback Y appear or disappear.
101
+ const mode = DeviceEventEmitter.addListener('yaverFeedback:dogfoodChanged', () => {
102
+ void refresh();
103
+ });
98
104
  return () => {
99
105
  trigger.remove();
100
106
  capability.remove();
107
+ mode.remove();
101
108
  };
102
109
  }, [refresh]);
103
110
 
@@ -44,11 +44,18 @@ export interface DogfoodLaneOption {
44
44
  /** One framework-to-lane matrix for Yaver and third-party consumers. */
45
45
  export function dogfoodLaneOptions(
46
46
  framework: string,
47
- capabilities: { nativeRuntimeAvailable?: boolean; selfDevelopment?: boolean } = {},
47
+ capabilities: {
48
+ nativeRuntimeAvailable?: boolean;
49
+ browserRuntimeAvailable?: boolean;
50
+ selfDevelopment?: boolean;
51
+ } = {},
48
52
  ): DogfoodLaneOption[] {
49
53
  const normalized = String(framework || '').trim().toLowerCase();
50
54
  const reactNative = normalized === 'expo' || normalized === 'react-native';
51
- const browserCapable = reactNative || [
55
+ // The framework matrix is the safe default. A positively detected browser
56
+ // target may add an exception (for example SwiftWasm/Tokamak), but a missing
57
+ // browser binary must not incorrectly remove RN/Flutter's browser build lane.
58
+ const browserCapable = capabilities.browserRuntimeAvailable === true || reactNative || [
52
59
  'flutter', 'web', 'next', 'nextjs', 'vite', 'remix', 'svelte', 'vue', 'angular',
53
60
  ].includes(normalized);
54
61
  const nativeAvailable = capabilities.nativeRuntimeAvailable === true;
@@ -68,8 +75,18 @@ export function dogfoodLaneOptions(
68
75
  ];
69
76
  }
70
77
 
71
- export function defaultDogfoodLane(framework: string): DogfoodLane {
72
- return dogfoodLaneOptions(framework).find((option) => option.default && option.supported)?.lane || 'browser';
78
+ export function defaultDogfoodLane(
79
+ framework: string,
80
+ capabilities: {
81
+ nativeRuntimeAvailable?: boolean;
82
+ browserRuntimeAvailable?: boolean;
83
+ selfDevelopment?: boolean;
84
+ } = {},
85
+ ): DogfoodLane {
86
+ const options = dogfoodLaneOptions(framework, capabilities);
87
+ return options.find((option) => option.default && option.supported)?.lane
88
+ || options.find((option) => option.supported)?.lane
89
+ || 'browser';
73
90
  }
74
91
 
75
92
  export interface DogfoodLogLine {
@@ -154,6 +154,7 @@ function runtimeTone(phase: DogfoodPhase, colors: DogfoodUiColors): string {
154
154
  * Logs; Hermes/WebRTC use the same lifecycle and failure/remedy treatment. */
155
155
  export const DogfoodLiveConsole: React.FC<{
156
156
  lane: DogfoodLane;
157
+ sourceLabel?: string;
157
158
  phase: DogfoodPhase;
158
159
  message: string;
159
160
  logs: readonly DogfoodLogLine[];
@@ -161,16 +162,18 @@ export const DogfoodLiveConsole: React.FC<{
161
162
  maxLines?: number;
162
163
  colors?: Partial<DogfoodUiColors>;
163
164
  renderText?: (text: string) => React.ReactNode;
164
- }> = ({ lane, phase, message, logs, failure, maxLines = 80, colors: colorOverrides, renderText }) => {
165
+ }> = ({ lane, sourceLabel, phase, message, logs, failure, maxLines = 80, colors: colorOverrides, renderText }) => {
165
166
  const colors = resolvedColors(colorOverrides);
166
167
  const text = logs.slice(-maxLines).map((line) => line.text).join('\n');
167
168
  const title = lane === 'browser' ? 'Browser Logs' : lane === 'hermes' ? 'Hermes Logs' : 'WebRTC Logs';
169
+ const accessibleTitle = sourceLabel ? `${title} · ${sourceLabel}` : title;
168
170
  return (
169
- <View style={[styles.console, { backgroundColor: colors.console, borderColor: colors.border }]} accessibilityLabel={title}>
171
+ <View style={[styles.console, { backgroundColor: colors.console, borderColor: colors.border }]} accessibilityLabel={accessibleTitle}>
170
172
  <View style={styles.consoleHeader}>
171
173
  <View style={[styles.statusDot, { backgroundColor: runtimeTone(phase, colors) }]} />
172
174
  <Text style={[styles.consoleTitle, { color: colors.text }]}>{title}</Text>
173
175
  </View>
176
+ {sourceLabel ? <Text style={[styles.consoleSource, { color: colors.muted }]}>Source · {sourceLabel}</Text> : null}
174
177
  <Text style={[styles.consoleStatus, { color: colors.muted }]}>{message}</Text>
175
178
  {text ? (
176
179
  renderText ? renderText(text) : <Text selectable style={[styles.consoleText, { color: colors.text }]}>{text}</Text>
@@ -204,6 +207,7 @@ const styles = StyleSheet.create({
204
207
  console: { width: '100%', maxHeight: 320, overflow: 'hidden', marginTop: 10, borderWidth: 1, borderRadius: 10, padding: 11, gap: 7 },
205
208
  consoleHeader: { flexDirection: 'row', alignItems: 'center', gap: 7 },
206
209
  consoleTitle: { fontSize: 12, fontWeight: '800' },
210
+ consoleSource: { fontSize: 10, lineHeight: 14 },
207
211
  consoleStatus: { fontSize: 11, lineHeight: 16 },
208
212
  consoleText: { fontFamily: 'monospace', fontSize: 10, lineHeight: 15 },
209
213
  consoleEmpty: { fontSize: 10, fontStyle: 'italic' },