yaver-feedback-react-native 0.9.10 → 0.9.12

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.
@@ -78,6 +78,37 @@ describe('DogfoodController', () => {
78
78
  await controller.stop();
79
79
  expect(stopSecond).toHaveBeenCalledTimes(1);
80
80
  });
81
+ test('keeps a failed preferred lane in the console and automatically recovers through browser', async () => {
82
+ const stopPreferred = jest.fn();
83
+ const lanes = [];
84
+ const controller = new DogfoodRuntime_1.DogfoodController({
85
+ ...expo,
86
+ lane: 'hermes',
87
+ fallbackLane: 'browser',
88
+ }, {
89
+ async start(ctx) {
90
+ lanes.push(ctx.project.lane);
91
+ if (ctx.project.lane === 'hermes') {
92
+ ctx.registerCleanup(stopPreferred);
93
+ throw new DogfoodRuntime_1.DogfoodRuntimeError({
94
+ code: 'DOGFOOD_HERMES_BUILD_FAILED',
95
+ error: 'Hermes build failed',
96
+ remedy: 'Use the browser build.',
97
+ retryable: true,
98
+ });
99
+ }
100
+ return { lane: 'browser', url: 'http://agent/dev/', metadata: { recovered: true } };
101
+ },
102
+ });
103
+ await expect(controller.trigger()).resolves.toMatchObject({
104
+ lane: 'browser',
105
+ metadata: { fallbackFrom: 'hermes', fallbackReason: 'DOGFOOD_HERMES_BUILD_FAILED' },
106
+ });
107
+ expect(lanes).toEqual(['hermes', 'browser']);
108
+ expect(stopPreferred).toHaveBeenCalledTimes(1);
109
+ expect(controller.snapshot()).toMatchObject({ phase: 'ready', project: { lane: 'browser' } });
110
+ expect(controller.snapshot().logs.map((line) => line.text)).toContain('[fallback] hermes failed (DOGFOOD_HERMES_BUILD_FAILED); trying browser');
111
+ });
81
112
  });
