yaver-feedback-react-native 0.8.3 → 0.8.6
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 +45 -18
- package/dist/FeedbackModal.js +307 -2
- package/dist/LoginScreen.js +19 -5
- package/dist/MachinePickerScreen.js +44 -6
- package/dist/P2PClient.d.ts +23 -2
- package/dist/P2PClient.js +53 -4
- package/dist/QuickActionIcon.js +27 -2
- package/dist/YaverFeedback.d.ts +13 -0
- package/dist/YaverFeedback.js +78 -34
- package/dist/__tests__/AuthDevices.test.d.ts +1 -0
- package/dist/__tests__/AuthDevices.test.js +82 -0
- package/dist/auth.d.ts +6 -0
- package/dist/auth.js +41 -0
- package/dist/types.d.ts +21 -0
- package/package.json +2 -2
- package/src/AuthOverlay.tsx +48 -19
- package/src/FeedbackModal.tsx +349 -1
- package/src/LoginScreen.tsx +19 -5
- package/src/MachinePickerScreen.tsx +45 -6
- package/src/P2PClient.ts +61 -5
- package/src/QuickActionIcon.tsx +37 -2
- package/src/YaverFeedback.ts +83 -34
- package/src/__tests__/AuthDevices.test.ts +93 -0
- package/src/auth.ts +46 -0
- package/src/types.ts +22 -0
package/src/FeedbackModal.tsx
CHANGED
|
@@ -72,6 +72,7 @@ export const FeedbackModal: React.FC = () => {
|
|
|
72
72
|
// is our guaranteed UI for bringing the icon back — we surface a
|
|
73
73
|
// small "Show quick icon" row when this is true.
|
|
74
74
|
const [quickIconHidden, setQuickIconHidden] = useState(false);
|
|
75
|
+
const [runnerAuthModal, setRunnerAuthModal] = useState<string | null>(null);
|
|
75
76
|
// Vibing-input mode: same expand-on-tap pattern as email login.
|
|
76
77
|
// Tap "Vibing" once → the button reveals an input + Send; that lets
|
|
77
78
|
// the user say WHAT they want to vibe on instead of firing a canned
|
|
@@ -82,6 +83,7 @@ export const FeedbackModal: React.FC = () => {
|
|
|
82
83
|
const [lastVibeTaskId, setLastVibeTaskId] = useState<string | null>(null);
|
|
83
84
|
const [quickIconColorPreset, setQuickIconColorPreset] =
|
|
84
85
|
useState<QuickIconColorPreset | null>(null);
|
|
86
|
+
const [keyboardInset, setKeyboardInset] = useState(0);
|
|
85
87
|
const [machineCard, setMachineCard] = useState<MachineCardState>({
|
|
86
88
|
device: null,
|
|
87
89
|
reachable: null,
|
|
@@ -256,6 +258,26 @@ export const FeedbackModal: React.FC = () => {
|
|
|
256
258
|
return () => clearInterval(interval);
|
|
257
259
|
}, [loadSelectedMachine, visible]);
|
|
258
260
|
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
if (!visible) {
|
|
263
|
+
setKeyboardInset(0);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
|
|
268
|
+
const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide';
|
|
269
|
+
const showSub = Keyboard.addListener(showEvent, (event) => {
|
|
270
|
+
setKeyboardInset(event.endCoordinates?.height ?? 0);
|
|
271
|
+
});
|
|
272
|
+
const hideSub = Keyboard.addListener(hideEvent, () => {
|
|
273
|
+
setKeyboardInset(0);
|
|
274
|
+
});
|
|
275
|
+
return () => {
|
|
276
|
+
showSub.remove();
|
|
277
|
+
hideSub.remove();
|
|
278
|
+
};
|
|
279
|
+
}, [visible]);
|
|
280
|
+
|
|
259
281
|
const closeSoon = useCallback((delayMs = 1200) => {
|
|
260
282
|
setTimeout(() => {
|
|
261
283
|
if (mountedRef.current) setVisible(false);
|
|
@@ -569,6 +591,7 @@ export const FeedbackModal: React.FC = () => {
|
|
|
569
591
|
<Pressable style={styles.overlay} onPress={handleClose}>
|
|
570
592
|
<KeyboardAvoidingView
|
|
571
593
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
594
|
+
keyboardVerticalOffset={Platform.OS === 'ios' ? 12 : 0}
|
|
572
595
|
style={styles.kbAvoider}
|
|
573
596
|
pointerEvents="box-none"
|
|
574
597
|
>
|
|
@@ -581,8 +604,14 @@ export const FeedbackModal: React.FC = () => {
|
|
|
581
604
|
>
|
|
582
605
|
<ScrollView
|
|
583
606
|
style={styles.scroll}
|
|
584
|
-
contentContainerStyle={
|
|
607
|
+
contentContainerStyle={[
|
|
608
|
+
styles.scrollContent,
|
|
609
|
+
showVibeInput && keyboardInset > 0
|
|
610
|
+
? { paddingBottom: 8 + keyboardInset }
|
|
611
|
+
: null,
|
|
612
|
+
]}
|
|
585
613
|
keyboardShouldPersistTaps="handled"
|
|
614
|
+
keyboardDismissMode={Platform.OS === 'ios' ? 'interactive' : 'on-drag'}
|
|
586
615
|
>
|
|
587
616
|
<View style={styles.header}>
|
|
588
617
|
<Text style={styles.title}>Send Feedback</Text>
|
|
@@ -781,6 +810,41 @@ export const FeedbackModal: React.FC = () => {
|
|
|
781
810
|
busy={action === 'capturing'}
|
|
782
811
|
/>
|
|
783
812
|
|
|
813
|
+
{/* Remote sign-in buttons — trigger codex/claude device-auth
|
|
814
|
+
on the selected agent without leaving the app. Opens a
|
|
815
|
+
small native modal showing the verification URL + 8-char
|
|
816
|
+
code the user enters in any browser. No API keys. */}
|
|
817
|
+
<View style={runnerAuthRowStyles.container}>
|
|
818
|
+
<Pressable
|
|
819
|
+
onPress={() => setRunnerAuthModal('codex')}
|
|
820
|
+
disabled={busy}
|
|
821
|
+
style={({ pressed }) => [
|
|
822
|
+
runnerAuthRowStyles.button,
|
|
823
|
+
pressed && runnerAuthRowStyles.buttonPressed,
|
|
824
|
+
busy && runnerAuthRowStyles.buttonDisabled,
|
|
825
|
+
]}
|
|
826
|
+
accessibilityRole="button"
|
|
827
|
+
accessibilityLabel="Remote sign-in Codex"
|
|
828
|
+
>
|
|
829
|
+
<Text style={runnerAuthRowStyles.buttonLabel}>Remote sign-in</Text>
|
|
830
|
+
<Text style={runnerAuthRowStyles.buttonName}>Codex</Text>
|
|
831
|
+
</Pressable>
|
|
832
|
+
<Pressable
|
|
833
|
+
onPress={() => setRunnerAuthModal('claude')}
|
|
834
|
+
disabled={busy}
|
|
835
|
+
style={({ pressed }) => [
|
|
836
|
+
runnerAuthRowStyles.button,
|
|
837
|
+
pressed && runnerAuthRowStyles.buttonPressed,
|
|
838
|
+
busy && runnerAuthRowStyles.buttonDisabled,
|
|
839
|
+
]}
|
|
840
|
+
accessibilityRole="button"
|
|
841
|
+
accessibilityLabel="Remote sign-in Claude"
|
|
842
|
+
>
|
|
843
|
+
<Text style={runnerAuthRowStyles.buttonLabel}>Remote sign-in</Text>
|
|
844
|
+
<Text style={runnerAuthRowStyles.buttonName}>Claude</Text>
|
|
845
|
+
</Pressable>
|
|
846
|
+
</View>
|
|
847
|
+
|
|
784
848
|
{progress !== null && (
|
|
785
849
|
<View style={styles.progressTrack}>
|
|
786
850
|
<View
|
|
@@ -811,6 +875,12 @@ export const FeedbackModal: React.FC = () => {
|
|
|
811
875
|
</Pressable>
|
|
812
876
|
</Modal>
|
|
813
877
|
)}
|
|
878
|
+
{runnerAuthModal ? (
|
|
879
|
+
<RunnerAuthNativeModal
|
|
880
|
+
runner={runnerAuthModal}
|
|
881
|
+
onClose={() => setRunnerAuthModal(null)}
|
|
882
|
+
/>
|
|
883
|
+
) : null}
|
|
814
884
|
</>
|
|
815
885
|
);
|
|
816
886
|
};
|
|
@@ -1167,3 +1237,281 @@ const styles = StyleSheet.create({
|
|
|
1167
1237
|
opacity: 0.7,
|
|
1168
1238
|
},
|
|
1169
1239
|
});
|
|
1240
|
+
|
|
1241
|
+
/**
|
|
1242
|
+
* Minimal native modal for the codex/claude remote sign-in flow. Opens
|
|
1243
|
+
* the device-auth session on the connected agent, surfaces the
|
|
1244
|
+
* verification URL + one-time code, polls every 1.5 s, and turns green
|
|
1245
|
+
* the moment the CLI writes its auth.json. No API keys, no SSH.
|
|
1246
|
+
*/
|
|
1247
|
+
const RunnerAuthNativeModal: React.FC<{
|
|
1248
|
+
runner: string;
|
|
1249
|
+
onClose: () => void;
|
|
1250
|
+
}> = ({ runner, onClose }) => {
|
|
1251
|
+
const [session, setSession] = useState<import('./types').RunnerBrowserAuthSession | null>(null);
|
|
1252
|
+
const [startError, setStartError] = useState<string | null>(null);
|
|
1253
|
+
const [copied, setCopied] = useState(false);
|
|
1254
|
+
const startedRef = useRef(false);
|
|
1255
|
+
|
|
1256
|
+
useEffect(() => {
|
|
1257
|
+
if (startedRef.current) return;
|
|
1258
|
+
startedRef.current = true;
|
|
1259
|
+
(async () => {
|
|
1260
|
+
try {
|
|
1261
|
+
const s = await YaverFeedback.startRunnerBrowserAuth(runner);
|
|
1262
|
+
setSession(s);
|
|
1263
|
+
} catch (err) {
|
|
1264
|
+
setStartError(err instanceof Error ? err.message : String(err));
|
|
1265
|
+
}
|
|
1266
|
+
})();
|
|
1267
|
+
}, [runner]);
|
|
1268
|
+
|
|
1269
|
+
useEffect(() => {
|
|
1270
|
+
if (!session) return;
|
|
1271
|
+
if (['completed', 'failed', 'cancelled'].includes(session.status)) return;
|
|
1272
|
+
const iv = setInterval(async () => {
|
|
1273
|
+
try {
|
|
1274
|
+
const s = await YaverFeedback.getRunnerBrowserAuthStatus(session.id);
|
|
1275
|
+
setSession(s);
|
|
1276
|
+
} catch {
|
|
1277
|
+
// keep polling
|
|
1278
|
+
}
|
|
1279
|
+
}, 1500);
|
|
1280
|
+
return () => clearInterval(iv);
|
|
1281
|
+
}, [session?.id, session?.status]);
|
|
1282
|
+
|
|
1283
|
+
const terminal = session && ['completed', 'failed', 'cancelled'].includes(session.status);
|
|
1284
|
+
const runnerLabel = runner === 'codex' ? 'OpenAI Codex' : runner === 'claude' ? 'Claude Code' : runner;
|
|
1285
|
+
|
|
1286
|
+
const handleClose = () => {
|
|
1287
|
+
if (session && !terminal) {
|
|
1288
|
+
YaverFeedback.cancelRunnerBrowserAuth(session.id).catch(() => {});
|
|
1289
|
+
}
|
|
1290
|
+
onClose();
|
|
1291
|
+
};
|
|
1292
|
+
|
|
1293
|
+
const copyCode = () => {
|
|
1294
|
+
if (!session?.code) return;
|
|
1295
|
+
try {
|
|
1296
|
+
// Avoid a hard Clipboard dep — host app can polyfill.
|
|
1297
|
+
const Clipboard = require('react-native').Clipboard;
|
|
1298
|
+
if (Clipboard?.setString) {
|
|
1299
|
+
Clipboard.setString(session.code);
|
|
1300
|
+
setCopied(true);
|
|
1301
|
+
setTimeout(() => setCopied(false), 1500);
|
|
1302
|
+
}
|
|
1303
|
+
} catch {
|
|
1304
|
+
// best-effort — code is visible on screen regardless
|
|
1305
|
+
}
|
|
1306
|
+
};
|
|
1307
|
+
|
|
1308
|
+
const openUrl = () => {
|
|
1309
|
+
if (!session?.openUrl) return;
|
|
1310
|
+
try {
|
|
1311
|
+
const { Linking } = require('react-native');
|
|
1312
|
+
Linking.openURL(session.openUrl).catch(() => {});
|
|
1313
|
+
} catch {
|
|
1314
|
+
/* ignore */
|
|
1315
|
+
}
|
|
1316
|
+
};
|
|
1317
|
+
|
|
1318
|
+
return (
|
|
1319
|
+
<Modal visible={true} transparent animationType="fade" onRequestClose={handleClose}>
|
|
1320
|
+
<View style={runnerAuthModalStyles.overlay}>
|
|
1321
|
+
<View style={runnerAuthModalStyles.card}>
|
|
1322
|
+
<View style={runnerAuthModalStyles.header}>
|
|
1323
|
+
<View style={{ flex: 1 }}>
|
|
1324
|
+
<Text style={runnerAuthModalStyles.title}>Sign in to {runnerLabel}</Text>
|
|
1325
|
+
<Text style={runnerAuthModalStyles.subtitle}>
|
|
1326
|
+
Opens a one-time URL + code. Enter it in any browser.
|
|
1327
|
+
</Text>
|
|
1328
|
+
</View>
|
|
1329
|
+
<Pressable onPress={handleClose} hitSlop={10}>
|
|
1330
|
+
<Text style={runnerAuthModalStyles.close}>×</Text>
|
|
1331
|
+
</Pressable>
|
|
1332
|
+
</View>
|
|
1333
|
+
|
|
1334
|
+
{startError ? (
|
|
1335
|
+
<View style={runnerAuthModalStyles.errorBox}>
|
|
1336
|
+
<Text style={runnerAuthModalStyles.errorTitle}>Couldn't start</Text>
|
|
1337
|
+
<Text style={runnerAuthModalStyles.errorBody}>{startError}</Text>
|
|
1338
|
+
</View>
|
|
1339
|
+
) : !session ? (
|
|
1340
|
+
<Text style={runnerAuthModalStyles.dim}>
|
|
1341
|
+
Starting the sign-in flow on the remote machine…
|
|
1342
|
+
</Text>
|
|
1343
|
+
) : session.status === 'completed' ? (
|
|
1344
|
+
<View style={runnerAuthModalStyles.successBox}>
|
|
1345
|
+
<Text style={runnerAuthModalStyles.successTitle}>✓ Signed in</Text>
|
|
1346
|
+
<Text style={runnerAuthModalStyles.successBody}>
|
|
1347
|
+
{session.detail || 'Auth stored on the remote machine.'}
|
|
1348
|
+
</Text>
|
|
1349
|
+
</View>
|
|
1350
|
+
) : session.status === 'failed' || session.status === 'cancelled' ? (
|
|
1351
|
+
<View style={runnerAuthModalStyles.errorBox}>
|
|
1352
|
+
<Text style={runnerAuthModalStyles.errorTitle}>
|
|
1353
|
+
{session.status === 'cancelled' ? 'Cancelled' : 'Failed'}
|
|
1354
|
+
</Text>
|
|
1355
|
+
<Text style={runnerAuthModalStyles.errorBody}>
|
|
1356
|
+
{session.error || session.detail || 'The CLI exited before sign-in completed.'}
|
|
1357
|
+
</Text>
|
|
1358
|
+
</View>
|
|
1359
|
+
) : (
|
|
1360
|
+
<View>
|
|
1361
|
+
{session.openUrl ? (
|
|
1362
|
+
<Pressable onPress={openUrl} style={runnerAuthModalStyles.urlBox}>
|
|
1363
|
+
<Text style={runnerAuthModalStyles.urlText} numberOfLines={2}>
|
|
1364
|
+
↗ {session.openUrl}
|
|
1365
|
+
</Text>
|
|
1366
|
+
</Pressable>
|
|
1367
|
+
) : (
|
|
1368
|
+
<Text style={runnerAuthModalStyles.dim}>
|
|
1369
|
+
Waiting for verification URL from the remote CLI…
|
|
1370
|
+
</Text>
|
|
1371
|
+
)}
|
|
1372
|
+
{session.code ? (
|
|
1373
|
+
<View style={{ marginTop: 12 }}>
|
|
1374
|
+
<Text style={runnerAuthModalStyles.codeLabel}>ENTER THIS CODE</Text>
|
|
1375
|
+
<Pressable onPress={copyCode} style={runnerAuthModalStyles.codeBox}>
|
|
1376
|
+
<Text style={runnerAuthModalStyles.codeText}>{session.code}</Text>
|
|
1377
|
+
<Text style={runnerAuthModalStyles.codeHint}>
|
|
1378
|
+
{copied ? 'copied' : 'tap to copy'}
|
|
1379
|
+
</Text>
|
|
1380
|
+
</Pressable>
|
|
1381
|
+
</View>
|
|
1382
|
+
) : null}
|
|
1383
|
+
<Text style={runnerAuthModalStyles.phishingHint}>
|
|
1384
|
+
Device codes are a common phishing target. Never share this code. This dialog
|
|
1385
|
+
turns green automatically once sign-in completes.
|
|
1386
|
+
</Text>
|
|
1387
|
+
</View>
|
|
1388
|
+
)}
|
|
1389
|
+
</View>
|
|
1390
|
+
</View>
|
|
1391
|
+
</Modal>
|
|
1392
|
+
);
|
|
1393
|
+
};
|
|
1394
|
+
|
|
1395
|
+
const runnerAuthRowStyles = StyleSheet.create({
|
|
1396
|
+
container: {
|
|
1397
|
+
flexDirection: 'row',
|
|
1398
|
+
gap: 8,
|
|
1399
|
+
marginTop: 8,
|
|
1400
|
+
flexWrap: 'wrap',
|
|
1401
|
+
},
|
|
1402
|
+
button: {
|
|
1403
|
+
flexGrow: 1,
|
|
1404
|
+
flexBasis: 0,
|
|
1405
|
+
minWidth: 120,
|
|
1406
|
+
paddingHorizontal: 12,
|
|
1407
|
+
paddingVertical: 10,
|
|
1408
|
+
borderRadius: 10,
|
|
1409
|
+
borderWidth: 1,
|
|
1410
|
+
borderColor: 'rgba(148,163,184,0.22)',
|
|
1411
|
+
backgroundColor: 'rgba(15,23,42,0.6)',
|
|
1412
|
+
},
|
|
1413
|
+
buttonPressed: { opacity: 0.7 },
|
|
1414
|
+
buttonDisabled: { opacity: 0.4 },
|
|
1415
|
+
buttonLabel: {
|
|
1416
|
+
fontSize: 10,
|
|
1417
|
+
color: '#94a3b8',
|
|
1418
|
+
textTransform: 'uppercase',
|
|
1419
|
+
letterSpacing: 0.8,
|
|
1420
|
+
},
|
|
1421
|
+
buttonName: {
|
|
1422
|
+
marginTop: 2,
|
|
1423
|
+
fontSize: 14,
|
|
1424
|
+
fontWeight: '600',
|
|
1425
|
+
color: '#f1f5f9',
|
|
1426
|
+
},
|
|
1427
|
+
});
|
|
1428
|
+
|
|
1429
|
+
const runnerAuthModalStyles = StyleSheet.create({
|
|
1430
|
+
overlay: {
|
|
1431
|
+
flex: 1,
|
|
1432
|
+
justifyContent: 'center',
|
|
1433
|
+
alignItems: 'center',
|
|
1434
|
+
backgroundColor: 'rgba(2,6,23,0.75)',
|
|
1435
|
+
padding: 16,
|
|
1436
|
+
},
|
|
1437
|
+
card: {
|
|
1438
|
+
width: '100%',
|
|
1439
|
+
maxWidth: 420,
|
|
1440
|
+
backgroundColor: '#0f172a',
|
|
1441
|
+
borderRadius: 14,
|
|
1442
|
+
borderWidth: 1,
|
|
1443
|
+
borderColor: 'rgba(148,163,184,0.18)',
|
|
1444
|
+
padding: 18,
|
|
1445
|
+
},
|
|
1446
|
+
header: {
|
|
1447
|
+
flexDirection: 'row',
|
|
1448
|
+
alignItems: 'flex-start',
|
|
1449
|
+
marginBottom: 12,
|
|
1450
|
+
},
|
|
1451
|
+
title: { color: '#f1f5f9', fontSize: 16, fontWeight: '600' },
|
|
1452
|
+
subtitle: { color: '#94a3b8', fontSize: 11, marginTop: 2 },
|
|
1453
|
+
close: { color: '#94a3b8', fontSize: 22, lineHeight: 22, paddingHorizontal: 4 },
|
|
1454
|
+
dim: {
|
|
1455
|
+
color: '#94a3b8',
|
|
1456
|
+
fontSize: 12,
|
|
1457
|
+
padding: 12,
|
|
1458
|
+
borderRadius: 10,
|
|
1459
|
+
borderWidth: 1,
|
|
1460
|
+
borderColor: 'rgba(148,163,184,0.2)',
|
|
1461
|
+
backgroundColor: 'rgba(15,23,42,0.6)',
|
|
1462
|
+
},
|
|
1463
|
+
errorBox: {
|
|
1464
|
+
padding: 12,
|
|
1465
|
+
borderRadius: 10,
|
|
1466
|
+
borderWidth: 1,
|
|
1467
|
+
borderColor: 'rgba(248,113,113,0.35)',
|
|
1468
|
+
backgroundColor: 'rgba(248,113,113,0.1)',
|
|
1469
|
+
},
|
|
1470
|
+
errorTitle: { color: '#fca5a5', fontWeight: '600', marginBottom: 4, fontSize: 13 },
|
|
1471
|
+
errorBody: { color: '#fca5a5', fontSize: 12 },
|
|
1472
|
+
successBox: {
|
|
1473
|
+
padding: 14,
|
|
1474
|
+
borderRadius: 10,
|
|
1475
|
+
borderWidth: 1,
|
|
1476
|
+
borderColor: 'rgba(34,197,94,0.35)',
|
|
1477
|
+
backgroundColor: 'rgba(34,197,94,0.1)',
|
|
1478
|
+
},
|
|
1479
|
+
successTitle: { color: '#4ade80', fontSize: 14, fontWeight: '600', marginBottom: 4 },
|
|
1480
|
+
successBody: { color: '#86efac', fontSize: 12 },
|
|
1481
|
+
urlBox: {
|
|
1482
|
+
padding: 12,
|
|
1483
|
+
borderRadius: 10,
|
|
1484
|
+
borderWidth: 1,
|
|
1485
|
+
borderColor: 'rgba(99,102,241,0.35)',
|
|
1486
|
+
backgroundColor: 'rgba(99,102,241,0.1)',
|
|
1487
|
+
},
|
|
1488
|
+
urlText: { color: '#c7d2fe', fontSize: 13 },
|
|
1489
|
+
codeLabel: {
|
|
1490
|
+
fontSize: 10,
|
|
1491
|
+
fontWeight: '600',
|
|
1492
|
+
color: '#94a3b8',
|
|
1493
|
+
letterSpacing: 0.8,
|
|
1494
|
+
marginBottom: 4,
|
|
1495
|
+
},
|
|
1496
|
+
codeBox: {
|
|
1497
|
+
padding: 14,
|
|
1498
|
+
borderRadius: 10,
|
|
1499
|
+
borderWidth: 1,
|
|
1500
|
+
borderColor: 'rgba(148,163,184,0.22)',
|
|
1501
|
+
backgroundColor: 'rgba(15,23,42,0.8)',
|
|
1502
|
+
alignItems: 'center',
|
|
1503
|
+
},
|
|
1504
|
+
codeText: {
|
|
1505
|
+
color: '#f1f5f9',
|
|
1506
|
+
fontSize: 22,
|
|
1507
|
+
letterSpacing: 6,
|
|
1508
|
+
fontFamily: 'Menlo',
|
|
1509
|
+
},
|
|
1510
|
+
codeHint: { color: '#64748b', fontSize: 10, marginTop: 4, textTransform: 'uppercase' },
|
|
1511
|
+
phishingHint: {
|
|
1512
|
+
color: '#475569',
|
|
1513
|
+
fontSize: 10,
|
|
1514
|
+
marginTop: 12,
|
|
1515
|
+
lineHeight: 14,
|
|
1516
|
+
},
|
|
1517
|
+
});
|
package/src/LoginScreen.tsx
CHANGED
|
@@ -236,9 +236,8 @@ export const YaverLoginScreen: React.FC<YaverLoginScreenProps> = ({
|
|
|
236
236
|
contentContainerStyle={styles.scrollContainer}
|
|
237
237
|
keyboardShouldPersistTaps="handled"
|
|
238
238
|
>
|
|
239
|
-
<View style={styles.
|
|
240
|
-
<
|
|
241
|
-
<Text style={styles.subtitle}>Sign in to send feedback</Text>
|
|
239
|
+
<View style={styles.topBar}>
|
|
240
|
+
<View style={styles.topBarSpacer} />
|
|
242
241
|
{onCancel && (
|
|
243
242
|
<Pressable onPress={onCancel} style={styles.cancel}>
|
|
244
243
|
<Text style={styles.cancelText}>Cancel</Text>
|
|
@@ -246,6 +245,11 @@ export const YaverLoginScreen: React.FC<YaverLoginScreenProps> = ({
|
|
|
246
245
|
)}
|
|
247
246
|
</View>
|
|
248
247
|
|
|
248
|
+
<View style={styles.header}>
|
|
249
|
+
<Text style={styles.logo}>Yaver</Text>
|
|
250
|
+
<Text style={styles.subtitle}>Sign in to send feedback</Text>
|
|
251
|
+
</View>
|
|
252
|
+
|
|
249
253
|
<View style={styles.buttons}>
|
|
250
254
|
{Platform.OS === 'ios'
|
|
251
255
|
? renderProvider('apple', 'Continue with Apple', handleApple)
|
|
@@ -391,11 +395,21 @@ const styles = StyleSheet.create({
|
|
|
391
395
|
paddingHorizontal: 24,
|
|
392
396
|
justifyContent: 'center',
|
|
393
397
|
},
|
|
398
|
+
topBar: {
|
|
399
|
+
minHeight: 32,
|
|
400
|
+
marginBottom: 24,
|
|
401
|
+
flexDirection: 'row',
|
|
402
|
+
alignItems: 'center',
|
|
403
|
+
justifyContent: 'space-between',
|
|
404
|
+
},
|
|
405
|
+
topBarSpacer: {
|
|
406
|
+
width: 56,
|
|
407
|
+
},
|
|
394
408
|
header: { alignItems: 'center', marginBottom: 40 },
|
|
395
409
|
logo: { fontSize: 44, fontWeight: '800', color: '#e0e0e0', letterSpacing: -1 },
|
|
396
410
|
subtitle: { fontSize: 15, color: '#9ca3af', marginTop: 6 },
|
|
397
|
-
cancel: {
|
|
398
|
-
cancelText: { color: '#9ca3af', fontSize: 14 },
|
|
411
|
+
cancel: { minWidth: 56, alignItems: 'flex-end', paddingVertical: 8 },
|
|
412
|
+
cancelText: { color: '#9ca3af', fontSize: 14, fontWeight: '500' },
|
|
399
413
|
buttons: { gap: 12 },
|
|
400
414
|
button: {
|
|
401
415
|
backgroundColor: 'rgba(255,255,255,0.06)',
|
|
@@ -11,8 +11,10 @@ import {
|
|
|
11
11
|
} from 'react-native';
|
|
12
12
|
import {
|
|
13
13
|
DeviceList,
|
|
14
|
+
DeviceReachability,
|
|
14
15
|
RemoteDevice,
|
|
15
16
|
listReachableDevices,
|
|
17
|
+
probeDeviceReachability,
|
|
16
18
|
saveSelectedDeviceId,
|
|
17
19
|
} from './auth';
|
|
18
20
|
import { PairDeviceModal } from './PairDeviceModal';
|
|
@@ -45,6 +47,7 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
|
|
|
45
47
|
const [error, setError] = useState<string | null>(null);
|
|
46
48
|
const [list, setList] = useState<DeviceList>({ owned: [], shared: [] });
|
|
47
49
|
const [pairingDevice, setPairingDevice] = useState<RemoteDevice | null>(null);
|
|
50
|
+
const [reachability, setReachability] = useState<Record<string, DeviceReachability | undefined>>({});
|
|
48
51
|
|
|
49
52
|
const load = useCallback(async (silent = false) => {
|
|
50
53
|
if (!silent) setLoading(true);
|
|
@@ -52,6 +55,25 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
|
|
|
52
55
|
try {
|
|
53
56
|
const result = await listReachableDevices(token);
|
|
54
57
|
setList(result);
|
|
58
|
+
setReachability({});
|
|
59
|
+
void (async () => {
|
|
60
|
+
const devices = [...result.owned, ...result.shared];
|
|
61
|
+
const settled = await Promise.allSettled(
|
|
62
|
+
devices.map(async (device) => ({
|
|
63
|
+
deviceId: device.deviceId,
|
|
64
|
+
result: await probeDeviceReachability(device),
|
|
65
|
+
})),
|
|
66
|
+
);
|
|
67
|
+
setReachability((prev) => {
|
|
68
|
+
const next = { ...prev };
|
|
69
|
+
for (const entry of settled) {
|
|
70
|
+
if (entry.status === 'fulfilled') {
|
|
71
|
+
next[entry.value.deviceId] = entry.value.result;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return next;
|
|
75
|
+
});
|
|
76
|
+
})();
|
|
55
77
|
if (result.owned.length === 0 && result.shared.length === 0) {
|
|
56
78
|
setError(
|
|
57
79
|
'No machines found yet. If you do not have your own computer, redeem a host invite code first. Otherwise run `yaver auth` + `yaver serve` on your machine.',
|
|
@@ -79,12 +101,19 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
|
|
|
79
101
|
setPairingDevice(device);
|
|
80
102
|
return;
|
|
81
103
|
}
|
|
104
|
+
const direct = await probeDeviceReachability(device);
|
|
105
|
+
if (!direct.reachable && !device.needsAuth) {
|
|
106
|
+
setError('Selected machine is not responding. Start `yaver serve` on it and try again.');
|
|
107
|
+
setReachability((prev) => ({ ...prev, [device.deviceId]: direct }));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
82
110
|
await saveSelectedDeviceId(device.deviceId);
|
|
83
111
|
onPick(device);
|
|
84
112
|
};
|
|
85
113
|
|
|
86
114
|
const renderDevice = (device: RemoteDevice) => {
|
|
87
115
|
const selected = device.deviceId === currentDeviceId;
|
|
116
|
+
const probe = reachability[device.deviceId];
|
|
88
117
|
// Trust Convex's `isOnline` — the backend already gates it on a
|
|
89
118
|
// fresh 90 s heartbeat (see backend/convex/devices.ts
|
|
90
119
|
// deriveIsOnline). Re-checking on the client produced false
|
|
@@ -95,18 +124,28 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
|
|
|
95
124
|
// healthy — a separate concern from "can I reach this machine?"
|
|
96
125
|
// Mobile app surfaces runner issues via a separate badge, not
|
|
97
126
|
// this dot. Picker's job is reachability, nothing more.
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
127
|
+
const effectivelyReachable = probe?.reachable === true;
|
|
128
|
+
const explicitlyOffline = probe?.reachable === false;
|
|
129
|
+
const healthColor = device.needsAuth
|
|
130
|
+
? '#f59e0b'
|
|
131
|
+
: effectivelyReachable
|
|
132
|
+
? '#22c55e'
|
|
133
|
+
: explicitlyOffline || !device.isOnline
|
|
134
|
+
? '#ef4444'
|
|
135
|
+
: '#22c55e';
|
|
103
136
|
// Derive a single short status phrase the user can act on.
|
|
104
137
|
let statusLine = device.platform;
|
|
105
|
-
if (
|
|
138
|
+
if (probe === undefined) {
|
|
139
|
+
statusLine = 'Checking connection…';
|
|
140
|
+
} else if (!device.isOnline && effectivelyReachable) {
|
|
141
|
+
statusLine = 'Reachable now — waiting for cloud status to refresh';
|
|
142
|
+
} else if (!device.isOnline) {
|
|
106
143
|
statusLine = 'Offline — start `yaver serve` on the Mac';
|
|
107
144
|
} else if (device.needsAuth) {
|
|
108
145
|
statusLine =
|
|
109
146
|
'Needs pairing — open the Yaver app to adopt this machine';
|
|
147
|
+
} else if (explicitlyOffline) {
|
|
148
|
+
statusLine = 'Agent not responding on this machine';
|
|
110
149
|
} else if (device.runnerDown) {
|
|
111
150
|
statusLine = 'Runner down — restart the coding agent on the Mac';
|
|
112
151
|
} else {
|
package/src/P2PClient.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Platform } from 'react-native';
|
|
2
|
-
import { FeedbackBundle, TestSession, VoiceCapability } from './types';
|
|
2
|
+
import { FeedbackBundle, RunnerBrowserAuthSession, TestSession, VoiceCapability } from './types';
|
|
3
3
|
|
|
4
4
|
export interface FeedbackEvent {
|
|
5
5
|
type: string;
|
|
@@ -117,10 +117,18 @@ function friendlyReloadError(status: number, body: string): string {
|
|
|
117
117
|
export class P2PClient {
|
|
118
118
|
private baseUrl: string;
|
|
119
119
|
private authToken: string;
|
|
120
|
+
/**
|
|
121
|
+
* Shared relay password. Required when baseUrl points through the
|
|
122
|
+
* Yaver managed relay (e.g. https://public.yaver.io/d/<deviceId>) —
|
|
123
|
+
* the relay rejects unauthenticated requests with 401. Attached as
|
|
124
|
+
* X-Relay-Password on every agent request.
|
|
125
|
+
*/
|
|
126
|
+
private relayPassword: string;
|
|
120
127
|
|
|
121
|
-
constructor(baseUrl: string, authToken: string) {
|
|
128
|
+
constructor(baseUrl: string, authToken: string, relayPassword: string = '') {
|
|
122
129
|
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
123
130
|
this.authToken = authToken;
|
|
131
|
+
this.relayPassword = relayPassword;
|
|
124
132
|
}
|
|
125
133
|
|
|
126
134
|
/** Update the base URL (e.g. after re-discovery). */
|
|
@@ -133,6 +141,56 @@ export class P2PClient {
|
|
|
133
141
|
this.authToken = token;
|
|
134
142
|
}
|
|
135
143
|
|
|
144
|
+
/** Update the relay password (used for managed-relay baseUrls). */
|
|
145
|
+
setRelayPassword(password: string): void {
|
|
146
|
+
this.relayPassword = password;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Merge in Authorization + (optional) X-Relay-Password on top of a header block. */
|
|
150
|
+
private authHeaders(extra: Record<string, string> = {}): Record<string, string> {
|
|
151
|
+
const h: Record<string, string> = { ...extra };
|
|
152
|
+
if (this.authToken) h.Authorization = `Bearer ${this.authToken}`;
|
|
153
|
+
if (this.relayPassword) h['X-Relay-Password'] = this.relayPassword;
|
|
154
|
+
return h;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Start a remote browser-style sign-in for a runner (codex --device-auth
|
|
159
|
+
* / claude auth login --console). Returns a session id; callers poll
|
|
160
|
+
* getRunnerBrowserAuthStatus to surface the verification URL + one-time
|
|
161
|
+
* code. No API keys involved — the CLI writes its own auth.json once
|
|
162
|
+
* the user completes the flow in any browser.
|
|
163
|
+
*/
|
|
164
|
+
async startRunnerBrowserAuth(runner: string): Promise<RunnerBrowserAuthSession> {
|
|
165
|
+
const resp = await fetch(`${this.baseUrl}/runner-auth/browser/start`, {
|
|
166
|
+
method: 'POST',
|
|
167
|
+
headers: this.authHeaders({ 'Content-Type': 'application/json' }),
|
|
168
|
+
body: JSON.stringify({ runner }),
|
|
169
|
+
});
|
|
170
|
+
if (!resp.ok) {
|
|
171
|
+
const text = await resp.text().catch(() => '');
|
|
172
|
+
throw new Error(`startRunnerBrowserAuth(${runner}) HTTP ${resp.status}: ${text}`);
|
|
173
|
+
}
|
|
174
|
+
const data = await resp.json();
|
|
175
|
+
return data.session as RunnerBrowserAuthSession;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async getRunnerBrowserAuthStatus(sessionId: string): Promise<RunnerBrowserAuthSession> {
|
|
179
|
+
const url = `${this.baseUrl}/runner-auth/browser/status?id=${encodeURIComponent(sessionId)}`;
|
|
180
|
+
const resp = await fetch(url, { headers: this.authHeaders() });
|
|
181
|
+
if (!resp.ok) {
|
|
182
|
+
const text = await resp.text().catch(() => '');
|
|
183
|
+
throw new Error(`getRunnerBrowserAuthStatus HTTP ${resp.status}: ${text}`);
|
|
184
|
+
}
|
|
185
|
+
const data = await resp.json();
|
|
186
|
+
return data.session as RunnerBrowserAuthSession;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async cancelRunnerBrowserAuth(sessionId: string): Promise<void> {
|
|
190
|
+
const url = `${this.baseUrl}/runner-auth/browser/cancel?id=${encodeURIComponent(sessionId)}`;
|
|
191
|
+
try { await fetch(url, { method: 'POST', headers: this.authHeaders() }); } catch { /* best-effort */ }
|
|
192
|
+
}
|
|
193
|
+
|
|
136
194
|
/** Health check — returns true if the agent is reachable. */
|
|
137
195
|
async health(): Promise<boolean> {
|
|
138
196
|
try {
|
|
@@ -682,9 +740,7 @@ export class P2PClient {
|
|
|
682
740
|
private async request(method: string, path: string): Promise<Response> {
|
|
683
741
|
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
684
742
|
method,
|
|
685
|
-
headers:
|
|
686
|
-
Authorization: `Bearer ${this.authToken}`,
|
|
687
|
-
},
|
|
743
|
+
headers: this.authHeaders(),
|
|
688
744
|
});
|
|
689
745
|
|
|
690
746
|
if (!response.ok) {
|