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.
@@ -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
  };
@@ -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.View style={styles.overlay}>
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" automaticallyAdjustKeyboardInsets={react_native_1.Platform.OS === 'ios'}>
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
- {dogfoodActive ? `${YaverFeedback_1.YaverFeedback.getDogfoodStatus().label || 'App'} Developer Mode` : 'Send Feedback'}
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
- {YaverFeedback_1.YaverFeedback.getDogfoodOnboarding() ? (<react_native_1.View style={styles.dogfoodWizard}>
1085
- <react_native_1.Text style={styles.dogfoodWizardTitle}>Dogfood this app</react_native_1.Text>
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
- ? 'Signed in · this installation is approved'
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="Choose runtime" tint="#5645d8" onPress={() => {
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
- if (!YaverFeedback_1.YaverFeedback.isAuthed()) {
1205
- YaverFeedback_1.YaverFeedback.showLogin();
1206
- return;
1207
- }
1208
- YaverFeedback_1.YaverFeedback.showMachinePicker();
1209
- }} style={[
1210
- styles.machineCard,
1211
- machineCard.status === 'live' && styles.machineCardLive,
1212
- machineCard.status === 'attention' && styles.machineCardAttention,
1213
- machineCard.status === 'offline' && styles.machineCardOffline,
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
- styles.machineDot,
1219
- machineCard.status === 'live' && styles.machineDotLive,
1220
- machineCard.status === 'attention' && styles.machineDotAttention,
1221
- machineCard.status === 'offline' && styles.machineDotOffline,
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
- ? 'Refreshing runner status on the selected machine…'
1253
- : `${readyRunnerCount} ready · ${needsAuthRunnerCount} need sign-in · ${missingRunnerCount} missing`}
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
- styles.runnerRefreshBtn,
1258
- pressed && styles.buttonPressed,
1259
- ]} accessibilityRole="button" accessibilityLabel="Refresh coding-agent status">
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
- styles.runnerCard,
1275
- row.tone === 'ok' && styles.runnerCardOk,
1276
- row.tone === 'warning' && styles.runnerCardWarning,
1277
- row.tone === 'error' && styles.runnerCardError,
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
- styles.runnerCardStatus,
1284
- row.tone === 'ok' && styles.runnerCardStatusOk,
1285
- row.tone === 'warning' && styles.runnerCardStatusWarning,
1286
- row.tone === 'error' && styles.runnerCardStatusError,
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
- const nextModel = row.models?.find((model) => model.isDefault)?.id || row.models?.[0]?.id || '';
1294
- setPreferredRunnerState(row.id);
1295
- setPreferredModelState(nextModel);
1296
- void (0, preferences_1.setPreferredRunner)(row.id);
1297
- void (0, preferences_1.setPreferredModel)(nextModel || null);
1298
- }} style={({ pressed }) => [styles.runnerActionBtn, preferredRunner === row.id && styles.runnerActionBtnSelected, pressed && styles.buttonPressed]} accessibilityRole="button" accessibilityLabel={`Use ${row.name} for Vibing`}>
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
- setPreferredModelState(model.id);
1309
- void (0, preferences_1.setPreferredModel)(model.id);
1310
- }} style={[styles.modelChoice, preferredModel === model.id && styles.modelChoiceSelected]} accessibilityRole="button" accessibilityLabel={`Use ${model.name || model.id} model`}>
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
- void YaverFeedback_1.YaverFeedback.setQuickIconVisible(true);
1326
- setQuickIconHidden(false);
1327
- }} style={({ pressed }) => [
1328
- styles.quickIconToggle,
1329
- pressed && styles.buttonPressed,
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
- void YaverFeedback_1.YaverFeedback.signOut().then(() => {
1337
- handleClose();
1338
- YaverFeedback_1.YaverFeedback.showLogin();
1339
- });
1340
- }} style={({ pressed }) => [styles.yaverSignOutBtn, pressed && styles.buttonPressed]} accessibilityRole="button" accessibilityLabel="Sign out of Yaver">
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
- const selected = quickIconColorPreset === preset;
1352
- return (<react_native_1.Pressable key={preset} onPress={() => {
1353
- setQuickIconColorPreset(preset);
1354
- void YaverFeedback_1.YaverFeedback.setQuickIconColorPreset(preset);
1355
- }} style={[
1356
- styles.iconOption,
1357
- iconOptionWidthOverride ? { width: iconOptionWidthOverride } : null,
1358
- selected && styles.iconOptionSelected,
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
- styles.iconOptionCircle,
1362
- {
1363
- backgroundColor: colors.backgroundColor,
1364
- borderColor: colors.borderColor,
1365
- shadowColor: colors.shadowColor,
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
- styles.iconOptionLabel,
1370
- { color: colors.foregroundColor },
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
- Rendered from the shared decision seam, so a production
1383
- build (__DEV__ false) renders nothing here at all, and a
1384
- blocked action shows greyed WITH its reason underneath
1385
- rather than vanishing. */}
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
- ? `${reloadAction.label}…`
1389
- : reloadAction.label} tint={reloadAction.id === 'rebuild' ? '#0369a1' : '#9a5700'} onPress={() => {
1390
- void handleReloadAction(reloadAction);
1391
- }}
1392
- // Never `disabled` at the Pressable level for a blocked
1393
- // action: we WANT the tap so we can say why. Only a
1394
- // genuinely busy modal blocks the press.
1395
- disabled={busy && reloadingId !== reloadAction.id} busy={reloadingId === reloadAction.id}/>
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
- ? reloadAction.hint
1399
- : reloadAction.disabledReason}
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.View>
1453
+ </react_native_1.KeyboardAvoidingView>
1447
1454
  </react_native_1.Modal>)}