82
113
  describe('Dogfood lanes and console events', () => {
83
114
  test('Flutter is first-class on browser but cannot be mislabeled Hermes', () => {
@@ -95,6 +126,20 @@ describe('Dogfood lanes and console events', () => {
95
126
  expect((0, DogfoodRuntime_1.defaultDogfoodLane)('flutter')).toBe('browser');
96
127
  expect(flutter.find((option) => option.lane === 'hermes')?.supported).toBe(false);
97
128
  });
129
+ test('uses browser as the framework-aware default and as second choice after an explicit native preference', () => {
130
+ expect((0, DogfoodRuntime_1.dogfoodLanePlan)('flutter', { nativeRuntimeAvailable: true })).toMatchObject({
131
+ preferred: 'browser', fallback: undefined,
132
+ });
133
+ expect((0, DogfoodRuntime_1.dogfoodLanePlan)('expo', { nativeRuntimeAvailable: true }, 'hermes')).toMatchObject({
134
+ preferred: 'hermes', fallback: 'browser',
135
+ });
136
+ expect((0, DogfoodRuntime_1.dogfoodLanePlan)('flutter', { nativeRuntimeAvailable: true }, 'webrtc')).toMatchObject({
137
+ preferred: 'webrtc', fallback: 'browser',
138
+ });
139
+ expect((0, DogfoodRuntime_1.dogfoodLanePlan)('swift', { nativeRuntimeAvailable: true }, 'webrtc')).toMatchObject({
140
+ preferred: 'webrtc', fallback: undefined,
141
+ });
142
+ });
98
143
  test('keeps Yaver self-development on the same RN three-lane contract', () => {
99
144
  const options = (0, DogfoodRuntime_1.dogfoodLaneOptions)('expo', { nativeRuntimeAvailable: true, selfDevelopment: true });
100
145
  expect(options).toHaveLength(3);
@@ -39,11 +39,16 @@ describe('FeedbackModal authenticated chat contract', () => {
39
39
  .toBeGreaterThanOrEqual(4.5);
40
40
  });
41
41
  it('uses Chat as the authenticated entry surface without legacy command buttons', () => {
42
- expect(source).toContain("setActiveTab(authenticated ? 'chat' : 'settings')");
42
+ expect(source).toContain("useState<'chat' | 'settings'>('chat')");
43
43
  expect(source).toContain('setShowVibeInput(authenticated || directDogfood)');
44
44
  expect(source).not.toContain('Screenshot & Fix');
45
45
  expect(source).not.toContain('<DeployPanel');
46
46
  });
47
+ it('opens explicit Dogfood onboarding on setup and makes the runtime console the first live surface', () => {
48
+ expect(source).toContain("setActiveTab('settings')");
49
+ expect(source).toContain("setDogfoodSetupStage('runtime')");
50
+ expect(source).toMatch(/dogfoodSetupStage === 'runtime'[\s\S]*?<DogfoodLiveConsole/);
51
+ });
47
52
  it('keeps Dogfood setup to box, runner, and checkout before asking for a runtime', () => {
48
53
  const setupSteps = source.match(/const dogfoodSetupSteps = \[([\s\S]*?)\n \];/)?.[1] || '';
49
54
  expect([...setupSteps.matchAll(/key: '([^']+)'/g)].map((match) => match[1]))
@@ -53,16 +58,21 @@ describe('FeedbackModal authenticated chat contract', () => {
53
58
  expect(setupSteps).not.toContain("key: 'model'");
54
59
  expect(setupSteps).not.toContain("key: 'lane'");
55
60
  expect(source).toContain("type DogfoodSetupStage = 'setup' | 'lane' | 'runtime'");
56
- expect(source).toContain('label="Choose runtime"');
61
+ expect(source).toContain("label={dogfoodSetupReady ? 'Continue to runtime' : 'Complete the choices above'}");
62
+ expect(source).toContain('{!dogfoodOnboarding ? <>');
63
+ expect(source).toContain("? `Set up ${dogfoodOnboarding.projectName || dogfoodOnboarding.label || 'this app'} Dogfood`");
57
64
  });
58
65
  it('passes the selected native target and labels the live log source', () => {
59
- expect(source).toContain("nativeTargetId: dogfoodLane === 'webrtc' ? dogfoodNativeTargetId : undefined");
66
+ expect(source).toContain("nativeTargetId: lanePlan.preferred === 'webrtc' ? dogfoodNativeTargetId : undefined");
67
+ expect(source).toContain('fallbackLane: lanePlan.fallback');
68
+ expect(source).toContain('fallbackLane={dogfoodLanePolicy.fallback}');
60
69
  expect(source).toMatch(/<DogfoodLiveConsole[\s\S]*?sourceLabel=\{dogfoodSourceLabel\}/);
61
70
  expect(source).toContain('Simulator, emulator, or device');
62
71
  });
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');
72
+ it('has one keyboard inset owner and no gesture-stealing sheet Pressable', () => {
73
+ expect(source).toContain("behavior={Platform.OS === 'ios' ? 'padding' : 'height'}");
74
+ expect(source).toContain('automaticallyAdjustKeyboardInsets={false}');
75
+ expect(source).toContain('<KeyboardAvoidingView');
66
76
  expect(source).not.toContain('keyboardInset');
67
77
  expect(source).toContain('<Pressable style={styles.backdrop} onPress={handleClose}');
68
78
  expect(source).toMatch(/<View[\s\S]{0,300}?style=\{\[\s*styles\.modal/);
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const fs_1 = __importDefault(require("fs"));
7
+ const path_1 = __importDefault(require("path"));
8
+ const source = fs_1.default.readFileSync(path_1.default.join(__dirname, '..', 'MachinePickerScreen.tsx'), 'utf8');
9
+ describe('MachinePickerScreen progressive reachability contract', () => {
10
+ it('makes heartbeat-online machines immediately selectable', () => {
11
+ expect(source).toContain("const direct = device.isOnline");
12
+ expect(source).toContain("? { reachable: true } as DeviceReachability");
13
+ expect(source).not.toContain("statusLine = 'Checking connection…'");
14
+ expect(source).toContain("statusLine = device.platform || 'Online'");
15
+ });
16
+ it('does not hold every row behind the slowest direct probe', () => {
17
+ expect(source).not.toContain('Promise.allSettled');
18
+ expect(source).toContain("filter((candidate) => !candidate.isOnline)");
19
+ expect(source).toContain("setReachability((prev) => ({ ...prev, [device.deviceId]: probe }))");
20
+ });
21
+ it('names the selected machine connection operation', () => {
22
+ expect(source).toContain("statusLine = 'Connecting…'");
23
+ expect(source).toContain('disabled={selectingDeviceId !== null}');
24
+ });
25
+ });
package/dist/index.d.ts CHANGED
@@ -56,8 +56,8 @@ export { resolveSDKDogfood } from './dogfoodPolicy';
56
56
  export type { DogfoodAccessSnapshot, DogfoodFlowSnapshot, SDKDogfoodConfig, SDKDogfoodStatus } from './dogfoodPolicy';
57
57
  export { YaverDeviceDogfood } from './deviceDogfood';
58
58
  export type { DeviceDogfoodOptions, DeviceDogfoodSession, DeviceDogfoodState } from './deviceDogfood';
59
- export { DogfoodController, DogfoodRuntimeError, defaultDogfoodLane, dogfoodLaneOptions, dogfoodLogLinesFromDevEvent, runtimeLogLinesFromDevEvent, validateDogfoodProject, } from './DogfoodRuntime';
60
- export type { DogfoodControllerOptions, DogfoodDriver, DogfoodFailure, DogfoodLane, DogfoodLaneOption, DogfoodLogLine, DogfoodPhase, DogfoodProject, DogfoodResult, DogfoodRunContext, DogfoodSnapshot, } from './DogfoodRuntime';
59
+ export { DogfoodController, DogfoodRuntimeError, defaultDogfoodLane, dogfoodLanePlan, dogfoodLaneOptions, dogfoodLogLinesFromDevEvent, runtimeLogLinesFromDevEvent, validateDogfoodProject, } from './DogfoodRuntime';
60
+ export type { DogfoodControllerOptions, DogfoodDriver, DogfoodFailure, DogfoodLane, DogfoodLaneOption, DogfoodLanePlan, DogfoodLogLine, DogfoodPhase, DogfoodProject, DogfoodResult, DogfoodRunContext, DogfoodSnapshot, } from './DogfoodRuntime';
61
61
  export { DogfoodLanePicker, DogfoodLiveConsole, DogfoodStatusRail } from './DogfoodSessionUi';
62
62
  export type { DogfoodStatusStep, DogfoodStatusTone, DogfoodUiColors, } from './DogfoodSessionUi';
63
63
  export { FeedbackModal } from './FeedbackModal';
package/dist/index.js CHANGED
@@ -28,8 +28,8 @@
28
28
  * ```
29
29
  */
30
30
  Object.defineProperty(exports, "__esModule", { value: true });
31
- exports.getToken = exports.getWebBaseUrl = exports.getConvexSiteUrl = exports.configureAuthEndpoints = exports.setPreferredDogfoodLane = exports.getPreferredDogfoodLane = exports.clearQuickIconDisabled = exports.setQuickIconDisabled = exports.getQuickIconDisabled = exports.FixReport = exports.QuickActionIcon = exports.DogfoodQuickControls = exports.FeedbackModal = exports.DogfoodStatusRail = exports.DogfoodLiveConsole = exports.DogfoodLanePicker = exports.validateDogfoodProject = exports.runtimeLogLinesFromDevEvent = exports.dogfoodLogLinesFromDevEvent = exports.dogfoodLaneOptions = exports.defaultDogfoodLane = exports.DogfoodRuntimeError = exports.DogfoodController = exports.YaverDeviceDogfood = exports.resolveSDKDogfood = exports.isYaverModeBadgeHidden = exports.showYaverModeBadge = exports.hideYaverModeBadge = exports.YaverModeBadge = exports.FloatingButton = exports.ShakeDetector = exports.AuthOverlay = exports.PairDeviceModal = exports.YaverMachinePickerScreen = exports.YaverLoginScreen = exports.YaverConnectionScreen = exports.RELOAD_APP_PATH = exports.RELOAD_PATH = exports.describeReloadFailure = exports.reloadFrameworkFamily = exports.reloadRequest = exports.reloadActions = exports.createP2PDogfoodDriver = exports.P2PClient = exports.YaverDiscovery = exports.initExpo = exports.YaverUpdates = exports.BlackBox = exports.captureStoreScreenshots = exports.YaverFeedback = void 0;
32
- exports.uploadFeedback = exports.isVideoRecording = exports.stopVideoRecording = exports.startVideoRecording = exports.pickFeedbackFile = exports.captureScreenshot = exports.DEFAULT_OAUTH_REDIRECT = exports.DEFAULT_WEB_BASE_URL = exports.DEFAULT_CONVEX_SITE_URL = exports.listReachableDevices = exports.loginWithEmail = exports.signupWithEmail = exports.signInWithOAuth = exports.signInWithApple = exports.validateToken = exports.clearSelectedDeviceId = exports.saveSelectedDeviceId = exports.getDogfoodAccountAccess = exports.getSelectedDeviceId = exports.saveUser = exports.getUser = exports.clearToken = exports.saveToken = void 0;
31
+ exports.getWebBaseUrl = exports.getConvexSiteUrl = exports.configureAuthEndpoints = exports.setPreferredDogfoodLane = exports.getPreferredDogfoodLane = exports.clearQuickIconDisabled = exports.setQuickIconDisabled = exports.getQuickIconDisabled = exports.FixReport = exports.QuickActionIcon = exports.DogfoodQuickControls = exports.FeedbackModal = exports.DogfoodStatusRail = exports.DogfoodLiveConsole = exports.DogfoodLanePicker = exports.validateDogfoodProject = exports.runtimeLogLinesFromDevEvent = exports.dogfoodLogLinesFromDevEvent = exports.dogfoodLaneOptions = exports.dogfoodLanePlan = exports.defaultDogfoodLane = exports.DogfoodRuntimeError = exports.DogfoodController = exports.YaverDeviceDogfood = exports.resolveSDKDogfood = exports.isYaverModeBadgeHidden = exports.showYaverModeBadge = exports.hideYaverModeBadge = exports.YaverModeBadge = exports.FloatingButton = exports.ShakeDetector = exports.AuthOverlay = exports.PairDeviceModal = exports.YaverMachinePickerScreen = exports.YaverLoginScreen = exports.YaverConnectionScreen = exports.RELOAD_APP_PATH = exports.RELOAD_PATH = exports.describeReloadFailure = exports.reloadFrameworkFamily = exports.reloadRequest = exports.reloadActions = exports.createP2PDogfoodDriver = exports.P2PClient = exports.YaverDiscovery = exports.initExpo = exports.YaverUpdates = exports.BlackBox = exports.captureStoreScreenshots = exports.YaverFeedback = void 0;
32
+ exports.uploadFeedback = exports.isVideoRecording = exports.stopVideoRecording = exports.startVideoRecording = exports.pickFeedbackFile = exports.captureScreenshot = exports.DEFAULT_OAUTH_REDIRECT = exports.DEFAULT_WEB_BASE_URL = exports.DEFAULT_CONVEX_SITE_URL = exports.listReachableDevices = exports.loginWithEmail = exports.signupWithEmail = exports.signInWithOAuth = exports.signInWithApple = exports.validateToken = exports.clearSelectedDeviceId = exports.saveSelectedDeviceId = exports.getDogfoodAccountAccess = exports.getSelectedDeviceId = exports.saveUser = exports.getUser = exports.clearToken = exports.saveToken = exports.getToken = void 0;
33
33
  var YaverFeedback_1 = require("./YaverFeedback");
34
34
  Object.defineProperty(exports, "YaverFeedback", { enumerable: true, get: function () { return YaverFeedback_1.YaverFeedback; } });
35
35
  var storeShots_1 = require("./storeShots");
@@ -83,6 +83,7 @@ var DogfoodRuntime_1 = require("./DogfoodRuntime");
83
83
  Object.defineProperty(exports, "DogfoodController", { enumerable: true, get: function () { return DogfoodRuntime_1.DogfoodController; } });
84
84
  Object.defineProperty(exports, "DogfoodRuntimeError", { enumerable: true, get: function () { return DogfoodRuntime_1.DogfoodRuntimeError; } });
85
85
  Object.defineProperty(exports, "defaultDogfoodLane", { enumerable: true, get: function () { return DogfoodRuntime_1.defaultDogfoodLane; } });
86
+ Object.defineProperty(exports, "dogfoodLanePlan", { enumerable: true, get: function () { return DogfoodRuntime_1.dogfoodLanePlan; } });
86
87
  Object.defineProperty(exports, "dogfoodLaneOptions", { enumerable: true, get: function () { return DogfoodRuntime_1.dogfoodLaneOptions; } });
87
88
  Object.defineProperty(exports, "dogfoodLogLinesFromDevEvent", { enumerable: true, get: function () { return DogfoodRuntime_1.dogfoodLogLinesFromDevEvent; } });
88
89
  Object.defineProperty(exports, "runtimeLogLinesFromDevEvent", { enumerable: true, get: function () { return DogfoodRuntime_1.runtimeLogLinesFromDevEvent; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yaver-feedback-react-native",
3
- "version": "0.9.10",
3
+ "version": "0.9.12",
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",
@@ -11,6 +11,8 @@ export const AuthOverlay: React.FC = () => {
11
11
  const [pickerVisible, setPickerVisible] = useState(false);
12
12
  const [token, setToken] = useState<string | null>(null);
13
13
  const activeOverlayRef = useRef<'none' | 'login' | 'picker'>('none');
14
+ const onboarding = YaverFeedback.getDogfoodOnboarding();
15
+ const dogfoodLabel = onboarding?.projectName || onboarding?.label || 'this app';
14
16
 
15
17
  const openLogin = useCallback(() => {
16
18
  activeOverlayRef.current = 'login';
@@ -94,13 +96,18 @@ export const AuthOverlay: React.FC = () => {
94
96
  return (
95
97
  <>
96
98
  <Modal visible={loginVisible} animationType="slide" presentationStyle="fullScreen" onRequestClose={closeAll}>
97
- <YaverLoginScreen onLoggedIn={handleLoggedIn} onCancel={closeAll} />
99
+ <YaverLoginScreen
100
+ onLoggedIn={handleLoggedIn}
101
+ onCancel={closeAll}
102
+ subtitle={onboarding ? `Sign in to set up ${dogfoodLabel} Dogfood` : undefined}
103
+ />
98
104
  </Modal>
99
105
  <Modal visible={pickerVisible && !!token} animationType="slide" presentationStyle="fullScreen" onRequestClose={closeAll}>
100
106
  {token && (
101
107
  <YaverMachinePickerScreen
102
108
  token={token}
103
109
  currentDeviceId={YaverFeedback.getConfig()?.preferredDeviceId}
110
+ title={onboarding ? `Choose a machine for ${dogfoodLabel}` : undefined}
104
111
  onPick={handleDevicePicked}
105
112
  onCancel={closeAll}
106
113
  />
@@ -27,6 +27,13 @@ export interface DogfoodProject {
27
27
  workDir: string;
28
28
  framework: string;
29
29
  lane: DogfoodLane;
30
+ /**
31
+ * Optional automatic recovery lane. The shared onboarding flow uses the
32
+ * browser lane here when a user prefers Hermes or WebRTC for a project that
33
+ * can also render in a browser. The failed preferred attempt remains in the
34
+ * live console; the fallback is never silent.
35
+ */
36
+ fallbackLane?: DogfoodLane;
30
37
  /** Optional source URL for drivers that can clone missing source. */
31
38
  repositoryUrl?: string;
32
39
  /** Optional native target from /remote-runtime/capabilities for WebRTC. */
@@ -41,6 +48,13 @@ export interface DogfoodLaneOption {
41
48
  reason?: string;
42
49
  }
43
50
 
51
+ export interface DogfoodLanePlan {
52
+ preferred: DogfoodLane;
53
+ /** Browser is the cheap, portable recovery lane for RN/Expo/Flutter/web. */
54
+ fallback?: DogfoodLane;
55
+ options: DogfoodLaneOption[];
56
+ }
57
+
44
58
  /** One framework-to-lane matrix for Yaver and third-party consumers. */
45
59
  export function dogfoodLaneOptions(
46
60
  framework: string,
@@ -89,6 +103,36 @@ export function defaultDogfoodLane(
89
103
  || 'browser';
90
104
  }
91
105
 
106
+ /**
107
+ * Resolve one ordered lane policy for Yaver and embedded SDK hosts.
108
+ *
109
+ * Browser is the onboarding default for browser-capable React Native, Expo,
110
+ * Flutter, and web projects. When the user explicitly prefers Hermes or
111
+ * WebRTC, browser becomes the automatic second attempt. Native-only projects
112
+ * remain WebRTC-only and never advertise a browser recovery they cannot run.
113
+ */
114
+ export function dogfoodLanePlan(
115
+ framework: string,
116
+ capabilities: {
117
+ nativeRuntimeAvailable?: boolean;
118
+ browserRuntimeAvailable?: boolean;
119
+ selfDevelopment?: boolean;
120
+ } = {},
121
+ preferred?: DogfoodLane | null,
122
+ ): DogfoodLanePlan {
123
+ const options = dogfoodLaneOptions(framework, capabilities);
124
+ const supported = (lane: DogfoodLane | null | undefined) =>
125
+ !!lane && options.some((option) => option.lane === lane && option.supported);
126
+ const resolved = supported(preferred)
127
+ ? preferred!
128
+ : defaultDogfoodLane(framework, capabilities);
129
+ return {
130
+ preferred: resolved,
131
+ fallback: resolved !== 'browser' && supported('browser') ? 'browser' : undefined,
132
+ options,
133
+ };
134
+ }
135
+
92
136
  export interface DogfoodLogLine {
93
137
  text: string;
94
138
  at: number;
@@ -291,8 +335,8 @@ export class DogfoodController {
291
335
  message: `Preparing ${this.project.name}…`, logs: [], startedAt: Date.now(),
292
336
  });
293
337
 
294
- const context: DogfoodRunContext = {
295
- project: this.project,
338
+ const makeContext = (project: DogfoodProject): DogfoodRunContext => ({
339
+ project,
296
340
  attempt,
297
341
  log: (line) => {
298
342
  if (generation !== this.generation) return;
@@ -316,16 +360,69 @@ export class DogfoodController {
316
360
  this.cleanups.set(generation, owned);
317
361
  },
318
362
  isCurrent: () => generation === this.generation,
319
- };
363
+ });
364
+
365
+ let activeProject = this.project;
366
+ let context = makeContext(activeProject);
320
367
 
321
368
  try {
322
- await this.driver.prepare?.(context);
323
- if (!context.isCurrent()) throw new DogfoodRuntimeError({
324
- code: 'DOGFOOD_ATTEMPT_REPLACED', error: 'A newer Dogfood attempt replaced this one.',
325
- remedy: 'Wait for the newer attempt.', retryable: true,
326
- });
327
- context.setPhase('starting', `Starting ${this.project.name} on the ${this.project.lane} lane…`);
328
- const result = await this.driver.start(context);
369
+ const start = async (): Promise<DogfoodResult> => {
370
+ await this.driver.prepare?.(context);
371
+ if (!context.isCurrent()) throw new DogfoodRuntimeError({
372
+ code: 'DOGFOOD_ATTEMPT_REPLACED', error: 'A newer Dogfood attempt replaced this one.',
373
+ remedy: 'Wait for the newer attempt.', retryable: true,
374
+ });
375
+ context.setPhase('starting', `Starting ${activeProject.name} on the ${activeProject.lane} lane…`);
376
+ return this.driver.start(context);
377
+ };
378
+
379
+ let result: DogfoodResult;
380
+ try {
381
+ result = await start();
382
+ } catch (primaryError) {
383
+ const primaryFailure = failureFrom(primaryError);
384
+ const fallbackLane = activeProject.fallbackLane;
385
+ const mayFallback = !!fallbackLane
386
+ && fallbackLane !== activeProject.lane
387
+ && primaryFailure.retryable
388
+ && primaryFailure.code !== 'DOGFOOD_ATTEMPT_REPLACED';
389
+ if (!mayFallback) throw primaryError;
390
+
391
+ await this.runCleanups('all', generation);
392
+ const fallbackProject: DogfoodProject = {
393
+ ...activeProject,
394
+ lane: fallbackLane!,
395
+ fallbackLane: undefined,
396
+ };
397
+ const fallbackInvalid = validateDogfoodProject(fallbackProject);
398
+ if (fallbackInvalid) throw primaryError;
399
+ const fallbackLine: DogfoodLogLine = {
400
+ text: `[fallback] ${activeProject.lane} failed (${primaryFailure.code}); trying ${fallbackLane}`,
401
+ at: Date.now(),
402
+ stream: 'system',
403
+ };
404
+ activeProject = fallbackProject;
405
+ this.replace({
406
+ ...this.state,
407
+ project: activeProject,
408
+ phase: 'preparing',
409
+ message: `Recovering ${activeProject.name} in the ${fallbackLane} lane…`,
410
+ logs: [...this.state.logs, fallbackLine].slice(-this.maxLogLines),
411
+ lastOutputAt: fallbackLine.at,
412
+ failure: undefined,
413
+ });
414
+ context = makeContext(activeProject);
415
+ result = await start();
416
+ result = {
417
+ ...result,
418
+ metadata: {
419
+ ...result.metadata,
420
+ preferredLane: this.project.lane,
421
+ fallbackFrom: this.project.lane,
422
+ fallbackReason: primaryFailure.code,
423
+ },
424
+ };
425
+ }
329
426
  if (!context.isCurrent()) {
330
427
  await this.runCleanups('all', generation);
331
428
  throw new DogfoodRuntimeError({
@@ -333,7 +430,7 @@ export class DogfoodController {
333
430
  remedy: 'Wait for the newer attempt.', retryable: true,
334
431
  });
335
432
  }
336
- this.replace({ ...this.state, phase: 'ready', message: `${this.project.name} is ready`, result, failure: undefined });
433
+ this.replace({ ...this.state, project: activeProject, phase: 'ready', message: `${activeProject.name} is ready`, result, failure: undefined });
337
434
  return result;
338
435
  } catch (error) {
339
436
  const failure = failureFrom(error);
@@ -101,10 +101,11 @@ export const DogfoodStatusRail: React.FC<{
101
101
  export const DogfoodLanePicker: React.FC<{
102
102
  options: readonly DogfoodLaneOption[];
103
103
  selected: DogfoodLane;
104
+ fallbackLane?: DogfoodLane;
104
105
  onSelect: (lane: DogfoodLane) => void;
105
106
  colors?: Partial<DogfoodUiColors>;
106
107
  showUnsupportedReasons?: boolean;
107
- }> = ({ options, selected, onSelect, colors: colorOverrides, showUnsupportedReasons = true }) => {
108
+ }> = ({ options, selected, fallbackLane, onSelect, colors: colorOverrides, showUnsupportedReasons = true }) => {
108
109
  const colors = resolvedColors(colorOverrides);
109
110
  return (
110
111
  <View accessibilityRole="radiogroup" accessibilityLabel="Dogfood runtime lane">
@@ -128,7 +129,8 @@ export const DogfoodLanePicker: React.FC<{
128
129
  ]}
129
130
  >
130
131
  <Text style={[styles.choiceText, { color: colors.text }, active && styles.choiceTextActive]}>
131
- {option.label}{option.default ? ' · default' : ''}
132
+ {option.label}
133
+ {active ? ' · preferred' : fallbackLane === option.lane ? ' · automatic fallback' : option.default ? ' · default' : ''}
132
134
  </Text>
133
135
  </Pressable>
134
136
  );
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import {
3
3
  ActivityIndicator,
4
4
  DeviceEventEmitter,
5
+ KeyboardAvoidingView,
5
6
  Linking,
6
7
  Modal,
7
8
  Platform,
@@ -40,6 +41,7 @@ import {
40
41
  import {
41
42
  DogfoodController,
42
43
  defaultDogfoodLane,
44
+ dogfoodLanePlan,
43
45
  dogfoodLaneOptions,
44
46
  type DogfoodLane,
45
47
  type DogfoodSnapshot,
@@ -389,12 +391,17 @@ export const FeedbackModal: React.FC = () => {
389
391
  if (!client || !dogfoodProject || !onboarding) return;
390
392
  await dogfoodControllerRef.current?.stop().catch(() => {});
391
393
  const framework = dogfoodProject.framework || onboarding.framework || 'expo';
394
+ const lanePlan = dogfoodLanePlan(framework, {
395
+ nativeRuntimeAvailable: dogfoodNativeAvailable,
396
+ browserRuntimeAvailable: dogfoodBrowserAvailable,
397
+ }, dogfoodLane);
392
398
  const controller = new DogfoodController({
393
399
  name: dogfoodProject.name,
394
400
  workDir: dogfoodProject.path,
395
401
  framework,
396
- lane: dogfoodLane,
397
- nativeTargetId: dogfoodLane === 'webrtc' ? dogfoodNativeTargetId : undefined,
402
+ lane: lanePlan.preferred,
403
+ fallbackLane: lanePlan.fallback,
404
+ nativeTargetId: lanePlan.preferred === 'webrtc' ? dogfoodNativeTargetId : undefined,
398
405
  }, createP2PDogfoodDriver(client), {
399
406
  onChange: (snapshot) => { if (mountedRef.current) setDogfoodRuntime(snapshot); },
400
407
  });
@@ -493,9 +500,6 @@ export const FeedbackModal: React.FC = () => {
493
500
  } else if (device.needsAuth) {
494
501
  status = 'attention';
495
502
  detail = 'Machine needs pairing again before feedback actions can run.';
496
- } else if (device.runnerDown) {
497
- status = 'attention';
498
- detail = 'Machine is online but the coding agent is down.';
499
503
  } else if (reachable === false) {
500
504
  status = 'offline';
501
505
  detail = 'Machine selected, but the agent is not responding.';
@@ -608,7 +612,11 @@ export const FeedbackModal: React.FC = () => {
608
612
  setShowVibeInput(authenticated || directDogfood);
609
613
  setVibePrompt('');
610
614
  if (onboarding) {
611
- setActiveTab(authenticated ? 'chat' : 'settings');
615
+ // A Dogfood shortcut is an explicit setup/runtime intent. Opening on
616
+ // Chat hid the machine/runner/checkout gate for signed-in SFMG users;
617
+ // keep the SDK-owned Dogfood surface visible, then show its live logs
618
+ // immediately when Start is tapped.
619
+ setActiveTab('settings');
612
620
  setDogfoodSetupStage('setup');
613
621
  setDogfoodExpandedStep(null);
614
622
  setDogfoodRuntime(null);
@@ -1052,10 +1060,15 @@ export const FeedbackModal: React.FC = () => {
1052
1060
  const dogfoodModelReady = !selectedDogfoodRunner?.models?.length
1053
1061
  || !!preferredModel && selectedDogfoodRunner.models.some((model) => model.id === preferredModel);
1054
1062
  const dogfoodFramework = dogfoodProject?.framework || YaverFeedback.getDogfoodOnboarding()?.framework || 'expo';
1063
+ const dogfoodOnboarding = YaverFeedback.getDogfoodOnboarding();
1055
1064
  const dogfoodLaneChoices = dogfoodLaneOptions(dogfoodFramework, {
1056
1065
  nativeRuntimeAvailable: dogfoodNativeAvailable,
1057
1066
  browserRuntimeAvailable: dogfoodBrowserAvailable,
1058
1067
  });
1068
+ const dogfoodLanePolicy = dogfoodLanePlan(dogfoodFramework, {
1069
+ nativeRuntimeAvailable: dogfoodNativeAvailable,
1070
+ browserRuntimeAvailable: dogfoodBrowserAvailable,
1071
+ }, dogfoodLane);
1059
1072
  const selectedDogfoodNativeTarget = dogfoodNativeTargets.find((target) => target.id === dogfoodNativeTargetId) || null;
1060
1073
  const dogfoodLaneReady = dogfoodLaneChoices.some((option) => option.lane === dogfoodLane && option.supported)
1061
1074
  && (dogfoodLane !== 'webrtc' || !!selectedDogfoodNativeTarget?.enabled);
@@ -1092,11 +1105,12 @@ export const FeedbackModal: React.FC = () => {
1092
1105
  },
1093
1106
  ];
1094
1107
  const dogfoodStartBlocked = !dogfoodSetupReady || !dogfoodLaneReady;
1095
- const dogfoodSourceLabel = dogfoodLane === 'webrtc'
1108
+ const activeDogfoodLane = dogfoodRuntime?.project.lane || dogfoodLane;
1109
+ const dogfoodSourceLabel = activeDogfoodLane === 'webrtc'
1096
1110
  ? selectedDogfoodNativeTarget
1097
1111
  ? [selectedDogfoodNativeTarget.label, selectedDogfoodNativeTarget.platform].filter(Boolean).join(' · ')
1098
1112
  : 'Native simulator, emulator, or device'
1099
- : dogfoodLane === 'hermes'
1113
+ : activeDogfoodLane === 'hermes'
1100
1114
  ? `Hermes build · ${machineCard.title}`
1101
1115
  : `${dogfoodFramework === 'flutter' ? 'Flutter web compiler' : 'Metro / browser build'} · ${machineCard.title}`;
1102
1116
 
@@ -1161,7 +1175,10 @@ export const FeedbackModal: React.FC = () => {
1161
1175
  transparent
1162
1176
  onRequestClose={handleClose}
1163
1177
  >
1164
- <View style={styles.overlay}>
1178
+ <KeyboardAvoidingView
1179
+ style={styles.overlay}
1180
+ behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
1181
+ >
1165
1182
  <Pressable style={styles.backdrop} onPress={handleClose} accessibilityLabel="Close feedback" />
1166
1183
  <View
1167
1184
  // Tablet: cap modal width and center as a card-style
@@ -1187,11 +1204,18 @@ export const FeedbackModal: React.FC = () => {
1187
1204
  keyboardShouldPersistTaps="handled"
1188
1205
  keyboardDismissMode={Platform.OS === 'ios' ? 'interactive' : 'on-drag'}
1189
1206
  contentInsetAdjustmentBehavior="always"
1190
- automaticallyAdjustKeyboardInsets={Platform.OS === 'ios'}
1207
+ // KeyboardAvoidingView is the single inset owner. Letting the
1208
+ // ScrollView add a second UIKit inset lifts the composer twice
1209
+ // on short phones and still leaves its action row clipped.
1210
+ automaticallyAdjustKeyboardInsets={false}
1191
1211
  >
1192
1212
  <View style={styles.header}>
1193
1213
  <Text style={styles.title}>
1194
- {dogfoodActive ? `${YaverFeedback.getDogfoodStatus().label || 'App'} Developer Mode` : 'Send Feedback'}
1214
+ {dogfoodOnboarding
1215
+ ? `Set up ${dogfoodOnboarding.projectName || dogfoodOnboarding.label || 'this app'} Dogfood`
1216
+ : dogfoodActive
1217
+ ? `${YaverFeedback.getDogfoodStatus().label || 'App'} Developer Mode`
1218
+ : 'Send Feedback'}
1195
1219
  </Text>
1196
1220
  <Pressable
1197
1221
  onPress={handleClose}
@@ -1204,6 +1228,7 @@ export const FeedbackModal: React.FC = () => {
1204
1228
  </Pressable>
1205
1229
  </View>
1206
1230
 
1231
+ {(!dogfoodOnboarding || dogfoodSetupStage === 'runtime') ? (
1207
1232
  <View style={styles.tabs} accessibilityRole="tablist">
1208
1233
  {(['chat', 'settings'] as const).map((tab) => (
1209
1234
  <Pressable
@@ -1217,15 +1242,16 @@ export const FeedbackModal: React.FC = () => {
1217
1242
  </Pressable>
1218
1243
  ))}
1219
1244
  </View>
1245
+ ) : null}
1220
1246
 
1221
1247
  <View style={[styles.tabContent, activeTab !== 'settings' && styles.hidden]}>
1222
1248
  <>
1223
- {YaverFeedback.getDogfoodOnboarding() ? (
1249
+ {dogfoodOnboarding ? (
1224
1250
  <View style={styles.dogfoodWizard}>
1225
- <Text style={styles.dogfoodWizardTitle}>Dogfood this app</Text>
1251
+ <Text style={styles.dogfoodWizardTitle}>Connect the app to its checkout</Text>
1226
1252
  <Text style={styles.dogfoodWizardHint}>
1227
1253
  {dogfoodEnrollment?.status === 'active'
1228
- ? 'Signed in · this installation is approved'
1254
+ ? 'Choose one development machine, coding runner, and checkout. Then choose how to preview it.'
1229
1255
  : `Signed in · installation ${dogfoodEnrollment?.status || 'checking'}`}
1230
1256
  </Text>
1231
1257
  {dogfoodEnrollment?.status === 'active' && dogfoodSetupStage === 'setup' ? (
@@ -1322,7 +1348,7 @@ export const FeedbackModal: React.FC = () => {
1322
1348
  </View>
1323
1349
  ) : null}
1324
1350
  <ActionRow
1325
- label="Choose runtime"
1351
+ label={dogfoodSetupReady ? 'Continue to runtime' : 'Complete the choices above'}
1326
1352
  tint="#5645d8"
1327
1353
  onPress={() => {
1328
1354
  setDogfoodExpandedStep(null);
@@ -1347,6 +1373,7 @@ export const FeedbackModal: React.FC = () => {
1347
1373
  <DogfoodLanePicker
1348
1374
  options={dogfoodLaneChoices}
1349
1375
  selected={dogfoodLane}
1376
+ fallbackLane={dogfoodLanePolicy.fallback}
1350
1377
  colors={FEEDBACK_DOGFOOD_LIGHT_COLORS}
1351
1378
  onSelect={(lane) => {
1352
1379
  setDogfoodLane(lane);
@@ -1425,6 +1452,7 @@ export const FeedbackModal: React.FC = () => {
1425
1452
  )}
1426
1453
  </View>
1427
1454
  ) : null}
1455
+ {!dogfoodOnboarding ? <>
1428
1456
  <Pressable
1429
1457
  onPress={() => {
1430
1458
  if (!YaverFeedback.isAuthed()) {
@@ -1702,6 +1730,7 @@ export const FeedbackModal: React.FC = () => {
1702
1730
  </Text>
1703
1731
  </View>
1704
1732
  ))}
1733
+ </> : null}
1705
1734
  </>
1706
1735
  </View>
1707
1736
 
@@ -1789,7 +1818,7 @@ export const FeedbackModal: React.FC = () => {
1789
1818
  </Pressable>
1790
1819
  </ScrollView>
1791
1820
  </View>
1792
- </View>
1821
+ </KeyboardAvoidingView>
1793
1822
  </Modal>
1794
1823
  )}
1795
1824
  {runnerAuthModal ? (
@@ -116,6 +116,8 @@ export interface YaverLoginScreenProps {
116
116
  onLoggedIn: (token: string) => void;
117
117
  /** Optional cancel button shown in header. */
118
118
  onCancel?: () => void;
119
+ /** Intent shown under the Yaver mark. Defaults to ordinary feedback. */
120
+ subtitle?: string;
119
121
  }
120
122
 
121
123
  /**
@@ -126,6 +128,7 @@ export interface YaverLoginScreenProps {
126
128
  export const YaverLoginScreen: React.FC<YaverLoginScreenProps> = ({
127
129
  onLoggedIn,
128
130
  onCancel,
131
+ subtitle = 'Sign in to send feedback',
129
132
  }) => {
130
133
  const [busyProvider, setBusyProvider] = useState<OAuthProvider | 'apple' | null>(null);
131
134
  const [showEmailForm, setShowEmailForm] = useState(false);
@@ -242,7 +245,7 @@ export const YaverLoginScreen: React.FC<YaverLoginScreenProps> = ({
242
245
 
243
246
  <View style={styles.header}>
244
247
  <Text style={styles.logo}>Yaver</Text>
245
- <Text style={styles.subtitle}>Sign in to send feedback</Text>
248
+ <Text style={styles.subtitle}>{subtitle}</Text>
246
249
  </View>
247
250
 
248
251
  <View style={styles.buttons}>