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.
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useEffect, useState } from 'react';
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import {
3
3
  View,
4
4
  Text,
@@ -23,8 +23,10 @@ export interface YaverMachinePickerProps {
23
23
  token: string;
24
24
  /** Currently-selected deviceId (from config / cache) — highlighted. */
25
25
  currentDeviceId?: string;
26
- onPick: (device: RemoteDevice) => void;
26
+ onPick: (device: RemoteDevice) => void | Promise<void>;
27
27
  onCancel?: () => void;
28
+ /** Optional flow-specific title, e.g. "Choose a machine for SFMG". */
29
+ title?: string;
28
30
  }
29
31
 
30
32
  /**
@@ -39,6 +41,7 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
39
41
  currentDeviceId,
40
42
  onPick,
41
43
  onCancel,
44
+ title = 'Choose a machine',
42
45
  }) => {
43
46
  const [loading, setLoading] = useState(true);
44
47
  const [refreshing, setRefreshing] = useState(false);
@@ -46,32 +49,28 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
46
49
  const [list, setList] = useState<DeviceList>({ owned: [] });
47
50
  const [pairingDevice, setPairingDevice] = useState<RemoteDevice | null>(null);
48
51
  const [reachability, setReachability] = useState<Record<string, DeviceReachability | undefined>>({});
52
+ const [selectingDeviceId, setSelectingDeviceId] = useState<string | null>(null);
53
+ const mountedRef = useRef(true);
54
+ const loadGenerationRef = useRef(0);
49
55
 
50
56
  const load = useCallback(async (silent = false) => {
57
+ const generation = ++loadGenerationRef.current;
51
58
  if (!silent) setLoading(true);
52
59
  setError(null);
53
60
  try {
54
61
  const result = await listReachableDevices(token);
55
62
  setList(result);
56
63
  setReachability({});
57
- void (async () => {
58
- const devices = result.owned;
59
- const settled = await Promise.allSettled(
60
- devices.map(async (device) => ({
61
- deviceId: device.deviceId,
62
- result: await probeDeviceReachability(device),
63
- })),
64
- );
65
- setReachability((prev) => {
66
- const next = { ...prev };
67
- for (const entry of settled) {
68
- if (entry.status === 'fulfilled') {
69
- next[entry.value.deviceId] = entry.value.result;
70
- }
71
- }
72
- return next;
73
- });
74
- })();
64
+ // Convex already gives us a fresh, heartbeat-gated online answer. Render
65
+ // that immediately. Only probe cloud-offline rows to detect a phone-LAN
66
+ // route that came back before the next heartbeat, and publish each result
67
+ // as it arrives instead of holding the whole list behind the slowest box.
68
+ for (const device of result.owned.filter((candidate) => !candidate.isOnline)) {
69
+ void probeDeviceReachability(device).then((probe) => {
70
+ if (!mountedRef.current || loadGenerationRef.current !== generation) return;
71
+ setReachability((prev) => ({ ...prev, [device.deviceId]: probe }));
72
+ }).catch(() => {});
73
+ }
75
74
  if (result.owned.length === 0) {
76
75
  setError('No machines found yet. Run `yaver auth` + `yaver serve` on your machine.');
77
76
  }
@@ -84,7 +83,9 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
84
83
  }, [token]);
85
84
 
86
85
  useEffect(() => {
86
+ mountedRef.current = true;
87
87
  void load();
88
+ return () => { mountedRef.current = false; };
88
89
  }, [load]);
89
90
 
90
91
  const handlePick = async (device: RemoteDevice) => {
@@ -97,7 +98,9 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
97
98
  setPairingDevice(device);
98
99
  return;
99
100
  }
100
- const direct = await probeDeviceReachability(device);
101
+ const direct = device.isOnline
102
+ ? { reachable: true } as DeviceReachability
103
+ : await probeDeviceReachability(device);
101
104
  // Do not hard-block selection just because the LAN /health probe
102
105
  // failed. The standalone SDK can still reach a healthy machine via
103
106
  // the normal selected-device discovery path (including relay), and
@@ -110,12 +113,18 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
110
113
  setReachability((prev) => ({ ...prev, [device.deviceId]: direct }));
111
114
  return;
112
115
  }
113
- await saveSelectedDeviceId(device.deviceId);
114
- onPick(device);
116
+ setSelectingDeviceId(device.deviceId);
117
+ try {
118
+ await saveSelectedDeviceId(device.deviceId);
119
+ await onPick(device);
120
+ } finally {
121
+ if (mountedRef.current) setSelectingDeviceId(null);
122
+ }
115
123
  };
116
124
 
117
125
  const renderDevice = (device: RemoteDevice) => {
118
126
  const selected = device.deviceId === currentDeviceId;
127
+ const selecting = device.deviceId === selectingDeviceId;
119
128
  const probe = reachability[device.deviceId];
120
129
  // Trust Convex's `isOnline` — the backend already gates it on a
121
130
  // fresh 90 s heartbeat (see backend/convex/devices.ts
@@ -134,14 +143,16 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
134
143
  : effectivelyReachable
135
144
  ? '#22c55e'
136
145
  : device.isOnline
137
- ? '#f59e0b'
146
+ ? '#22c55e'
138
147
  : explicitlyOffline || !device.isOnline
139
148
  ? '#ef4444'
140
149
  : '#22c55e';
141
150
  // Derive a single short status phrase the user can act on.
142
151
  let statusLine = device.platform;
143
- if (probe === undefined) {
144
- statusLine = 'Checking connection…';
152
+ if (selecting) {
153
+ statusLine = 'Connecting…';
154
+ } else if (probe === undefined && device.isOnline) {
155
+ statusLine = device.platform || 'Online';
145
156
  } else if (!device.isOnline && effectivelyReachable) {
146
157
  statusLine = 'Reachable now — waiting for cloud status to refresh';
147
158
  } else if (!device.isOnline) {
@@ -162,13 +173,15 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
162
173
  key={device.deviceId}
163
174
  style={[styles.deviceRow, selected && styles.deviceSelected]}
164
175
  onPress={() => handlePick(device)}
176
+ disabled={selectingDeviceId !== null}
165
177
  >
166
178
  <View style={[styles.health, { backgroundColor: healthColor }]} />
167
179
  <View style={{ flex: 1 }}>
168
180
  <Text style={styles.deviceName}>{device.name || device.deviceId}</Text>
169
181
  <Text style={styles.deviceMeta}>{statusLine}</Text>
170
182
  </View>
171
- {selected && <Text style={styles.selectedBadge}>seçili</Text>}
183
+ {selecting ? <ActivityIndicator color="#a5b4fc" size="small" /> : null}
184
+ {selected && !selecting && <Text style={styles.selectedBadge}>selected</Text>}
172
185
  </TouchableOpacity>
173
186
  );
174
187
  };
@@ -176,10 +189,10 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
176
189
  return (
177
190
  <SafeAreaView style={styles.container}>
178
191
  <View style={styles.header}>
179
- <Text style={styles.title}>Makine Seç</Text>
192
+ <Text style={styles.title}>{title}</Text>
180
193
  {onCancel && (
181
194
  <TouchableOpacity onPress={onCancel} style={styles.cancel}>
182
- <Text style={styles.cancelText}>Kapat</Text>
195
+ <Text style={styles.cancelText}>Close</Text>
183
196
  </TouchableOpacity>
184
197
  )}
185
198
  </View>
@@ -203,7 +216,7 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
203
216
  <>
204
217
  {list.owned.length > 0 && (
205
218
  <View style={styles.section}>
206
- <Text style={styles.sectionTitle}>Kendi makinelerim</Text>
219
+ <Text style={styles.sectionTitle}>Your machines</Text>
207
220
  {list.owned.map(renderDevice)}
208
221
  </View>
209
222
  )}
@@ -2,6 +2,7 @@ import {
2
2
  DogfoodController,
3
3
  DogfoodRuntimeError,
4
4
  defaultDogfoodLane,
5
+ dogfoodLanePlan,
5
6
  dogfoodLaneOptions,
6
7
  dogfoodLogLinesFromDevEvent,
7
8
  validateDogfoodProject,
@@ -92,6 +93,41 @@ describe('DogfoodController', () => {
92
93
  await controller.stop();
93
94
  expect(stopSecond).toHaveBeenCalledTimes(1);
94
95
  });
96
+
97
+ test('keeps a failed preferred lane in the console and automatically recovers through browser', async () => {
98
+ const stopPreferred = jest.fn();
99
+ const lanes: string[] = [];
100
+ const controller = new DogfoodController({
101
+ ...expo,
102
+ lane: 'hermes',
103
+ fallbackLane: 'browser',
104
+ }, {
105
+ async start(ctx) {
106
+ lanes.push(ctx.project.lane);
107
+ if (ctx.project.lane === 'hermes') {
108
+ ctx.registerCleanup(stopPreferred);
109
+ throw new DogfoodRuntimeError({
110
+ code: 'DOGFOOD_HERMES_BUILD_FAILED',
111
+ error: 'Hermes build failed',
112
+ remedy: 'Use the browser build.',
113
+ retryable: true,
114
+ });
115
+ }
116
+ return { lane: 'browser', url: 'http://agent/dev/', metadata: { recovered: true } };
117
+ },
118
+ });
119
+
120
+ await expect(controller.trigger()).resolves.toMatchObject({
121
+ lane: 'browser',
122
+ metadata: { fallbackFrom: 'hermes', fallbackReason: 'DOGFOOD_HERMES_BUILD_FAILED' },
123
+ });
124
+ expect(lanes).toEqual(['hermes', 'browser']);
125
+ expect(stopPreferred).toHaveBeenCalledTimes(1);
126
+ expect(controller.snapshot()).toMatchObject({ phase: 'ready', project: { lane: 'browser' } });
127
+ expect(controller.snapshot().logs.map((line) => line.text)).toContain(
128
+ '[fallback] hermes failed (DOGFOOD_HERMES_BUILD_FAILED); trying browser',
129
+ );
130
+ });
95
131
  });
96
132
 
97
133
  describe('Dogfood lanes and console events', () => {
@@ -112,6 +148,21 @@ describe('Dogfood lanes and console events', () => {
112
148
  expect(flutter.find((option) => option.lane === 'hermes')?.supported).toBe(false);
113
149
  });
114
150
 
151
+ test('uses browser as the framework-aware default and as second choice after an explicit native preference', () => {
152
+ expect(dogfoodLanePlan('flutter', { nativeRuntimeAvailable: true })).toMatchObject({
153
+ preferred: 'browser', fallback: undefined,
154
+ });
155
+ expect(dogfoodLanePlan('expo', { nativeRuntimeAvailable: true }, 'hermes')).toMatchObject({
156
+ preferred: 'hermes', fallback: 'browser',
157
+ });
158
+ expect(dogfoodLanePlan('flutter', { nativeRuntimeAvailable: true }, 'webrtc')).toMatchObject({
159
+ preferred: 'webrtc', fallback: 'browser',
160
+ });
161
+ expect(dogfoodLanePlan('swift', { nativeRuntimeAvailable: true }, 'webrtc')).toMatchObject({
162
+ preferred: 'webrtc', fallback: undefined,
163
+ });
164
+ });
165
+
115
166
  test('keeps Yaver self-development on the same RN three-lane contract', () => {
116
167
  const options = dogfoodLaneOptions('expo', { nativeRuntimeAvailable: true, selfDevelopment: true });
117
168
  expect(options).toHaveLength(3);
@@ -46,12 +46,18 @@ describe('FeedbackModal authenticated chat contract', () => {
46
46
  });
47
47
 
48
48
  it('uses Chat as the authenticated entry surface without legacy command buttons', () => {
49
- expect(source).toContain("setActiveTab(authenticated ? 'chat' : 'settings')");
49
+ expect(source).toContain("useState<'chat' | 'settings'>('chat')");
50
50
  expect(source).toContain('setShowVibeInput(authenticated || directDogfood)');
51
51
  expect(source).not.toContain('Screenshot & Fix');
52
52
  expect(source).not.toContain('<DeployPanel');
53
53
  });
54
54
 
55
+ it('opens explicit Dogfood onboarding on setup and makes the runtime console the first live surface', () => {
56
+ expect(source).toContain("setActiveTab('settings')");
57
+ expect(source).toContain("setDogfoodSetupStage('runtime')");
58
+ expect(source).toMatch(/dogfoodSetupStage === 'runtime'[\s\S]*?<DogfoodLiveConsole/);
59
+ });
60
+
55
61
  it('keeps Dogfood setup to box, runner, and checkout before asking for a runtime', () => {
56
62
  const setupSteps = source.match(/const dogfoodSetupSteps = \[([\s\S]*?)\n \];/)?.[1] || '';
57
63
  expect([...setupSteps.matchAll(/key: '([^']+)'/g)].map((match) => match[1]))
@@ -61,18 +67,23 @@ describe('FeedbackModal authenticated chat contract', () => {
61
67
  expect(setupSteps).not.toContain("key: 'model'");
62
68
  expect(setupSteps).not.toContain("key: 'lane'");
63
69
  expect(source).toContain("type DogfoodSetupStage = 'setup' | 'lane' | 'runtime'");
64
- expect(source).toContain('label="Choose runtime"');
70
+ expect(source).toContain("label={dogfoodSetupReady ? 'Continue to runtime' : 'Complete the choices above'}");
71
+ expect(source).toContain('{!dogfoodOnboarding ? <>');
72
+ expect(source).toContain("? `Set up ${dogfoodOnboarding.projectName || dogfoodOnboarding.label || 'this app'} Dogfood`");
65
73
  });
66
74
 
67
75
  it('passes the selected native target and labels the live log source', () => {
68
- expect(source).toContain("nativeTargetId: dogfoodLane === 'webrtc' ? dogfoodNativeTargetId : undefined");
76
+ expect(source).toContain("nativeTargetId: lanePlan.preferred === 'webrtc' ? dogfoodNativeTargetId : undefined");
77
+ expect(source).toContain('fallbackLane: lanePlan.fallback');
78
+ expect(source).toContain('fallbackLane={dogfoodLanePolicy.fallback}');
69
79
  expect(source).toMatch(/<DogfoodLiveConsole[\s\S]*?sourceLabel=\{dogfoodSourceLabel\}/);
70
80
  expect(source).toContain('Simulator, emulator, or device');
71
81
  });
72
82
 
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');
83
+ it('has one keyboard inset owner and no gesture-stealing sheet Pressable', () => {
84
+ expect(source).toContain("behavior={Platform.OS === 'ios' ? 'padding' : 'height'}");
85
+ expect(source).toContain('automaticallyAdjustKeyboardInsets={false}');
86
+ expect(source).toContain('<KeyboardAvoidingView');
76
87
  expect(source).not.toContain('keyboardInset');
77
88
  expect(source).toContain('<Pressable style={styles.backdrop} onPress={handleClose}');
78
89
  expect(source).toMatch(/<View[\s\S]{0,300}?style=\{\[\s*styles\.modal/);
@@ -0,0 +1,24 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ const source = fs.readFileSync(path.join(__dirname, '..', 'MachinePickerScreen.tsx'), 'utf8');
5
+
6
+ describe('MachinePickerScreen progressive reachability contract', () => {
7
+ it('makes heartbeat-online machines immediately selectable', () => {
8
+ expect(source).toContain("const direct = device.isOnline");
9
+ expect(source).toContain("? { reachable: true } as DeviceReachability");
10
+ expect(source).not.toContain("statusLine = 'Checking connection…'");
11
+ expect(source).toContain("statusLine = device.platform || 'Online'");
12
+ });
13
+
14
+ it('does not hold every row behind the slowest direct probe', () => {
15
+ expect(source).not.toContain('Promise.allSettled');
16
+ expect(source).toContain("filter((candidate) => !candidate.isOnline)");
17
+ expect(source).toContain("setReachability((prev) => ({ ...prev, [device.deviceId]: probe }))");
18
+ });
19
+
20
+ it('names the selected machine connection operation', () => {
21
+ expect(source).toContain("statusLine = 'Connecting…'");
22
+ expect(source).toContain('disabled={selectingDeviceId !== null}');
23
+ });
24
+ });
package/src/index.ts CHANGED
@@ -92,6 +92,7 @@ export {
92
92
  DogfoodController,
93
93
  DogfoodRuntimeError,
94
94
  defaultDogfoodLane,
95
+ dogfoodLanePlan,
95
96
  dogfoodLaneOptions,
96
97
  dogfoodLogLinesFromDevEvent,
97
98
  runtimeLogLinesFromDevEvent,
@@ -103,6 +104,7 @@ export type {
103
104
  DogfoodFailure,
104
105
  DogfoodLane,
105
106
  DogfoodLaneOption,
107
+ DogfoodLanePlan,
106
108
  DogfoodLogLine,
107
109
  DogfoodPhase,
108
110
  DogfoodProject,