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,29 @@
1
+ import type { DogfoodUiColors } from './DogfoodSessionUi';
2
+
3
+ /** High-contrast colors for shared Dogfood controls inside the light SDK sheet. */
4
+ export const FEEDBACK_DOGFOOD_LIGHT_COLORS: DogfoodUiColors = {
5
+ background: '#ffffff',
6
+ border: '#c9c9d5',
7
+ text: '#222229',
8
+ muted: '#5f5f6b',
9
+ accent: '#5645d8',
10
+ accentSoft: '#e7e5ff',
11
+ ready: '#137a3f',
12
+ attention: '#9a5700',
13
+ blocked: '#b42318',
14
+ console: '#ffffff',
15
+ };
16
+
17
+ /** The log surface stays dark, but its foregrounds are explicit and readable. */
18
+ export const FEEDBACK_DOGFOOD_CONSOLE_COLORS: DogfoodUiColors = {
19
+ ...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
+ };
package/src/P2PClient.ts CHANGED
@@ -315,7 +315,7 @@ function friendlyReloadError(status: number, body: string): string {
315
315
  if (hasRefused && hasLoopback) {
316
316
  return (
317
317
  'No dev server running on your machine. ' +
318
- 'Start Metro with `yaver dev start` or use "Screenshot & Fix" instead.'
318
+ 'Start Metro with `yaver dev start`, or describe the issue in Chat so the agent can inspect it.'
319
319
  );
320
320
  }
321
321
  if (
@@ -491,6 +491,8 @@ export class P2PClient {
491
491
  framework?: string;
492
492
  frameworks?: string[];
493
493
  surfaces?: string[];
494
+ branch?: string;
495
+ gitRemote?: string;
494
496
  }>> {
495
497
  const resp = await fetch(`${this.baseUrl}/projects`, { headers: this.authHeaders() });
496
498
  if (!resp.ok) {
@@ -56,8 +56,18 @@ export function createP2PDogfoodDriver(
56
56
  retryable: true,
57
57
  });
58
58
  }
59
+ context.log({
60
+ text: `[runtime] source · ${target.label}${target.platform ? ` · ${target.platform}` : ''}`,
61
+ at: Date.now(),
62
+ stream: 'system',
63
+ });
59
64
  context.setPhase('starting', `Starting ${target.label} over WebRTC…`);
60
65
  const session = await client.startDogfoodRemoteRuntime(project.workDir, project.framework, target.id);
66
+ context.log({
67
+ text: `[runtime] ${session.status || 'starting'}${session.note ? ` · ${session.note}` : ''}`,
68
+ at: Date.now(),
69
+ stream: 'system',
70
+ });
61
71
  context.registerCleanup(() => client.stopDogfoodRemoteRuntime(session.id), 'session');
62
72
  return {
63
73
  lane: 'webrtc',
@@ -67,6 +77,13 @@ export function createP2PDogfoodDriver(
67
77
  }
68
78
  context.setPhase(project.lane === 'hermes' ? 'compiling' : 'starting',
69
79
  project.lane === 'hermes' ? `Compiling ${project.name} with Hermes…` : `Starting ${project.name} in the browser…`);
80
+ context.log({
81
+ text: project.lane === 'hermes'
82
+ ? `[runtime] source · Hermes build on ${project.workDir}`
83
+ : `[runtime] source · browser build on ${project.workDir}`,
84
+ at: Date.now(),
85
+ stream: 'system',
86
+ });
70
87
  const status = await client.startDogfoodDevServer({
71
88
  framework: project.framework,
72
89
  workDir: project.workDir,
@@ -1077,6 +1077,16 @@ export class YaverFeedback {
1077
1077
  }
1078
1078
  return base;
1079
1079
  }
1080
+ // Backend approval makes the installation eligible; it does not mean the
1081
+ // tester is currently in Dogfood. Keep all in-app controls absent until
1082
+ // enableDeviceDogfood() has minted a live scoped session, and make Exit
1083
+ // Dogfood actually remove both the gesture and fallback Y.
1084
+ if (!YaverFeedback.getDogfoodStatus().active) {
1085
+ if (typeof native?.setEnabled === 'function') {
1086
+ await native.setEnabled(false, 900).catch(() => undefined);
1087
+ }
1088
+ return { ...base, reason: 'dogfood-session-inactive' };
1089
+ }
1080
1090
  if (IS_HOST_MODE || isRunningInsideYaverHost()) {
1081
1091
  if (typeof native?.setEnabled === 'function') {
1082
1092
  await native.setEnabled(false, 900).catch(() => undefined);
@@ -124,6 +124,21 @@ describe('Dogfood lanes and console events', () => {
124
124
  expect(swift.map((option) => [option.lane, option.supported])).toEqual([
125
125
  ['browser', false], ['hermes', false], ['webrtc', true],
126
126
  ]);
127
+ expect(dogfoodLaneOptions('kotlin', { nativeRuntimeAvailable: true })
128
+ .map((option) => [option.lane, option.supported])).toEqual([
129
+ ['browser', false], ['hermes', false], ['webrtc', true],
130
+ ]);
131
+ expect(defaultDogfoodLane('swift', { nativeRuntimeAvailable: true })).toBe('webrtc');
132
+ });
133
+
134
+ test('a detected browser target adds real framework exceptions such as SwiftWasm', () => {
135
+ const swiftWasm = dogfoodLaneOptions('swift', {
136
+ nativeRuntimeAvailable: false,
137
+ browserRuntimeAvailable: true,
138
+ });
139
+ expect(swiftWasm.map((option) => [option.lane, option.supported])).toEqual([
140
+ ['browser', true], ['hermes', false], ['webrtc', false],
141
+ ]);
127
142
  });
128
143
 
129
144
  test('reads raw and replayed package-manager logs from /dev/events', () => {
@@ -0,0 +1,80 @@
1
+ import { readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import {
4
+ FEEDBACK_DOGFOOD_CONSOLE_COLORS,
5
+ FEEDBACK_DOGFOOD_LIGHT_COLORS,
6
+ } from '../FeedbackModalTheme';
7
+
8
+ const SOURCE_PATH = join(__dirname, '..', 'FeedbackModal.tsx');
9
+
10
+ describe('FeedbackModal authenticated chat contract', () => {
11
+ const source = readFileSync(SOURCE_PATH, 'utf8');
12
+
13
+ it('themes every shared Dogfood surface for the light feedback sheet', () => {
14
+ expect(source).toMatch(/<DogfoodStatusRail[\s\S]*?colors=\{FEEDBACK_DOGFOOD_LIGHT_COLORS\}/);
15
+ expect(source).toMatch(/<DogfoodLanePicker[\s\S]*?colors=\{FEEDBACK_DOGFOOD_LIGHT_COLORS\}/);
16
+ expect(source).toMatch(/<DogfoodLiveConsole[\s\S]*?colors=\{FEEDBACK_DOGFOOD_CONSOLE_COLORS\}/);
17
+ });
18
+
19
+ it('keeps normal light-sheet copy at WCAG AA contrast', () => {
20
+ const rgb = (hex: string) => hex.match(/[a-f\d]{2}/gi)!.map((part) => parseInt(part, 16) / 255);
21
+ const luminance = (hex: string) => {
22
+ const channels = rgb(hex).map((channel) => channel <= 0.03928
23
+ ? channel / 12.92
24
+ : ((channel + 0.055) / 1.055) ** 2.4);
25
+ return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2];
26
+ };
27
+ const ratio = (foreground: string, background: string) => {
28
+ const [lighter, darker] = [luminance(foreground), luminance(background)].sort((a, b) => b - a);
29
+ return (lighter + 0.05) / (darker + 0.05);
30
+ };
31
+
32
+ for (const foreground of [
33
+ FEEDBACK_DOGFOOD_LIGHT_COLORS.text,
34
+ FEEDBACK_DOGFOOD_LIGHT_COLORS.muted,
35
+ FEEDBACK_DOGFOOD_LIGHT_COLORS.accent,
36
+ FEEDBACK_DOGFOOD_LIGHT_COLORS.ready,
37
+ FEEDBACK_DOGFOOD_LIGHT_COLORS.attention,
38
+ FEEDBACK_DOGFOOD_LIGHT_COLORS.blocked,
39
+ ]) {
40
+ expect(ratio(foreground, FEEDBACK_DOGFOOD_LIGHT_COLORS.background)).toBeGreaterThanOrEqual(4.5);
41
+ }
42
+ expect(ratio(FEEDBACK_DOGFOOD_CONSOLE_COLORS.text, FEEDBACK_DOGFOOD_CONSOLE_COLORS.console))
43
+ .toBeGreaterThanOrEqual(4.5);
44
+ expect(ratio(FEEDBACK_DOGFOOD_CONSOLE_COLORS.muted, FEEDBACK_DOGFOOD_CONSOLE_COLORS.console))
45
+ .toBeGreaterThanOrEqual(4.5);
46
+ });
47
+
48
+ it('uses Chat as the authenticated entry surface without legacy command buttons', () => {
49
+ expect(source).toContain("setActiveTab(authenticated ? 'chat' : 'settings')");
50
+ expect(source).toContain('setShowVibeInput(authenticated || directDogfood)');
51
+ expect(source).not.toContain('Screenshot & Fix');
52
+ expect(source).not.toContain('<DeployPanel');
53
+ });
54
+
55
+ it('keeps Dogfood setup to box, runner, and checkout before asking for a runtime', () => {
56
+ const setupSteps = source.match(/const dogfoodSetupSteps = \[([\s\S]*?)\n \];/)?.[1] || '';
57
+ expect([...setupSteps.matchAll(/key: '([^']+)'/g)].map((match) => match[1]))
58
+ .toEqual(['box', 'runner', 'checkout']);
59
+ expect(setupSteps).not.toContain("key: 'oauth'");
60
+ expect(setupSteps).not.toContain("key: 'installation'");
61
+ expect(setupSteps).not.toContain("key: 'model'");
62
+ expect(setupSteps).not.toContain("key: 'lane'");
63
+ expect(source).toContain("type DogfoodSetupStage = 'setup' | 'lane' | 'runtime'");
64
+ expect(source).toContain('label="Choose runtime"');
65
+ });
66
+
67
+ it('passes the selected native target and labels the live log source', () => {
68
+ expect(source).toContain("nativeTargetId: dogfoodLane === 'webrtc' ? dogfoodNativeTargetId : undefined");
69
+ expect(source).toMatch(/<DogfoodLiveConsole[\s\S]*?sourceLabel=\{dogfoodSourceLabel\}/);
70
+ expect(source).toContain('Simulator, emulator, or device');
71
+ });
72
+
73
+ it('has one iOS keyboard inset owner and no gesture-stealing sheet Pressable', () => {
74
+ expect(source).toContain('automaticallyAdjustKeyboardInsets={Platform.OS === \'ios\'}');
75
+ expect(source).not.toContain('<KeyboardAvoidingView');
76
+ expect(source).not.toContain('keyboardInset');
77
+ expect(source).toContain('<Pressable style={styles.backdrop} onPress={handleClose}');
78
+ expect(source).toMatch(/<View[\s\S]{0,300}?style=\{\[\s*styles\.modal/);
79
+ });
80
+ });
@@ -3,12 +3,37 @@ import { join } from 'path';
3
3
 
4
4
  const plugin = require('../../app.plugin.js') as {
5
5
  __test: {
6
+ patchHotReloadBootConfirmation(contents: string): string;
6
7
  patchDogfoodAppShortcut(contents: string): string;
7
8
  patchDogfoodSceneDelegate(contents: string): string;
8
9
  };
9
10
  };
10
11
 
11
12
  describe('native Dogfood shortcut contract', () => {
13
+ it('never treats process uptime as a successful React render', () => {
14
+ const staleHook = `
15
+ // Crash-revert safety net: clear the boot-attempt counter once
16
+ // RN renders its first frame, OR after 10 s of uptime — whichever
17
+ // fires first. If neither fires (bundle crashes before render),
18
+ // YaverHotReload.bundleURL() will eventually revert to the
19
+ // TestFlight-installed bundle after 3 failed boots. See
20
+ // YaverHotReload.swift for the full state machine.
21
+ NotificationCenter.default.addObserver(
22
+ forName: NSNotification.Name(rawValue: "RCTContentDidAppearNotification"),
23
+ object: nil,
24
+ queue: .main
25
+ ) { _ in YaverHotReload.markBootSuccessful() }
26
+ DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
27
+ YaverHotReload.markBootSuccessful()
28
+ }
29
+ `;
30
+ const patched = plugin.__test.patchHotReloadBootConfirmation(staleHook);
31
+ expect(patched).toContain('RCTContentDidAppearNotification');
32
+ expect(patched).toContain('only a real first React frame confirms boot');
33
+ expect(patched).not.toContain('asyncAfter(deadline: .now() + 10)');
34
+ expect(plugin.__test.patchHotReloadBootConfirmation(patched)).toBe(patched);
35
+ });
36
+
12
37
  it('patches cold and warm iOS shortcut delivery idempotently', () => {
13
38
  const source = `
14
39
  public class AppDelegate: ExpoAppDelegate {
@@ -50,12 +50,16 @@ describe('createP2PDogfoodDriver', () => {
50
50
  })),
51
51
  stopDogfoodRemoteRuntime: closeRuntime,
52
52
  } as unknown as P2PClient;
53
+ const snapshots: string[][] = [];
53
54
  const controller = new DogfoodController(
54
55
  { name: 'Native app', framework: 'flutter', workDir: '/workspace/app', lane: 'webrtc' },
55
56
  createP2PDogfoodDriver(client),
57
+ { onChange: (snapshot) => snapshots.push(snapshot.logs.map((line) => line.text)) },
56
58
  );
57
59
 
58
60
  await expect(controller.trigger()).resolves.toMatchObject({ lane: 'webrtc', sessionId: 'runtime-1' });
61
+ expect(snapshots.flat()).toContain('[runtime] source · iPhone simulator');
62
+ expect(snapshots.flat()).toContain('[runtime] starting');
59
63
  expect(closeRuntime).not.toHaveBeenCalled();
60
64
  await controller.stop();
61
65
  expect(closeRuntime).toHaveBeenCalledWith('runtime-1');
@@ -72,6 +72,27 @@ beforeEach(() => {
72
72
  jest.clearAllMocks();
73
73
  });
74
74
 
75
+ function activeDogfood(controlGesture: true | { durationMs: number } = true) {
76
+ return {
77
+ enabled: true,
78
+ appId: 'io.example.app',
79
+ installationId: 'phone-1',
80
+ installationStatus: 'active' as const,
81
+ controlGesture,
82
+ };
83
+ }
84
+
85
+ function initActiveDogfood(options: { authToken?: string; controlGesture?: true | { durationMs: number } } = {}) {
86
+ const controlGesture = options.controlGesture ?? true;
87
+ YaverFeedback.init({
88
+ authToken: options.authToken,
89
+ bundleId: 'io.example.app',
90
+ dogfood: { controlGesture },
91
+ });
92
+ Object.assign(YaverFeedback.getConfig()!.dogfood!, activeDogfood(controlGesture));
93
+ jest.clearAllMocks();
94
+ }
95
+
75
96
  describe('YaverFeedback', () => {
76
97
  describe('Dogfood onboarding', () => {
77
98
  it('starts with Yaver OAuth when the host has no session', async () => {
@@ -187,8 +208,7 @@ describe('YaverFeedback', () => {
187
208
  });
188
209
 
189
210
  it('keeps the Y visible until the authorized phone has completed onboarding', async () => {
190
- YaverFeedback.init({ bundleId: 'io.example.app', dogfood: {} });
191
- YaverFeedback.getConfig()!.dogfood!.controlGesture = true;
211
+ initActiveDogfood();
192
212
  jest.spyOn(YaverFeedback, 'getDogfoodAccess').mockResolvedValueOnce({
193
213
  appId: 'io.example.app',
194
214
  yaverAuthenticated: true,
@@ -210,8 +230,7 @@ describe('YaverFeedback', () => {
210
230
  });
211
231
 
212
232
  it('uses the invisible three-finger hold only after onboarding selects it', async () => {
213
- YaverFeedback.init({ bundleId: 'io.example.app', dogfood: {} });
214
- YaverFeedback.getConfig()!.dogfood!.controlGesture = true;
233
+ initActiveDogfood();
215
234
  jest.spyOn(YaverFeedback, 'getDogfoodAccess').mockResolvedValueOnce({
216
235
  appId: 'io.example.app',
217
236
  yaverAuthenticated: true,
@@ -235,8 +254,7 @@ describe('YaverFeedback', () => {
235
254
  });
236
255
 
237
256
  it('keeps the Y when native capability reports support but enabling the gesture fails', async () => {
238
- YaverFeedback.init({ bundleId: 'io.example.app', dogfood: {} });
239
- YaverFeedback.getConfig()!.dogfood!.controlGesture = true;
257
+ initActiveDogfood();
240
258
  jest.spyOn(YaverFeedback, 'getDogfoodAccess').mockResolvedValueOnce({
241
259
  appId: 'io.example.app',
242
260
  yaverAuthenticated: true,
@@ -264,8 +282,7 @@ describe('YaverFeedback', () => {
264
282
  });
265
283
 
266
284
  it('persists first-run completion for the exact account app installation', async () => {
267
- YaverFeedback.init({ authToken: 'owner-token', bundleId: 'io.example.app', dogfood: {} });
268
- YaverFeedback.getConfig()!.dogfood!.controlGesture = true;
285
+ initActiveDogfood({ authToken: 'owner-token' });
269
286
  jest.spyOn(YaverFeedback, 'getDogfoodAccess').mockResolvedValueOnce({
270
287
  appId: 'io.example.app',
271
288
  yaverAuthenticated: true,
@@ -288,8 +305,7 @@ describe('YaverFeedback', () => {
288
305
  });
289
306
 
290
307
  it('falls back to the minimized Y when accessibility owns multi-touch', async () => {
291
- YaverFeedback.init({ bundleId: 'io.example.app', dogfood: {} });
292
- YaverFeedback.getConfig()!.dogfood!.controlGesture = { durationMs: 1200 };
308
+ initActiveDogfood({ controlGesture: { durationMs: 1200 } });
293
309
  jest.spyOn(YaverFeedback, 'getDogfoodAccess').mockResolvedValueOnce({
294
310
  appId: 'io.example.app',
295
311
  yaverAuthenticated: true,
@@ -311,13 +327,25 @@ describe('YaverFeedback', () => {
311
327
  });
312
328
 
313
329
  it('suppresses guest controls inside the Yaver split-view container', async () => {
314
- YaverFeedback.init({ bundleId: 'io.example.app', dogfood: {} });
315
- YaverFeedback.getConfig()!.dogfood!.controlGesture = true;
330
+ initActiveDogfood();
316
331
  (NativeModules as any).YaverInfo = { isYaver: true };
317
332
  const state = await YaverFeedback.syncDogfoodControlGesture();
318
333
  delete (NativeModules as any).YaverInfo;
319
334
  expect(state).toMatchObject({ gestureEnabled: false, fallbackVisible: false, reason: 'yaver-host-owns-controls' });
320
335
  });
336
+
337
+ it('removes the gesture and fallback Y when Dogfood exits', async () => {
338
+ initActiveDogfood();
339
+ await YaverFeedback.exitDogfoodMode();
340
+ const state = await YaverFeedback.syncDogfoodControlGesture();
341
+ expect(state).toMatchObject({
342
+ authorized: false,
343
+ gestureEnabled: false,
344
+ fallbackVisible: false,
345
+ reason: 'dogfood-session-inactive',
346
+ });
347
+ expect((NativeModules as any).YaverDogfoodGesture.setEnabled).toHaveBeenCalledWith(false, 900);
348
+ });
321
349
  });
322
350
 
323
351
  describe('init()', () => {
package/src/index.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.