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.
- package/dist/AuthOverlay.js +4 -2
- package/dist/DogfoodRuntime.d.ts +26 -0
- package/dist/DogfoodRuntime.js +85 -11
- package/dist/DogfoodSessionUi.d.ts +1 -0
- package/dist/DogfoodSessionUi.js +3 -2
- package/dist/FeedbackModal.js +126 -105
- package/dist/LoginScreen.d.ts +2 -0
- package/dist/LoginScreen.js +2 -2
- package/dist/MachinePickerScreen.d.ts +3 -1
- package/dist/MachinePickerScreen.js +43 -28
- package/dist/__tests__/DogfoodRuntime.test.js +45 -0
- package/dist/__tests__/FeedbackModalContract.test.js +16 -6
- package/dist/__tests__/MachinePickerScreenContract.test.d.ts +1 -0
- package/dist/__tests__/MachinePickerScreenContract.test.js +25 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -2
- package/package.json +1 -1
- package/src/AuthOverlay.tsx +8 -1
- package/src/DogfoodRuntime.ts +108 -11
- package/src/DogfoodSessionUi.tsx +4 -2
- package/src/FeedbackModal.tsx +45 -16
- package/src/LoginScreen.tsx +4 -1
- package/src/MachinePickerScreen.tsx +43 -30
- package/src/__tests__/DogfoodRuntime.test.ts +51 -0
- package/src/__tests__/FeedbackModalContract.test.ts +17 -6
- package/src/__tests__/MachinePickerScreenContract.test.ts +24 -0
- package/src/index.ts +2 -0
|
@@ -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("
|
|
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(
|
|
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:
|
|
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
|
|
64
|
-
expect(source).toContain(
|
|
65
|
-
expect(source).
|
|
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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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.
|
|
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.
|
|
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",
|
package/src/AuthOverlay.tsx
CHANGED
|
@@ -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
|
|
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
|
/>
|
package/src/DogfoodRuntime.ts
CHANGED
|
@@ -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
|
|
295
|
-
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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: `${
|
|
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);
|
package/src/DogfoodSessionUi.tsx
CHANGED
|
@@ -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}
|
|
132
|
+
{option.label}
|
|
133
|
+
{active ? ' · preferred' : fallbackLane === option.lane ? ' · automatic fallback' : option.default ? ' · default' : ''}
|
|
132
134
|
</Text>
|
|
133
135
|
</Pressable>
|
|
134
136
|
);
|
package/src/FeedbackModal.tsx
CHANGED
|
@@ -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:
|
|
397
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
:
|
|
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
|
-
<
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
1249
|
+
{dogfoodOnboarding ? (
|
|
1224
1250
|
<View style={styles.dogfoodWizard}>
|
|
1225
|
-
<Text style={styles.dogfoodWizardTitle}>
|
|
1251
|
+
<Text style={styles.dogfoodWizardTitle}>Connect the app to its checkout</Text>
|
|
1226
1252
|
<Text style={styles.dogfoodWizardHint}>
|
|
1227
1253
|
{dogfoodEnrollment?.status === 'active'
|
|
1228
|
-
? '
|
|
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=
|
|
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
|
-
</
|
|
1821
|
+
</KeyboardAvoidingView>
|
|
1793
1822
|
</Modal>
|
|
1794
1823
|
)}
|
|
1795
1824
|
{runnerAuthModal ? (
|
package/src/LoginScreen.tsx
CHANGED
|
@@ -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}>
|
|
248
|
+
<Text style={styles.subtitle}>{subtitle}</Text>
|
|
246
249
|
</View>
|
|
247
250
|
|
|
248
251
|
<View style={styles.buttons}>
|