1448
1455
  {runnerAuthModal ? (<RunnerAuthNativeModal runner={runnerAuthModal} onClose={() => {
1449
1456
  setRunnerAuthModal(null);
@@ -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
@@ -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}>Sign in to send feedback</react_native_1.Text>
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
- void (async () => {
64
- const devices = result.owned;
65
- const settled = await Promise.allSettled(devices.map(async (device) => ({
66
- deviceId: device.deviceId,
67
- result: await (0, auth_1.probeDeviceReachability)(device),
68
- })));
69
- setReachability((prev) => {
70
- const next = { ...prev };
71
- for (const entry of settled) {
72
- if (entry.status === 'fulfilled') {
73
- next[entry.value.deviceId] = entry.value.result;
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 = await (0, auth_1.probeDeviceReachability)(device);
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
- await (0, auth_1.saveSelectedDeviceId)(device.deviceId);
118
- onPick(device);
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
- ? '#f59e0b'
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 (probe === undefined) {
147
- statusLine = 'Checking connection…';
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
- {selected && <react_native_1.Text style={styles.selectedBadge}>seçili</react_native_1.Text>}
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}>Makine Seç</react_native_1.Text>
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}>Kapat</react_native_1.Text>
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}>Kendi makinelerim</react_native_1.Text>
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('label="Choose runtime"');
61
+ expect(source).toContain("label={dogfoodSetupReady ? 'Continue to runtime' : 'Complete the choices above'}");
62
+ expect(source).toContain('{!dogfoodOnboarding ? <>');
63
+ expect(source).toContain("? `Set up ${dogfoodOnboarding.projectName || dogfoodOnboarding.label || 'this app'} Dogfood`");
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 iOS keyboard inset owner and no gesture-stealing sheet Pressable', () => {
71
- expect(source).toContain('automaticallyAdjustKeyboardInsets={Platform.OS === \'ios\'}');
72
- expect(source).not.toContain('<KeyboardAvoidingView');
72
+ it('has one keyboard inset owner and no gesture-stealing sheet Pressable', () => {
73
+ expect(source).toContain("behavior={Platform.OS === 'ios' ? 'padding' : 'height'}");
74
+ expect(source).toContain('automaticallyAdjustKeyboardInsets={false}');
75
+ expect(source).toContain('<KeyboardAvoidingView');
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,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.11",
3
+ "version": "0.9.12",
4
4
  "description": "Visual feedback SDK for Yaver — bug reports, screen recording, voice vibe coding, and local-first developer workflows",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -11,6 +11,8 @@ export const AuthOverlay: React.FC = () => {
11
11
  const [pickerVisible, setPickerVisible] = useState(false);
12
12
  const [token, setToken] = useState<string | null>(null);
13
13
  const activeOverlayRef = useRef<'none' | 'login' | 'picker'>('none');
14
+ const onboarding = YaverFeedback.getDogfoodOnboarding();
15
+ const dogfoodLabel = onboarding?.projectName || onboarding?.label || 'this app';
14
16
 
15
17
  const openLogin = useCallback(() => {
16
18
  activeOverlayRef.current = 'login';
@@ -94,13 +96,18 @@ export const AuthOverlay: React.FC = () => {
94
96
  return (
95
97
  <>
96
98
  <Modal visible={loginVisible} animationType="slide" presentationStyle="fullScreen" onRequestClose={closeAll}>
97
- <YaverLoginScreen onLoggedIn={handleLoggedIn} onCancel={closeAll} />
99
+ <YaverLoginScreen
100
+ onLoggedIn={handleLoggedIn}
101
+ onCancel={closeAll}
102
+ subtitle={onboarding ? `Sign in to set up ${dogfoodLabel} Dogfood` : undefined}
103
+ />
98
104
  </Modal>
99
105
  <Modal visible={pickerVisible && !!token} animationType="slide" presentationStyle="fullScreen" onRequestClose={closeAll}>
100
106
  {token && (
101
107
  <YaverMachinePickerScreen
102
108
  token={token}
103
109
  currentDeviceId={YaverFeedback.getConfig()?.preferredDeviceId}
110
+ title={onboarding ? `Choose a machine for ${dogfoodLabel}` : undefined}
104
111
  onPick={handleDevicePicked}
105
112
  onCancel={closeAll}
106
113
  />
@@ -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
- <View style={styles.overlay}>
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
- automaticallyAdjustKeyboardInsets={Platform.OS === 'ios'}
1207
+ // KeyboardAvoidingView is the single inset owner. Letting the
1208
+ // ScrollView add a second UIKit inset lifts the composer twice
1209
+ // on short phones and still leaves its action row clipped.
1210
+ automaticallyAdjustKeyboardInsets={false}
1206
1211
  >
1207
1212
  <View style={styles.header}>
1208
1213
  <Text style={styles.title}>
1209
- {dogfoodActive ? `${YaverFeedback.getDogfoodStatus().label || 'App'} Developer Mode` : 'Send Feedback'}
1214
+ {dogfoodOnboarding
1215
+ ? `Set up ${dogfoodOnboarding.projectName || dogfoodOnboarding.label || 'this app'} Dogfood`
1216
+ : dogfoodActive
1217
+ ? `${YaverFeedback.getDogfoodStatus().label || 'App'} Developer Mode`
1218
+ : 'Send Feedback'}
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
- {YaverFeedback.getDogfoodOnboarding() ? (
1249
+ {dogfoodOnboarding ? (
1239
1250
  <View style={styles.dogfoodWizard}>
1240
- <Text style={styles.dogfoodWizardTitle}>Dogfood this app</Text>
1251
+ <Text style={styles.dogfoodWizardTitle}>Connect the app to its checkout</Text>
1241
1252
  <Text style={styles.dogfoodWizardHint}>
1242
1253
  {dogfoodEnrollment?.status === 'active'
1243
- ? 'Signed in · this installation is approved'
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="Choose runtime"
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
- </View>
1821
+ </KeyboardAvoidingView>
1809
1822
  </Modal>
1810
1823
  )}
1811
1824
  {runnerAuthModal ? (
@@ -116,6 +116,8 @@ export interface YaverLoginScreenProps {
116
116
  onLoggedIn: (token: string) => void;
117
117
  /** Optional cancel button shown in header. */
118
118
  onCancel?: () => void;
119
+ /** Intent shown under the Yaver mark. Defaults to ordinary feedback. */
120
+ subtitle?: string;
119
121
  }
120
122
 
121
123
  /**
@@ -126,6 +128,7 @@ export interface YaverLoginScreenProps {
126
128
  export const YaverLoginScreen: React.FC<YaverLoginScreenProps> = ({
127
129
  onLoggedIn,
128
130
  onCancel,
131
+ subtitle = 'Sign in to send feedback',
129
132
  }) => {
130
133
  const [busyProvider, setBusyProvider] = useState<OAuthProvider | 'apple' | null>(null);
131
134
  const [showEmailForm, setShowEmailForm] = useState(false);
@@ -242,7 +245,7 @@ export const YaverLoginScreen: React.FC<YaverLoginScreenProps> = ({
242
245
 
243
246
  <View style={styles.header}>
244
247
  <Text style={styles.logo}>Yaver</Text>
245
- <Text style={styles.subtitle}>Sign in to send feedback</Text>
248
+ <Text style={styles.subtitle}>{subtitle}</Text>
246
249
  </View>
247
250
 
248
251
  <View style={styles.buttons}>
@@ -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
  )}
@@ -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('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`");
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 iOS keyboard inset owner and no gesture-stealing sheet Pressable', () => {
82
- expect(source).toContain('automaticallyAdjustKeyboardInsets={Platform.OS === \'ios\'}');
83
- 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');
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
+ });