yaver-feedback-react-native 0.9.11 → 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/FeedbackModal.js +106 -99
- 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__/FeedbackModalContract.test.js +7 -4
- package/dist/__tests__/MachinePickerScreenContract.test.d.ts +1 -0
- package/dist/__tests__/MachinePickerScreenContract.test.js +25 -0
- package/package.json +1 -1
- package/src/AuthOverlay.tsx +8 -1
- package/src/FeedbackModal.tsx +24 -11
- package/src/LoginScreen.tsx +4 -1
- package/src/MachinePickerScreen.tsx +43 -30
- package/src/__tests__/FeedbackModalContract.test.ts +7 -4
- package/src/__tests__/MachinePickerScreenContract.test.ts +24 -0
package/dist/AuthOverlay.js
CHANGED
|
@@ -46,6 +46,8 @@ const AuthOverlay = () => {
|
|
|
46
46
|
const [pickerVisible, setPickerVisible] = (0, react_1.useState)(false);
|
|
47
47
|
const [token, setToken] = (0, react_1.useState)(null);
|
|
48
48
|
const activeOverlayRef = (0, react_1.useRef)('none');
|
|
49
|
+
const onboarding = YaverFeedback_1.YaverFeedback.getDogfoodOnboarding();
|
|
50
|
+
const dogfoodLabel = onboarding?.projectName || onboarding?.label || 'this app';
|
|
49
51
|
const openLogin = (0, react_1.useCallback)(() => {
|
|
50
52
|
activeOverlayRef.current = 'login';
|
|
51
53
|
setPickerVisible(false);
|
|
@@ -120,10 +122,10 @@ const AuthOverlay = () => {
|
|
|
120
122
|
};
|
|
121
123
|
return (<>
|
|
122
124
|
<react_native_1.Modal visible={loginVisible} animationType="slide" presentationStyle="fullScreen" onRequestClose={closeAll}>
|
|
123
|
-
<LoginScreen_1.YaverLoginScreen onLoggedIn={handleLoggedIn} onCancel={closeAll}/>
|
|
125
|
+
<LoginScreen_1.YaverLoginScreen onLoggedIn={handleLoggedIn} onCancel={closeAll} subtitle={onboarding ? `Sign in to set up ${dogfoodLabel} Dogfood` : undefined}/>
|
|
124
126
|
</react_native_1.Modal>
|
|
125
127
|
<react_native_1.Modal visible={pickerVisible && !!token} animationType="slide" presentationStyle="fullScreen" onRequestClose={closeAll}>
|
|
126
|
-
{token && (<MachinePickerScreen_1.YaverMachinePickerScreen token={token} currentDeviceId={YaverFeedback_1.YaverFeedback.getConfig()?.preferredDeviceId} onPick={handleDevicePicked} onCancel={closeAll}/>)}
|
|
128
|
+
{token && (<MachinePickerScreen_1.YaverMachinePickerScreen token={token} currentDeviceId={YaverFeedback_1.YaverFeedback.getConfig()?.preferredDeviceId} title={onboarding ? `Choose a machine for ${dogfoodLabel}` : undefined} onPick={handleDevicePicked} onCancel={closeAll}/>)}
|
|
127
129
|
</react_native_1.Modal>
|
|
128
130
|
</>);
|
|
129
131
|
};
|
package/dist/FeedbackModal.js
CHANGED
|
@@ -420,10 +420,6 @@ const FeedbackModal = () => {
|
|
|
420
420
|
status = 'attention';
|
|
421
421
|
detail = 'Machine needs pairing again before feedback actions can run.';
|
|
422
422
|
}
|
|
423
|
-
else if (device.runnerDown) {
|
|
424
|
-
status = 'attention';
|
|
425
|
-
detail = 'Machine is online but the coding agent is down.';
|
|
426
|
-
}
|
|
427
423
|
else if (reachable === false) {
|
|
428
424
|
status = 'offline';
|
|
429
425
|
detail = 'Machine selected, but the agent is not responding.';
|
|
@@ -963,6 +959,7 @@ const FeedbackModal = () => {
|
|
|
963
959
|
const dogfoodModelReady = !selectedDogfoodRunner?.models?.length
|
|
964
960
|
|| !!preferredModel && selectedDogfoodRunner.models.some((model) => model.id === preferredModel);
|
|
965
961
|
const dogfoodFramework = dogfoodProject?.framework || YaverFeedback_1.YaverFeedback.getDogfoodOnboarding()?.framework || 'expo';
|
|
962
|
+
const dogfoodOnboarding = YaverFeedback_1.YaverFeedback.getDogfoodOnboarding();
|
|
966
963
|
const dogfoodLaneChoices = (0, DogfoodRuntime_1.dogfoodLaneOptions)(dogfoodFramework, {
|
|
967
964
|
nativeRuntimeAvailable: dogfoodNativeAvailable,
|
|
968
965
|
browserRuntimeAvailable: dogfoodBrowserAvailable,
|
|
@@ -1045,7 +1042,7 @@ const FeedbackModal = () => {
|
|
|
1045
1042
|
<QuickActionIcon_1.QuickActionIcon />
|
|
1046
1043
|
<YaverModeBadgeGate />
|
|
1047
1044
|
{visible && (<react_native_1.Modal visible={visible} animationType="slide" transparent onRequestClose={handleClose}>
|
|
1048
|
-
<react_native_1.
|
|
1045
|
+
<react_native_1.KeyboardAvoidingView style={styles.overlay} behavior={react_native_1.Platform.OS === 'ios' ? 'padding' : 'height'}>
|
|
1049
1046
|
<react_native_1.Pressable style={styles.backdrop} onPress={handleClose} accessibilityLabel="Close feedback"/>
|
|
1050
1047
|
<react_native_1.View
|
|
1051
1048
|
// Tablet: cap modal width and center as a card-style
|
|
@@ -1063,29 +1060,37 @@ const FeedbackModal = () => {
|
|
|
1063
1060
|
}
|
|
1064
1061
|
: null,
|
|
1065
1062
|
]}>
|
|
1066
|
-
<react_native_1.ScrollView ref={scrollRef} style={styles.scroll} contentContainerStyle={styles.scrollContent} keyboardShouldPersistTaps="handled" keyboardDismissMode={react_native_1.Platform.OS === 'ios' ? 'interactive' : 'on-drag'} contentInsetAdjustmentBehavior="always"
|
|
1063
|
+
<react_native_1.ScrollView ref={scrollRef} style={styles.scroll} contentContainerStyle={styles.scrollContent} keyboardShouldPersistTaps="handled" keyboardDismissMode={react_native_1.Platform.OS === 'ios' ? 'interactive' : 'on-drag'} contentInsetAdjustmentBehavior="always"
|
|
1064
|
+
// KeyboardAvoidingView is the single inset owner. Letting the
|
|
1065
|
+
// ScrollView add a second UIKit inset lifts the composer twice
|
|
1066
|
+
// on short phones and still leaves its action row clipped.
|
|
1067
|
+
automaticallyAdjustKeyboardInsets={false}>
|
|
1067
1068
|
<react_native_1.View style={styles.header}>
|
|
1068
1069
|
<react_native_1.Text style={styles.title}>
|
|
1069
|
-
{
|
|
1070
|
+
{dogfoodOnboarding
|
|
1071
|
+
? `Set up ${dogfoodOnboarding.projectName || dogfoodOnboarding.label || 'this app'} Dogfood`
|
|
1072
|
+
: dogfoodActive
|
|
1073
|
+
? `${YaverFeedback_1.YaverFeedback.getDogfoodStatus().label || 'App'} Developer Mode`
|
|
1074
|
+
: 'Send Feedback'}
|
|
1070
1075
|
</react_native_1.Text>
|
|
1071
1076
|
<react_native_1.Pressable onPress={handleClose} hitSlop={12} style={styles.closeBtn} accessibilityRole="button" accessibilityLabel="Close">
|
|
1072
1077
|
<react_native_1.Text style={styles.closeIcon}>×</react_native_1.Text>
|
|
1073
1078
|
</react_native_1.Pressable>
|
|
1074
1079
|
</react_native_1.View>
|
|
1075
1080
|
|
|
1076
|
-
<react_native_1.View style={styles.tabs} accessibilityRole="tablist">
|
|
1081
|
+
{(!dogfoodOnboarding || dogfoodSetupStage === 'runtime') ? (<react_native_1.View style={styles.tabs} accessibilityRole="tablist">
|
|
1077
1082
|
{['chat', 'settings'].map((tab) => (<react_native_1.Pressable key={tab} onPress={() => setActiveTab(tab)} style={[styles.tab, activeTab === tab && styles.tabSelected]} accessibilityRole="tab" accessibilityState={{ selected: activeTab === tab }}>
|
|
1078
1083
|
<react_native_1.Text style={[styles.tabText, activeTab === tab && styles.tabTextSelected]}>{tab === 'chat' ? 'Chat' : 'Settings'}</react_native_1.Text>
|
|
1079
1084
|
</react_native_1.Pressable>))}
|
|
1080
|
-
</react_native_1.View>
|
|
1085
|
+
</react_native_1.View>) : null}
|
|
1081
1086
|
|
|
1082
1087
|
<react_native_1.View style={[styles.tabContent, activeTab !== 'settings' && styles.hidden]}>
|
|
1083
1088
|
<>
|
|
1084
|
-
{
|
|
1085
|
-
<react_native_1.Text style={styles.dogfoodWizardTitle}>
|
|
1089
|
+
{dogfoodOnboarding ? (<react_native_1.View style={styles.dogfoodWizard}>
|
|
1090
|
+
<react_native_1.Text style={styles.dogfoodWizardTitle}>Connect the app to its checkout</react_native_1.Text>
|
|
1086
1091
|
<react_native_1.Text style={styles.dogfoodWizardHint}>
|
|
1087
1092
|
{dogfoodEnrollment?.status === 'active'
|
|
1088
|
-
? '
|
|
1093
|
+
? 'Choose one development machine, coding runner, and checkout. Then choose how to preview it.'
|
|
1089
1094
|
: `Signed in · installation ${dogfoodEnrollment?.status || 'checking'}`}
|
|
1090
1095
|
</react_native_1.Text>
|
|
1091
1096
|
{dogfoodEnrollment?.status === 'active' && dogfoodSetupStage === 'setup' ? (<DogfoodSessionUi_1.DogfoodStatusRail steps={dogfoodSetupSteps} colors={FeedbackModalTheme_1.FEEDBACK_DOGFOOD_LIGHT_COLORS}/>) : null}
|
|
@@ -1139,7 +1144,7 @@ const FeedbackModal = () => {
|
|
|
1139
1144
|
<react_native_1.Text style={styles.dogfoodProjectDetail}>{dogfoodCheckoutDetail(project)}</react_native_1.Text>
|
|
1140
1145
|
</react_native_1.Pressable>))}
|
|
1141
1146
|
</react_native_1.View>) : null}
|
|
1142
|
-
<ActionRow label=
|
|
1147
|
+
<ActionRow label={dogfoodSetupReady ? 'Continue to runtime' : 'Complete the choices above'} tint="#5645d8" onPress={() => {
|
|
1143
1148
|
setDogfoodExpandedStep(null);
|
|
1144
1149
|
setDogfoodSetupStage('lane');
|
|
1145
1150
|
}} disabled={!dogfoodSetupReady}/>
|
|
@@ -1200,26 +1205,27 @@ const FeedbackModal = () => {
|
|
|
1200
1205
|
</>) : null}
|
|
1201
1206
|
</>)}
|
|
1202
1207
|
</react_native_1.View>) : null}
|
|
1208
|
+
{!dogfoodOnboarding ? <>
|
|
1203
1209
|
<react_native_1.Pressable onPress={() => {
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1210
|
+
if (!YaverFeedback_1.YaverFeedback.isAuthed()) {
|
|
1211
|
+
YaverFeedback_1.YaverFeedback.showLogin();
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
YaverFeedback_1.YaverFeedback.showMachinePicker();
|
|
1215
|
+
}} style={[
|
|
1216
|
+
styles.machineCard,
|
|
1217
|
+
machineCard.status === 'live' && styles.machineCardLive,
|
|
1218
|
+
machineCard.status === 'attention' && styles.machineCardAttention,
|
|
1219
|
+
machineCard.status === 'offline' && styles.machineCardOffline,
|
|
1220
|
+
]}>
|
|
1215
1221
|
<react_native_1.View style={styles.machineHeader}>
|
|
1216
1222
|
<react_native_1.View style={styles.machineTitleWrap}>
|
|
1217
1223
|
<react_native_1.View style={[
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1224
|
+
styles.machineDot,
|
|
1225
|
+
machineCard.status === 'live' && styles.machineDotLive,
|
|
1226
|
+
machineCard.status === 'attention' && styles.machineDotAttention,
|
|
1227
|
+
machineCard.status === 'offline' && styles.machineDotOffline,
|
|
1228
|
+
]}/>
|
|
1223
1229
|
<react_native_1.Text style={styles.machineLabel}>Coding Machine</react_native_1.Text>
|
|
1224
1230
|
</react_native_1.View>
|
|
1225
1231
|
<react_native_1.Text style={styles.machineAction}>
|
|
@@ -1249,14 +1255,14 @@ const FeedbackModal = () => {
|
|
|
1249
1255
|
<react_native_1.Text style={styles.runnerSectionTitle}>Coding Agents</react_native_1.Text>
|
|
1250
1256
|
<react_native_1.Text style={styles.runnerSectionSubtitle}>
|
|
1251
1257
|
{runnerStatusLoading
|
|
1252
|
-
|
|
1253
|
-
|
|
1258
|
+
? 'Refreshing runner status on the selected machine…'
|
|
1259
|
+
: `${readyRunnerCount} ready · ${needsAuthRunnerCount} need sign-in · ${missingRunnerCount} missing`}
|
|
1254
1260
|
</react_native_1.Text>
|
|
1255
1261
|
</react_native_1.View>
|
|
1256
1262
|
<react_native_1.Pressable onPress={() => void loadRunnerStatuses()} style={({ pressed }) => [
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1263
|
+
styles.runnerRefreshBtn,
|
|
1264
|
+
pressed && styles.buttonPressed,
|
|
1265
|
+
]} accessibilityRole="button" accessibilityLabel="Refresh coding-agent status">
|
|
1260
1266
|
<react_native_1.Text style={styles.runnerRefreshBtnText}>
|
|
1261
1267
|
{runnerStatusLoading ? 'Refreshing…' : 'Refresh'}
|
|
1262
1268
|
</react_native_1.Text>
|
|
@@ -1271,31 +1277,31 @@ const FeedbackModal = () => {
|
|
|
1271
1277
|
</react_native_1.View>
|
|
1272
1278
|
|
|
1273
1279
|
{runnerCards.map((row) => (<react_native_1.View key={row.id} style={[
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1280
|
+
styles.runnerCard,
|
|
1281
|
+
row.tone === 'ok' && styles.runnerCardOk,
|
|
1282
|
+
row.tone === 'warning' && styles.runnerCardWarning,
|
|
1283
|
+
row.tone === 'error' && styles.runnerCardError,
|
|
1284
|
+
]}>
|
|
1279
1285
|
<react_native_1.View style={styles.runnerCardTop}>
|
|
1280
1286
|
<react_native_1.View style={{ flex: 1 }}>
|
|
1281
1287
|
<react_native_1.Text style={styles.runnerCardTitle}>{row.name}</react_native_1.Text>
|
|
1282
1288
|
<react_native_1.Text style={[
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1289
|
+
styles.runnerCardStatus,
|
|
1290
|
+
row.tone === 'ok' && styles.runnerCardStatusOk,
|
|
1291
|
+
row.tone === 'warning' && styles.runnerCardStatusWarning,
|
|
1292
|
+
row.tone === 'error' && styles.runnerCardStatusError,
|
|
1293
|
+
]}>
|
|
1288
1294
|
{row.statusLine}
|
|
1289
1295
|
</react_native_1.Text>
|
|
1290
1296
|
</react_native_1.View>
|
|
1291
1297
|
<react_native_1.View style={styles.runnerCardActions}>
|
|
1292
1298
|
{row.ready ? (<react_native_1.Pressable onPress={() => {
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
+
const nextModel = row.models?.find((model) => model.isDefault)?.id || row.models?.[0]?.id || '';
|
|
1300
|
+
setPreferredRunnerState(row.id);
|
|
1301
|
+
setPreferredModelState(nextModel);
|
|
1302
|
+
void (0, preferences_1.setPreferredRunner)(row.id);
|
|
1303
|
+
void (0, preferences_1.setPreferredModel)(nextModel || null);
|
|
1304
|
+
}} style={({ pressed }) => [styles.runnerActionBtn, preferredRunner === row.id && styles.runnerActionBtnSelected, pressed && styles.buttonPressed]} accessibilityRole="button" accessibilityLabel={`Use ${row.name} for Vibing`}>
|
|
1299
1305
|
<react_native_1.Text style={styles.runnerActionBtnText}>{preferredRunner === row.id ? 'Using' : 'Use'}</react_native_1.Text>
|
|
1300
1306
|
</react_native_1.Pressable>) : null}
|
|
1301
1307
|
{row.actionRunner ? (<react_native_1.Pressable onPress={() => setRunnerAuthModal(row.actionRunner ?? null)} style={({ pressed }) => [styles.runnerActionBtn, pressed && styles.buttonPressed]} accessibilityRole="button" accessibilityLabel={`${row.actionLabel} ${row.name}`}>
|
|
@@ -1305,9 +1311,9 @@ const FeedbackModal = () => {
|
|
|
1305
1311
|
</react_native_1.View>
|
|
1306
1312
|
{preferredRunner === row.id && (row.models?.length || 0) > 0 ? (<react_native_1.ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.modelChoiceRow}>
|
|
1307
1313
|
{row.models.map((model) => (<react_native_1.Pressable key={model.id} onPress={() => {
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1314
|
+
setPreferredModelState(model.id);
|
|
1315
|
+
void (0, preferences_1.setPreferredModel)(model.id);
|
|
1316
|
+
}} style={[styles.modelChoice, preferredModel === model.id && styles.modelChoiceSelected]} accessibilityRole="button" accessibilityLabel={`Use ${model.name || model.id} model`}>
|
|
1311
1317
|
<react_native_1.Text style={[styles.modelChoiceText, preferredModel === model.id && styles.modelChoiceTextSelected]}>{model.name || model.id}</react_native_1.Text>
|
|
1312
1318
|
</react_native_1.Pressable>))}
|
|
1313
1319
|
</react_native_1.ScrollView>) : null}
|
|
@@ -1322,22 +1328,22 @@ const FeedbackModal = () => {
|
|
|
1322
1328
|
Quick access icon is hidden. Shake the phone if you want feedback back fast.
|
|
1323
1329
|
</react_native_1.Text>
|
|
1324
1330
|
<react_native_1.Pressable onPress={() => {
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
+
void YaverFeedback_1.YaverFeedback.setQuickIconVisible(true);
|
|
1332
|
+
setQuickIconHidden(false);
|
|
1333
|
+
}} style={({ pressed }) => [
|
|
1334
|
+
styles.quickIconToggle,
|
|
1335
|
+
pressed && styles.buttonPressed,
|
|
1336
|
+
]}>
|
|
1331
1337
|
<react_native_1.Text style={styles.quickIconToggleText}>Show quick icon again</react_native_1.Text>
|
|
1332
1338
|
</react_native_1.Pressable>
|
|
1333
1339
|
</react_native_1.View>)}
|
|
1334
1340
|
|
|
1335
1341
|
{YaverFeedback_1.YaverFeedback.isAuthed() ? (<react_native_1.Pressable onPress={() => {
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1342
|
+
void YaverFeedback_1.YaverFeedback.signOut().then(() => {
|
|
1343
|
+
handleClose();
|
|
1344
|
+
YaverFeedback_1.YaverFeedback.showLogin();
|
|
1345
|
+
});
|
|
1346
|
+
}} style={({ pressed }) => [styles.yaverSignOutBtn, pressed && styles.buttonPressed]} accessibilityRole="button" accessibilityLabel="Sign out of Yaver">
|
|
1341
1347
|
<react_native_1.Text style={styles.yaverSignOutText}>Sign out of Yaver</react_native_1.Text>
|
|
1342
1348
|
</react_native_1.Pressable>) : null}
|
|
1343
1349
|
|
|
@@ -1348,57 +1354,58 @@ const FeedbackModal = () => {
|
|
|
1348
1354
|
</react_native_1.Text>
|
|
1349
1355
|
<react_native_1.View style={styles.iconSelectorGrid}>
|
|
1350
1356
|
{Object.entries(preferences_1.QUICK_ICON_COLOR_PRESETS).map(([preset, colors]) => {
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1357
|
+
const selected = quickIconColorPreset === preset;
|
|
1358
|
+
return (<react_native_1.Pressable key={preset} onPress={() => {
|
|
1359
|
+
setQuickIconColorPreset(preset);
|
|
1360
|
+
void YaverFeedback_1.YaverFeedback.setQuickIconColorPreset(preset);
|
|
1361
|
+
}} style={[
|
|
1362
|
+
styles.iconOption,
|
|
1363
|
+
iconOptionWidthOverride ? { width: iconOptionWidthOverride } : null,
|
|
1364
|
+
selected && styles.iconOptionSelected,
|
|
1365
|
+
]}>
|
|
1360
1366
|
<react_native_1.View style={[
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1367
|
+
styles.iconOptionCircle,
|
|
1368
|
+
{
|
|
1369
|
+
backgroundColor: colors.backgroundColor,
|
|
1370
|
+
borderColor: colors.borderColor,
|
|
1371
|
+
shadowColor: colors.shadowColor,
|
|
1372
|
+
},
|
|
1373
|
+
]}>
|
|
1368
1374
|
<react_native_1.Text style={[
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1375
|
+
styles.iconOptionLabel,
|
|
1376
|
+
{ color: colors.foregroundColor },
|
|
1377
|
+
]}>
|
|
1372
1378
|
y
|
|
1373
1379
|
</react_native_1.Text>
|
|
1374
1380
|
</react_native_1.View>
|
|
1375
1381
|
<react_native_1.Text style={styles.iconOptionText}>{colors.label}</react_native_1.Text>
|
|
1376
1382
|
</react_native_1.Pressable>);
|
|
1377
|
-
|
|
1383
|
+
})}
|
|
1378
1384
|
</react_native_1.View>
|
|
1379
1385
|
</react_native_1.View>
|
|
1380
1386
|
|
|
1381
1387
|
{/* 1. Reload — Hot / Full / Rebuild Bundle.
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1388
|
+
Rendered from the shared decision seam, so a production
|
|
1389
|
+
build (__DEV__ false) renders nothing here at all, and a
|
|
1390
|
+
blocked action shows greyed WITH its reason underneath
|
|
1391
|
+
rather than vanishing. */}
|
|
1386
1392
|
{availableReloadActions.map((reloadAction) => (<react_native_1.View key={reloadAction.id} style={styles.reloadRow}>
|
|
1387
1393
|
<ActionRow label={reloadingId === reloadAction.id
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1394
|
+
? `${reloadAction.label}…`
|
|
1395
|
+
: reloadAction.label} tint={reloadAction.id === 'rebuild' ? '#0369a1' : '#9a5700'} onPress={() => {
|
|
1396
|
+
void handleReloadAction(reloadAction);
|
|
1397
|
+
}}
|
|
1398
|
+
// Never `disabled` at the Pressable level for a blocked
|
|
1399
|
+
// action: we WANT the tap so we can say why. Only a
|
|
1400
|
+
// genuinely busy modal blocks the press.
|
|
1401
|
+
disabled={busy && reloadingId !== reloadAction.id} busy={reloadingId === reloadAction.id}/>
|
|
1396
1402
|
<react_native_1.Text style={styles.reloadHint}>
|
|
1397
1403
|
{reloadAction.enabled
|
|
1398
|
-
|
|
1399
|
-
|
|
1404
|
+
? reloadAction.hint
|
|
1405
|
+
: reloadAction.disabledReason}
|
|
1400
1406
|
</react_native_1.Text>
|
|
1401
1407
|
</react_native_1.View>))}
|
|
1408
|
+
</> : null}
|
|
1402
1409
|
</>
|
|
1403
1410
|
</react_native_1.View>
|
|
1404
1411
|
|
|
@@ -1443,7 +1450,7 @@ const FeedbackModal = () => {
|
|
|
1443
1450
|
</react_native_1.Pressable>
|
|
1444
1451
|
</react_native_1.ScrollView>
|
|
1445
1452
|
</react_native_1.View>
|
|
1446
|
-
</react_native_1.
|
|
1453
|
+
</react_native_1.KeyboardAvoidingView>
|
|
1447
1454
|
</react_native_1.Modal>)}
|
|
1448
1455
|
{runnerAuthModal ? (<RunnerAuthNativeModal runner={runnerAuthModal} onClose={() => {
|
|
1449
1456
|
setRunnerAuthModal(null);
|
package/dist/LoginScreen.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export interface YaverLoginScreenProps {
|
|
|
4
4
|
onLoggedIn: (token: string) => void;
|
|
5
5
|
/** Optional cancel button shown in header. */
|
|
6
6
|
onCancel?: () => void;
|
|
7
|
+
/** Intent shown under the Yaver mark. Defaults to ordinary feedback. */
|
|
8
|
+
subtitle?: string;
|
|
7
9
|
}
|
|
8
10
|
/**
|
|
9
11
|
* Full-screen in-SDK login. Mirrors the Yaver mobile app login UX: native
|
package/dist/LoginScreen.js
CHANGED
|
@@ -91,7 +91,7 @@ const iconStyles = react_native_1.StyleSheet.create({
|
|
|
91
91
|
* Apple Sign-In on iOS, in-app browser OAuth for Google/GitHub/GitLab/
|
|
92
92
|
* Microsoft (no codes, no leaving the app), and inline email/password.
|
|
93
93
|
*/
|
|
94
|
-
const YaverLoginScreen = ({ onLoggedIn, onCancel, }) => {
|
|
94
|
+
const YaverLoginScreen = ({ onLoggedIn, onCancel, subtitle = 'Sign in to send feedback', }) => {
|
|
95
95
|
const [busyProvider, setBusyProvider] = (0, react_1.useState)(null);
|
|
96
96
|
const [showEmailForm, setShowEmailForm] = (0, react_1.useState)(false);
|
|
97
97
|
const [isSignUp, setIsSignUp] = (0, react_1.useState)(false);
|
|
@@ -186,7 +186,7 @@ const YaverLoginScreen = ({ onLoggedIn, onCancel, }) => {
|
|
|
186
186
|
|
|
187
187
|
<react_native_1.View style={styles.header}>
|
|
188
188
|
<react_native_1.Text style={styles.logo}>Yaver</react_native_1.Text>
|
|
189
|
-
<react_native_1.Text style={styles.subtitle}>
|
|
189
|
+
<react_native_1.Text style={styles.subtitle}>{subtitle}</react_native_1.Text>
|
|
190
190
|
</react_native_1.View>
|
|
191
191
|
|
|
192
192
|
<react_native_1.View style={styles.buttons}>
|
|
@@ -4,8 +4,10 @@ export interface YaverMachinePickerProps {
|
|
|
4
4
|
token: string;
|
|
5
5
|
/** Currently-selected deviceId (from config / cache) — highlighted. */
|
|
6
6
|
currentDeviceId?: string;
|
|
7
|
-
onPick: (device: RemoteDevice) => void
|
|
7
|
+
onPick: (device: RemoteDevice) => void | Promise<void>;
|
|
8
8
|
onCancel?: () => void;
|
|
9
|
+
/** Optional flow-specific title, e.g. "Choose a machine for SFMG". */
|
|
10
|
+
title?: string;
|
|
9
11
|
}
|
|
10
12
|
/**
|
|
11
13
|
* List of remote dev machines owned by the signed-in user.
|
|
@@ -45,14 +45,18 @@ const PairDeviceModal_1 = require("./PairDeviceModal");
|
|
|
45
45
|
* SDK then uses that device's deviceId for agent discovery (LAN probe +
|
|
46
46
|
* relay fallback through Convex).
|
|
47
47
|
*/
|
|
48
|
-
const YaverMachinePickerScreen = ({ token, currentDeviceId, onPick, onCancel, }) => {
|
|
48
|
+
const YaverMachinePickerScreen = ({ token, currentDeviceId, onPick, onCancel, title = 'Choose a machine', }) => {
|
|
49
49
|
const [loading, setLoading] = (0, react_1.useState)(true);
|
|
50
50
|
const [refreshing, setRefreshing] = (0, react_1.useState)(false);
|
|
51
51
|
const [error, setError] = (0, react_1.useState)(null);
|
|
52
52
|
const [list, setList] = (0, react_1.useState)({ owned: [] });
|
|
53
53
|
const [pairingDevice, setPairingDevice] = (0, react_1.useState)(null);
|
|
54
54
|
const [reachability, setReachability] = (0, react_1.useState)({});
|
|
55
|
+
const [selectingDeviceId, setSelectingDeviceId] = (0, react_1.useState)(null);
|
|
56
|
+
const mountedRef = (0, react_1.useRef)(true);
|
|
57
|
+
const loadGenerationRef = (0, react_1.useRef)(0);
|
|
55
58
|
const load = (0, react_1.useCallback)(async (silent = false) => {
|
|
59
|
+
const generation = ++loadGenerationRef.current;
|
|
56
60
|
if (!silent)
|
|
57
61
|
setLoading(true);
|
|
58
62
|
setError(null);
|
|
@@ -60,22 +64,17 @@ const YaverMachinePickerScreen = ({ token, currentDeviceId, onPick, onCancel, })
|
|
|
60
64
|
const result = await (0, auth_1.listReachableDevices)(token);
|
|
61
65
|
setList(result);
|
|
62
66
|
setReachability({});
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return next;
|
|
77
|
-
});
|
|
78
|
-
})();
|
|
67
|
+
// Convex already gives us a fresh, heartbeat-gated online answer. Render
|
|
68
|
+
// that immediately. Only probe cloud-offline rows to detect a phone-LAN
|
|
69
|
+
// route that came back before the next heartbeat, and publish each result
|
|
70
|
+
// as it arrives instead of holding the whole list behind the slowest box.
|
|
71
|
+
for (const device of result.owned.filter((candidate) => !candidate.isOnline)) {
|
|
72
|
+
void (0, auth_1.probeDeviceReachability)(device).then((probe) => {
|
|
73
|
+
if (!mountedRef.current || loadGenerationRef.current !== generation)
|
|
74
|
+
return;
|
|
75
|
+
setReachability((prev) => ({ ...prev, [device.deviceId]: probe }));
|
|
76
|
+
}).catch(() => { });
|
|
77
|
+
}
|
|
79
78
|
if (result.owned.length === 0) {
|
|
80
79
|
setError('No machines found yet. Run `yaver auth` + `yaver serve` on your machine.');
|
|
81
80
|
}
|
|
@@ -89,7 +88,9 @@ const YaverMachinePickerScreen = ({ token, currentDeviceId, onPick, onCancel, })
|
|
|
89
88
|
}
|
|
90
89
|
}, [token]);
|
|
91
90
|
(0, react_1.useEffect)(() => {
|
|
91
|
+
mountedRef.current = true;
|
|
92
92
|
void load();
|
|
93
|
+
return () => { mountedRef.current = false; };
|
|
93
94
|
}, [load]);
|
|
94
95
|
const handlePick = async (device) => {
|
|
95
96
|
// Needs-auth device — show the in-SDK pair modal instead of
|
|
@@ -101,7 +102,9 @@ const YaverMachinePickerScreen = ({ token, currentDeviceId, onPick, onCancel, })
|
|
|
101
102
|
setPairingDevice(device);
|
|
102
103
|
return;
|
|
103
104
|
}
|
|
104
|
-
const direct =
|
|
105
|
+
const direct = device.isOnline
|
|
106
|
+
? { reachable: true }
|
|
107
|
+
: await (0, auth_1.probeDeviceReachability)(device);
|
|
105
108
|
// Do not hard-block selection just because the LAN /health probe
|
|
106
109
|
// failed. The standalone SDK can still reach a healthy machine via
|
|
107
110
|
// the normal selected-device discovery path (including relay), and
|
|
@@ -114,11 +117,19 @@ const YaverMachinePickerScreen = ({ token, currentDeviceId, onPick, onCancel, })
|
|
|
114
117
|
setReachability((prev) => ({ ...prev, [device.deviceId]: direct }));
|
|
115
118
|
return;
|
|
116
119
|
}
|
|
117
|
-
|
|
118
|
-
|
|
120
|
+
setSelectingDeviceId(device.deviceId);
|
|
121
|
+
try {
|
|
122
|
+
await (0, auth_1.saveSelectedDeviceId)(device.deviceId);
|
|
123
|
+
await onPick(device);
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
if (mountedRef.current)
|
|
127
|
+
setSelectingDeviceId(null);
|
|
128
|
+
}
|
|
119
129
|
};
|
|
120
130
|
const renderDevice = (device) => {
|
|
121
131
|
const selected = device.deviceId === currentDeviceId;
|
|
132
|
+
const selecting = device.deviceId === selectingDeviceId;
|
|
122
133
|
const probe = reachability[device.deviceId];
|
|
123
134
|
// Trust Convex's `isOnline` — the backend already gates it on a
|
|
124
135
|
// fresh 90 s heartbeat (see backend/convex/devices.ts
|
|
@@ -137,14 +148,17 @@ const YaverMachinePickerScreen = ({ token, currentDeviceId, onPick, onCancel, })
|
|
|
137
148
|
: effectivelyReachable
|
|
138
149
|
? '#22c55e'
|
|
139
150
|
: device.isOnline
|
|
140
|
-
? '#
|
|
151
|
+
? '#22c55e'
|
|
141
152
|
: explicitlyOffline || !device.isOnline
|
|
142
153
|
? '#ef4444'
|
|
143
154
|
: '#22c55e';
|
|
144
155
|
// Derive a single short status phrase the user can act on.
|
|
145
156
|
let statusLine = device.platform;
|
|
146
|
-
if (
|
|
147
|
-
statusLine = '
|
|
157
|
+
if (selecting) {
|
|
158
|
+
statusLine = 'Connecting…';
|
|
159
|
+
}
|
|
160
|
+
else if (probe === undefined && device.isOnline) {
|
|
161
|
+
statusLine = device.platform || 'Online';
|
|
148
162
|
}
|
|
149
163
|
else if (!device.isOnline && effectivelyReachable) {
|
|
150
164
|
statusLine = 'Reachable now — waiting for cloud status to refresh';
|
|
@@ -166,20 +180,21 @@ const YaverMachinePickerScreen = ({ token, currentDeviceId, onPick, onCancel, })
|
|
|
166
180
|
// Happy-path subtitle.
|
|
167
181
|
statusLine = device.platform;
|
|
168
182
|
}
|
|
169
|
-
return (<react_native_1.TouchableOpacity key={device.deviceId} style={[styles.deviceRow, selected && styles.deviceSelected]} onPress={() => handlePick(device)}>
|
|
183
|
+
return (<react_native_1.TouchableOpacity key={device.deviceId} style={[styles.deviceRow, selected && styles.deviceSelected]} onPress={() => handlePick(device)} disabled={selectingDeviceId !== null}>
|
|
170
184
|
<react_native_1.View style={[styles.health, { backgroundColor: healthColor }]}/>
|
|
171
185
|
<react_native_1.View style={{ flex: 1 }}>
|
|
172
186
|
<react_native_1.Text style={styles.deviceName}>{device.name || device.deviceId}</react_native_1.Text>
|
|
173
187
|
<react_native_1.Text style={styles.deviceMeta}>{statusLine}</react_native_1.Text>
|
|
174
188
|
</react_native_1.View>
|
|
175
|
-
{
|
|
189
|
+
{selecting ? <react_native_1.ActivityIndicator color="#a5b4fc" size="small"/> : null}
|
|
190
|
+
{selected && !selecting && <react_native_1.Text style={styles.selectedBadge}>selected</react_native_1.Text>}
|
|
176
191
|
</react_native_1.TouchableOpacity>);
|
|
177
192
|
};
|
|
178
193
|
return (<react_native_1.SafeAreaView style={styles.container}>
|
|
179
194
|
<react_native_1.View style={styles.header}>
|
|
180
|
-
<react_native_1.Text style={styles.title}>
|
|
195
|
+
<react_native_1.Text style={styles.title}>{title}</react_native_1.Text>
|
|
181
196
|
{onCancel && (<react_native_1.TouchableOpacity onPress={onCancel} style={styles.cancel}>
|
|
182
|
-
<react_native_1.Text style={styles.cancelText}>
|
|
197
|
+
<react_native_1.Text style={styles.cancelText}>Close</react_native_1.Text>
|
|
183
198
|
</react_native_1.TouchableOpacity>)}
|
|
184
199
|
</react_native_1.View>
|
|
185
200
|
|
|
@@ -189,7 +204,7 @@ const YaverMachinePickerScreen = ({ token, currentDeviceId, onPick, onCancel, })
|
|
|
189
204
|
}} tintColor="#6366f1"/>}>
|
|
190
205
|
{loading ? (<react_native_1.ActivityIndicator color="#6366f1" style={{ marginTop: 60 }}/>) : (<>
|
|
191
206
|
{list.owned.length > 0 && (<react_native_1.View style={styles.section}>
|
|
192
|
-
<react_native_1.Text style={styles.sectionTitle}>
|
|
207
|
+
<react_native_1.Text style={styles.sectionTitle}>Your machines</react_native_1.Text>
|
|
193
208
|
{list.owned.map(renderDevice)}
|
|
194
209
|
</react_native_1.View>)}
|
|
195
210
|
{error && <react_native_1.Text style={styles.error}>{error}</react_native_1.Text>}
|
|
@@ -58,7 +58,9 @@ describe('FeedbackModal authenticated chat contract', () => {
|
|
|
58
58
|
expect(setupSteps).not.toContain("key: 'model'");
|
|
59
59
|
expect(setupSteps).not.toContain("key: 'lane'");
|
|
60
60
|
expect(source).toContain("type DogfoodSetupStage = 'setup' | 'lane' | 'runtime'");
|
|
61
|
-
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`");
|
|
62
64
|
});
|
|
63
65
|
it('passes the selected native target and labels the live log source', () => {
|
|
64
66
|
expect(source).toContain("nativeTargetId: lanePlan.preferred === 'webrtc' ? dogfoodNativeTargetId : undefined");
|
|
@@ -67,9 +69,10 @@ describe('FeedbackModal authenticated chat contract', () => {
|
|
|
67
69
|
expect(source).toMatch(/<DogfoodLiveConsole[\s\S]*?sourceLabel=\{dogfoodSourceLabel\}/);
|
|
68
70
|
expect(source).toContain('Simulator, emulator, or device');
|
|
69
71
|
});
|
|
70
|
-
it('has one
|
|
71
|
-
expect(source).toContain(
|
|
72
|
-
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');
|
|
73
76
|
expect(source).not.toContain('keyboardInset');
|
|
74
77
|
expect(source).toContain('<Pressable style={styles.backdrop} onPress={handleClose}');
|
|
75
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/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/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,
|
|
@@ -499,9 +500,6 @@ export const FeedbackModal: React.FC = () => {
|
|
|
499
500
|
} else if (device.needsAuth) {
|
|
500
501
|
status = 'attention';
|
|
501
502
|
detail = 'Machine needs pairing again before feedback actions can run.';
|
|
502
|
-
} else if (device.runnerDown) {
|
|
503
|
-
status = 'attention';
|
|
504
|
-
detail = 'Machine is online but the coding agent is down.';
|
|
505
503
|
} else if (reachable === false) {
|
|
506
504
|
status = 'offline';
|
|
507
505
|
detail = 'Machine selected, but the agent is not responding.';
|
|
@@ -1062,6 +1060,7 @@ export const FeedbackModal: React.FC = () => {
|
|
|
1062
1060
|
const dogfoodModelReady = !selectedDogfoodRunner?.models?.length
|
|
1063
1061
|
|| !!preferredModel && selectedDogfoodRunner.models.some((model) => model.id === preferredModel);
|
|
1064
1062
|
const dogfoodFramework = dogfoodProject?.framework || YaverFeedback.getDogfoodOnboarding()?.framework || 'expo';
|
|
1063
|
+
const dogfoodOnboarding = YaverFeedback.getDogfoodOnboarding();
|
|
1065
1064
|
const dogfoodLaneChoices = dogfoodLaneOptions(dogfoodFramework, {
|
|
1066
1065
|
nativeRuntimeAvailable: dogfoodNativeAvailable,
|
|
1067
1066
|
browserRuntimeAvailable: dogfoodBrowserAvailable,
|
|
@@ -1176,7 +1175,10 @@ export const FeedbackModal: React.FC = () => {
|
|
|
1176
1175
|
transparent
|
|
1177
1176
|
onRequestClose={handleClose}
|
|
1178
1177
|
>
|
|
1179
|
-
<
|
|
1178
|
+
<KeyboardAvoidingView
|
|
1179
|
+
style={styles.overlay}
|
|
1180
|
+
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
1181
|
+
>
|
|
1180
1182
|
<Pressable style={styles.backdrop} onPress={handleClose} accessibilityLabel="Close feedback" />
|
|
1181
1183
|
<View
|
|
1182
1184
|
// Tablet: cap modal width and center as a card-style
|
|
@@ -1202,11 +1204,18 @@ export const FeedbackModal: React.FC = () => {
|
|
|
1202
1204
|
keyboardShouldPersistTaps="handled"
|
|
1203
1205
|
keyboardDismissMode={Platform.OS === 'ios' ? 'interactive' : 'on-drag'}
|
|
1204
1206
|
contentInsetAdjustmentBehavior="always"
|
|
1205
|
-
|
|
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}
|
|
1206
1211
|
>
|
|
1207
1212
|
<View style={styles.header}>
|
|
1208
1213
|
<Text style={styles.title}>
|
|
1209
|
-
{
|
|
1214
|
+
{dogfoodOnboarding
|
|
1215
|
+
? `Set up ${dogfoodOnboarding.projectName || dogfoodOnboarding.label || 'this app'} Dogfood`
|
|
1216
|
+
: dogfoodActive
|
|
1217
|
+
? `${YaverFeedback.getDogfoodStatus().label || 'App'} Developer Mode`
|
|
1218
|
+
: 'Send Feedback'}
|
|
1210
1219
|
</Text>
|
|
1211
1220
|
<Pressable
|
|
1212
1221
|
onPress={handleClose}
|
|
@@ -1219,6 +1228,7 @@ export const FeedbackModal: React.FC = () => {
|
|
|
1219
1228
|
</Pressable>
|
|
1220
1229
|
</View>
|
|
1221
1230
|
|
|
1231
|
+
{(!dogfoodOnboarding || dogfoodSetupStage === 'runtime') ? (
|
|
1222
1232
|
<View style={styles.tabs} accessibilityRole="tablist">
|
|
1223
1233
|
{(['chat', 'settings'] as const).map((tab) => (
|
|
1224
1234
|
<Pressable
|
|
@@ -1232,15 +1242,16 @@ export const FeedbackModal: React.FC = () => {
|
|
|
1232
1242
|
</Pressable>
|
|
1233
1243
|
))}
|
|
1234
1244
|
</View>
|
|
1245
|
+
) : null}
|
|
1235
1246
|
|
|
1236
1247
|
<View style={[styles.tabContent, activeTab !== 'settings' && styles.hidden]}>
|
|
1237
1248
|
<>
|
|
1238
|
-
{
|
|
1249
|
+
{dogfoodOnboarding ? (
|
|
1239
1250
|
<View style={styles.dogfoodWizard}>
|
|
1240
|
-
<Text style={styles.dogfoodWizardTitle}>
|
|
1251
|
+
<Text style={styles.dogfoodWizardTitle}>Connect the app to its checkout</Text>
|
|
1241
1252
|
<Text style={styles.dogfoodWizardHint}>
|
|
1242
1253
|
{dogfoodEnrollment?.status === 'active'
|
|
1243
|
-
? '
|
|
1254
|
+
? 'Choose one development machine, coding runner, and checkout. Then choose how to preview it.'
|
|
1244
1255
|
: `Signed in · installation ${dogfoodEnrollment?.status || 'checking'}`}
|
|
1245
1256
|
</Text>
|
|
1246
1257
|
{dogfoodEnrollment?.status === 'active' && dogfoodSetupStage === 'setup' ? (
|
|
@@ -1337,7 +1348,7 @@ export const FeedbackModal: React.FC = () => {
|
|
|
1337
1348
|
</View>
|
|
1338
1349
|
) : null}
|
|
1339
1350
|
<ActionRow
|
|
1340
|
-
label=
|
|
1351
|
+
label={dogfoodSetupReady ? 'Continue to runtime' : 'Complete the choices above'}
|
|
1341
1352
|
tint="#5645d8"
|
|
1342
1353
|
onPress={() => {
|
|
1343
1354
|
setDogfoodExpandedStep(null);
|
|
@@ -1441,6 +1452,7 @@ export const FeedbackModal: React.FC = () => {
|
|
|
1441
1452
|
)}
|
|
1442
1453
|
</View>
|
|
1443
1454
|
) : null}
|
|
1455
|
+
{!dogfoodOnboarding ? <>
|
|
1444
1456
|
<Pressable
|
|
1445
1457
|
onPress={() => {
|
|
1446
1458
|
if (!YaverFeedback.isAuthed()) {
|
|
@@ -1718,6 +1730,7 @@ export const FeedbackModal: React.FC = () => {
|
|
|
1718
1730
|
</Text>
|
|
1719
1731
|
</View>
|
|
1720
1732
|
))}
|
|
1733
|
+
</> : null}
|
|
1721
1734
|
</>
|
|
1722
1735
|
</View>
|
|
1723
1736
|
|
|
@@ -1805,7 +1818,7 @@ export const FeedbackModal: React.FC = () => {
|
|
|
1805
1818
|
</Pressable>
|
|
1806
1819
|
</ScrollView>
|
|
1807
1820
|
</View>
|
|
1808
|
-
</
|
|
1821
|
+
</KeyboardAvoidingView>
|
|
1809
1822
|
</Modal>
|
|
1810
1823
|
)}
|
|
1811
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}>
|
|
@@ -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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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 =
|
|
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
|
-
|
|
114
|
-
|
|
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
|
-
? '#
|
|
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 (
|
|
144
|
-
statusLine = '
|
|
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
|
-
{
|
|
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}>
|
|
192
|
+
<Text style={styles.title}>{title}</Text>
|
|
180
193
|
{onCancel && (
|
|
181
194
|
<TouchableOpacity onPress={onCancel} style={styles.cancel}>
|
|
182
|
-
<Text style={styles.cancelText}>
|
|
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}>
|
|
219
|
+
<Text style={styles.sectionTitle}>Your machines</Text>
|
|
207
220
|
{list.owned.map(renderDevice)}
|
|
208
221
|
</View>
|
|
209
222
|
)}
|
|
@@ -67,7 +67,9 @@ describe('FeedbackModal authenticated chat contract', () => {
|
|
|
67
67
|
expect(setupSteps).not.toContain("key: 'model'");
|
|
68
68
|
expect(setupSteps).not.toContain("key: 'lane'");
|
|
69
69
|
expect(source).toContain("type DogfoodSetupStage = 'setup' | 'lane' | 'runtime'");
|
|
70
|
-
expect(source).toContain(
|
|
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`");
|
|
71
73
|
});
|
|
72
74
|
|
|
73
75
|
it('passes the selected native target and labels the live log source', () => {
|
|
@@ -78,9 +80,10 @@ describe('FeedbackModal authenticated chat contract', () => {
|
|
|
78
80
|
expect(source).toContain('Simulator, emulator, or device');
|
|
79
81
|
});
|
|
80
82
|
|
|
81
|
-
it('has one
|
|
82
|
-
expect(source).toContain(
|
|
83
|
-
expect(source).
|
|
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');
|
|
84
87
|
expect(source).not.toContain('keyboardInset');
|
|
85
88
|
expect(source).toContain('<Pressable style={styles.backdrop} onPress={handleClose}');
|
|
86
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
|
+
});
|