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/dist/AuthOverlay.js
CHANGED
|
@@ -61,6 +61,31 @@ const AuthOverlay = () => {
|
|
|
61
61
|
const [pickerVisible, setPickerVisible] = (0, react_1.useState)(false);
|
|
62
62
|
const [token, setToken] = (0, react_1.useState)(null);
|
|
63
63
|
const [pendingInviteCode, setPendingInviteCode] = (0, react_1.useState)(null);
|
|
64
|
+
const activeOverlayRef = (0, react_1.useRef)('none');
|
|
65
|
+
const openLogin = (0, react_1.useCallback)(() => {
|
|
66
|
+
activeOverlayRef.current = 'login';
|
|
67
|
+
setGuestVisible(false);
|
|
68
|
+
setPickerVisible(false);
|
|
69
|
+
setLoginVisible(true);
|
|
70
|
+
}, []);
|
|
71
|
+
const openGuest = (0, react_1.useCallback)(() => {
|
|
72
|
+
activeOverlayRef.current = 'guest';
|
|
73
|
+
setLoginVisible(false);
|
|
74
|
+
setPickerVisible(false);
|
|
75
|
+
setGuestVisible(true);
|
|
76
|
+
}, []);
|
|
77
|
+
const openPicker = (0, react_1.useCallback)(() => {
|
|
78
|
+
activeOverlayRef.current = 'picker';
|
|
79
|
+
setLoginVisible(false);
|
|
80
|
+
setGuestVisible(false);
|
|
81
|
+
setPickerVisible(true);
|
|
82
|
+
}, []);
|
|
83
|
+
const closeAll = (0, react_1.useCallback)(() => {
|
|
84
|
+
activeOverlayRef.current = 'none';
|
|
85
|
+
setLoginVisible(false);
|
|
86
|
+
setGuestVisible(false);
|
|
87
|
+
setPickerVisible(false);
|
|
88
|
+
}, []);
|
|
64
89
|
(0, react_1.useEffect)(() => {
|
|
65
90
|
let mounted = true;
|
|
66
91
|
(async () => {
|
|
@@ -68,65 +93,67 @@ const AuthOverlay = () => {
|
|
|
68
93
|
if (mounted && cached)
|
|
69
94
|
setToken(cached);
|
|
70
95
|
})();
|
|
71
|
-
const loginSub = react_native_1.DeviceEventEmitter.addListener('yaverFeedback:startLogin', () =>
|
|
96
|
+
const loginSub = react_native_1.DeviceEventEmitter.addListener('yaverFeedback:startLogin', () => {
|
|
97
|
+
if (activeOverlayRef.current !== 'none')
|
|
98
|
+
return;
|
|
99
|
+
openLogin();
|
|
100
|
+
});
|
|
72
101
|
const pickerSub = react_native_1.DeviceEventEmitter.addListener('yaverFeedback:startMachinePicker', async () => {
|
|
102
|
+
if (activeOverlayRef.current !== 'none')
|
|
103
|
+
return;
|
|
73
104
|
const cached = await (0, auth_1.getToken)();
|
|
74
105
|
if (cached)
|
|
75
106
|
setToken(cached);
|
|
76
107
|
if (cached)
|
|
77
|
-
|
|
108
|
+
openPicker();
|
|
78
109
|
});
|
|
79
110
|
return () => {
|
|
80
111
|
mounted = false;
|
|
81
112
|
loginSub.remove();
|
|
82
113
|
pickerSub.remove();
|
|
83
114
|
};
|
|
84
|
-
}, []);
|
|
115
|
+
}, [openLogin, openPicker]);
|
|
85
116
|
const continueAfterAuth = async (newToken, inviteCode) => {
|
|
86
117
|
setToken(newToken);
|
|
87
118
|
await YaverFeedback_1.YaverFeedback.setAuthToken(newToken);
|
|
88
119
|
const devices = await (0, auth_1.listReachableDevices)(newToken).catch(() => ({ owned: [], shared: [] }));
|
|
89
|
-
setLoginVisible(false);
|
|
90
120
|
const cleanedInviteCode = (inviteCode ?? '').trim().toUpperCase();
|
|
91
121
|
if (cleanedInviteCode) {
|
|
92
122
|
setPendingInviteCode(cleanedInviteCode);
|
|
93
|
-
|
|
123
|
+
openGuest();
|
|
94
124
|
return;
|
|
95
125
|
}
|
|
96
126
|
if (devices.owned.length === 0 && devices.shared.length === 0) {
|
|
97
|
-
|
|
127
|
+
openGuest();
|
|
98
128
|
return;
|
|
99
129
|
}
|
|
100
|
-
|
|
130
|
+
openPicker();
|
|
101
131
|
};
|
|
102
132
|
const handleLoggedIn = async (newToken, opts) => {
|
|
103
133
|
await continueAfterAuth(newToken, opts?.inviteCode);
|
|
104
134
|
};
|
|
105
135
|
const handleDevicePicked = async (device) => {
|
|
106
136
|
await YaverFeedback_1.YaverFeedback.setPreferredDevice(device.deviceId);
|
|
107
|
-
|
|
108
|
-
setGuestVisible(false);
|
|
137
|
+
closeAll();
|
|
109
138
|
// Continue straight into the feedback flow the user originally triggered.
|
|
110
139
|
react_native_1.DeviceEventEmitter.emit('yaverFeedback:startReport');
|
|
111
140
|
};
|
|
112
141
|
return (<>
|
|
113
|
-
<react_native_1.Modal visible={loginVisible} animationType="slide" presentationStyle="fullScreen" onRequestClose={
|
|
114
|
-
<LoginScreen_1.YaverLoginScreen onLoggedIn={handleLoggedIn} onCancel={
|
|
142
|
+
<react_native_1.Modal visible={loginVisible} animationType="slide" presentationStyle="fullScreen" onRequestClose={closeAll}>
|
|
143
|
+
<LoginScreen_1.YaverLoginScreen onLoggedIn={handleLoggedIn} onCancel={closeAll} initialInviteCode={pendingInviteCode ?? YaverFeedback_1.YaverFeedback.getConfig()?.guestInviteCode}/>
|
|
115
144
|
</react_native_1.Modal>
|
|
116
145
|
|
|
117
|
-
<react_native_1.Modal visible={pickerVisible && !!token} animationType="slide" presentationStyle="fullScreen" onRequestClose={
|
|
118
|
-
{token && (<MachinePickerScreen_1.YaverMachinePickerScreen token={token} currentDeviceId={YaverFeedback_1.YaverFeedback.getConfig()?.preferredDeviceId} onPick={handleDevicePicked} onCancel={
|
|
146
|
+
<react_native_1.Modal visible={pickerVisible && !!token} animationType="slide" presentationStyle="fullScreen" onRequestClose={closeAll}>
|
|
147
|
+
{token && (<MachinePickerScreen_1.YaverMachinePickerScreen token={token} currentDeviceId={YaverFeedback_1.YaverFeedback.getConfig()?.preferredDeviceId} onPick={handleDevicePicked} onCancel={closeAll}/>)}
|
|
119
148
|
</react_native_1.Modal>
|
|
120
149
|
|
|
121
|
-
<react_native_1.Modal visible={guestVisible && !!token} animationType="slide" presentationStyle="fullScreen" onRequestClose={
|
|
150
|
+
<react_native_1.Modal visible={guestVisible && !!token} animationType="slide" presentationStyle="fullScreen" onRequestClose={closeAll}>
|
|
122
151
|
{token && (<GuestOnboardingScreen_1.YaverGuestOnboardingScreen token={token} initialInviteCode={pendingInviteCode ?? YaverFeedback_1.YaverFeedback.getConfig()?.guestInviteCode} onContinue={() => {
|
|
123
|
-
setGuestVisible(false);
|
|
124
152
|
setPendingInviteCode(null);
|
|
125
|
-
|
|
153
|
+
openPicker();
|
|
126
154
|
}} onCancel={() => {
|
|
127
|
-
setGuestVisible(false);
|
|
128
155
|
setPendingInviteCode(null);
|
|
129
|
-
|
|
156
|
+
openPicker();
|
|
130
157
|
}}/>)}
|
|
131
158
|
</react_native_1.Modal>
|
|
132
159
|
</>);
|
package/dist/FeedbackModal.js
CHANGED
|
@@ -54,6 +54,7 @@ const FeedbackModal = () => {
|
|
|
54
54
|
// is our guaranteed UI for bringing the icon back — we surface a
|
|
55
55
|
// small "Show quick icon" row when this is true.
|
|
56
56
|
const [quickIconHidden, setQuickIconHidden] = (0, react_1.useState)(false);
|
|
57
|
+
const [runnerAuthModal, setRunnerAuthModal] = (0, react_1.useState)(null);
|
|
57
58
|
// Vibing-input mode: same expand-on-tap pattern as email login.
|
|
58
59
|
// Tap "Vibing" once → the button reveals an input + Send; that lets
|
|
59
60
|
// the user say WHAT they want to vibe on instead of firing a canned
|
|
@@ -63,6 +64,7 @@ const FeedbackModal = () => {
|
|
|
63
64
|
const [vibePrompt, setVibePrompt] = (0, react_1.useState)('');
|
|
64
65
|
const [lastVibeTaskId, setLastVibeTaskId] = (0, react_1.useState)(null);
|
|
65
66
|
const [quickIconColorPreset, setQuickIconColorPreset] = (0, react_1.useState)(null);
|
|
67
|
+
const [keyboardInset, setKeyboardInset] = (0, react_1.useState)(0);
|
|
66
68
|
const [machineCard, setMachineCard] = (0, react_1.useState)({
|
|
67
69
|
device: null,
|
|
68
70
|
reachable: null,
|
|
@@ -231,6 +233,24 @@ const FeedbackModal = () => {
|
|
|
231
233
|
}, 5000);
|
|
232
234
|
return () => clearInterval(interval);
|
|
233
235
|
}, [loadSelectedMachine, visible]);
|
|
236
|
+
(0, react_1.useEffect)(() => {
|
|
237
|
+
if (!visible) {
|
|
238
|
+
setKeyboardInset(0);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const showEvent = react_native_1.Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
|
|
242
|
+
const hideEvent = react_native_1.Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide';
|
|
243
|
+
const showSub = react_native_1.Keyboard.addListener(showEvent, (event) => {
|
|
244
|
+
setKeyboardInset(event.endCoordinates?.height ?? 0);
|
|
245
|
+
});
|
|
246
|
+
const hideSub = react_native_1.Keyboard.addListener(hideEvent, () => {
|
|
247
|
+
setKeyboardInset(0);
|
|
248
|
+
});
|
|
249
|
+
return () => {
|
|
250
|
+
showSub.remove();
|
|
251
|
+
hideSub.remove();
|
|
252
|
+
};
|
|
253
|
+
}, [visible]);
|
|
234
254
|
const closeSoon = (0, react_1.useCallback)((delayMs = 1200) => {
|
|
235
255
|
setTimeout(() => {
|
|
236
256
|
if (mountedRef.current)
|
|
@@ -515,12 +535,17 @@ const FeedbackModal = () => {
|
|
|
515
535
|
<QuickActionIcon_1.QuickActionIcon />
|
|
516
536
|
{visible && (<react_native_1.Modal visible={visible} animationType="slide" transparent onRequestClose={handleClose}>
|
|
517
537
|
<react_native_1.Pressable style={styles.overlay} onPress={handleClose}>
|
|
518
|
-
<react_native_1.KeyboardAvoidingView behavior={react_native_1.Platform.OS === 'ios' ? 'padding' : 'height'} style={styles.kbAvoider} pointerEvents="box-none">
|
|
538
|
+
<react_native_1.KeyboardAvoidingView behavior={react_native_1.Platform.OS === 'ios' ? 'padding' : 'height'} keyboardVerticalOffset={react_native_1.Platform.OS === 'ios' ? 12 : 0} style={styles.kbAvoider} pointerEvents="box-none">
|
|
519
539
|
<react_native_1.Pressable style={styles.modal} onPress={(e) => {
|
|
520
540
|
e.stopPropagation();
|
|
521
541
|
react_native_1.Keyboard.dismiss();
|
|
522
542
|
}}>
|
|
523
|
-
<react_native_1.ScrollView style={styles.scroll} contentContainerStyle={
|
|
543
|
+
<react_native_1.ScrollView style={styles.scroll} contentContainerStyle={[
|
|
544
|
+
styles.scrollContent,
|
|
545
|
+
showVibeInput && keyboardInset > 0
|
|
546
|
+
? { paddingBottom: 8 + keyboardInset }
|
|
547
|
+
: null,
|
|
548
|
+
]} keyboardShouldPersistTaps="handled" keyboardDismissMode={react_native_1.Platform.OS === 'ios' ? 'interactive' : 'on-drag'}>
|
|
524
549
|
<react_native_1.View style={styles.header}>
|
|
525
550
|
<react_native_1.Text style={styles.title}>Send Feedback</react_native_1.Text>
|
|
526
551
|
<react_native_1.Pressable onPress={handleClose} hitSlop={12} style={styles.closeBtn} accessibilityRole="button" accessibilityLabel="Close">
|
|
@@ -642,6 +667,29 @@ const FeedbackModal = () => {
|
|
|
642
667
|
{/* Screenshot & Fix */}
|
|
643
668
|
<ActionRow label={action === 'capturing' ? 'Working…' : 'Screenshot & Fix'} tint="#22c55e" onPress={handleScreenshotAndFix} disabled={busy} busy={action === 'capturing'}/>
|
|
644
669
|
|
|
670
|
+
{/* Remote sign-in buttons — trigger codex/claude device-auth
|
|
671
|
+
on the selected agent without leaving the app. Opens a
|
|
672
|
+
small native modal showing the verification URL + 8-char
|
|
673
|
+
code the user enters in any browser. No API keys. */}
|
|
674
|
+
<react_native_1.View style={runnerAuthRowStyles.container}>
|
|
675
|
+
<react_native_1.Pressable onPress={() => setRunnerAuthModal('codex')} disabled={busy} style={({ pressed }) => [
|
|
676
|
+
runnerAuthRowStyles.button,
|
|
677
|
+
pressed && runnerAuthRowStyles.buttonPressed,
|
|
678
|
+
busy && runnerAuthRowStyles.buttonDisabled,
|
|
679
|
+
]} accessibilityRole="button" accessibilityLabel="Remote sign-in Codex">
|
|
680
|
+
<react_native_1.Text style={runnerAuthRowStyles.buttonLabel}>Remote sign-in</react_native_1.Text>
|
|
681
|
+
<react_native_1.Text style={runnerAuthRowStyles.buttonName}>Codex</react_native_1.Text>
|
|
682
|
+
</react_native_1.Pressable>
|
|
683
|
+
<react_native_1.Pressable onPress={() => setRunnerAuthModal('claude')} disabled={busy} style={({ pressed }) => [
|
|
684
|
+
runnerAuthRowStyles.button,
|
|
685
|
+
pressed && runnerAuthRowStyles.buttonPressed,
|
|
686
|
+
busy && runnerAuthRowStyles.buttonDisabled,
|
|
687
|
+
]} accessibilityRole="button" accessibilityLabel="Remote sign-in Claude">
|
|
688
|
+
<react_native_1.Text style={runnerAuthRowStyles.buttonLabel}>Remote sign-in</react_native_1.Text>
|
|
689
|
+
<react_native_1.Text style={runnerAuthRowStyles.buttonName}>Claude</react_native_1.Text>
|
|
690
|
+
</react_native_1.Pressable>
|
|
691
|
+
</react_native_1.View>
|
|
692
|
+
|
|
645
693
|
{progress !== null && (<react_native_1.View style={styles.progressTrack}>
|
|
646
694
|
<react_native_1.View style={[
|
|
647
695
|
styles.progressFill,
|
|
@@ -662,6 +710,7 @@ const FeedbackModal = () => {
|
|
|
662
710
|
</react_native_1.KeyboardAvoidingView>
|
|
663
711
|
</react_native_1.Pressable>
|
|
664
712
|
</react_native_1.Modal>)}
|
|
713
|
+
{runnerAuthModal ? (<RunnerAuthNativeModal runner={runnerAuthModal} onClose={() => setRunnerAuthModal(null)}/>) : null}
|
|
665
714
|
</>);
|
|
666
715
|
};
|
|
667
716
|
exports.FeedbackModal = FeedbackModal;
|
|
@@ -990,3 +1039,259 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
990
1039
|
opacity: 0.7,
|
|
991
1040
|
},
|
|
992
1041
|
});
|
|
1042
|
+
/**
|
|
1043
|
+
* Minimal native modal for the codex/claude remote sign-in flow. Opens
|
|
1044
|
+
* the device-auth session on the connected agent, surfaces the
|
|
1045
|
+
* verification URL + one-time code, polls every 1.5 s, and turns green
|
|
1046
|
+
* the moment the CLI writes its auth.json. No API keys, no SSH.
|
|
1047
|
+
*/
|
|
1048
|
+
const RunnerAuthNativeModal = ({ runner, onClose }) => {
|
|
1049
|
+
const [session, setSession] = (0, react_1.useState)(null);
|
|
1050
|
+
const [startError, setStartError] = (0, react_1.useState)(null);
|
|
1051
|
+
const [copied, setCopied] = (0, react_1.useState)(false);
|
|
1052
|
+
const startedRef = (0, react_1.useRef)(false);
|
|
1053
|
+
(0, react_1.useEffect)(() => {
|
|
1054
|
+
if (startedRef.current)
|
|
1055
|
+
return;
|
|
1056
|
+
startedRef.current = true;
|
|
1057
|
+
(async () => {
|
|
1058
|
+
try {
|
|
1059
|
+
const s = await YaverFeedback_1.YaverFeedback.startRunnerBrowserAuth(runner);
|
|
1060
|
+
setSession(s);
|
|
1061
|
+
}
|
|
1062
|
+
catch (err) {
|
|
1063
|
+
setStartError(err instanceof Error ? err.message : String(err));
|
|
1064
|
+
}
|
|
1065
|
+
})();
|
|
1066
|
+
}, [runner]);
|
|
1067
|
+
(0, react_1.useEffect)(() => {
|
|
1068
|
+
if (!session)
|
|
1069
|
+
return;
|
|
1070
|
+
if (['completed', 'failed', 'cancelled'].includes(session.status))
|
|
1071
|
+
return;
|
|
1072
|
+
const iv = setInterval(async () => {
|
|
1073
|
+
try {
|
|
1074
|
+
const s = await YaverFeedback_1.YaverFeedback.getRunnerBrowserAuthStatus(session.id);
|
|
1075
|
+
setSession(s);
|
|
1076
|
+
}
|
|
1077
|
+
catch {
|
|
1078
|
+
// keep polling
|
|
1079
|
+
}
|
|
1080
|
+
}, 1500);
|
|
1081
|
+
return () => clearInterval(iv);
|
|
1082
|
+
}, [session?.id, session?.status]);
|
|
1083
|
+
const terminal = session && ['completed', 'failed', 'cancelled'].includes(session.status);
|
|
1084
|
+
const runnerLabel = runner === 'codex' ? 'OpenAI Codex' : runner === 'claude' ? 'Claude Code' : runner;
|
|
1085
|
+
const handleClose = () => {
|
|
1086
|
+
if (session && !terminal) {
|
|
1087
|
+
YaverFeedback_1.YaverFeedback.cancelRunnerBrowserAuth(session.id).catch(() => { });
|
|
1088
|
+
}
|
|
1089
|
+
onClose();
|
|
1090
|
+
};
|
|
1091
|
+
const copyCode = () => {
|
|
1092
|
+
if (!session?.code)
|
|
1093
|
+
return;
|
|
1094
|
+
try {
|
|
1095
|
+
// Avoid a hard Clipboard dep — host app can polyfill.
|
|
1096
|
+
const Clipboard = require('react-native').Clipboard;
|
|
1097
|
+
if (Clipboard?.setString) {
|
|
1098
|
+
Clipboard.setString(session.code);
|
|
1099
|
+
setCopied(true);
|
|
1100
|
+
setTimeout(() => setCopied(false), 1500);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
catch {
|
|
1104
|
+
// best-effort — code is visible on screen regardless
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
const openUrl = () => {
|
|
1108
|
+
if (!session?.openUrl)
|
|
1109
|
+
return;
|
|
1110
|
+
try {
|
|
1111
|
+
const { Linking } = require('react-native');
|
|
1112
|
+
Linking.openURL(session.openUrl).catch(() => { });
|
|
1113
|
+
}
|
|
1114
|
+
catch {
|
|
1115
|
+
/* ignore */
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1118
|
+
return (<react_native_1.Modal visible={true} transparent animationType="fade" onRequestClose={handleClose}>
|
|
1119
|
+
<react_native_1.View style={runnerAuthModalStyles.overlay}>
|
|
1120
|
+
<react_native_1.View style={runnerAuthModalStyles.card}>
|
|
1121
|
+
<react_native_1.View style={runnerAuthModalStyles.header}>
|
|
1122
|
+
<react_native_1.View style={{ flex: 1 }}>
|
|
1123
|
+
<react_native_1.Text style={runnerAuthModalStyles.title}>Sign in to {runnerLabel}</react_native_1.Text>
|
|
1124
|
+
<react_native_1.Text style={runnerAuthModalStyles.subtitle}>
|
|
1125
|
+
Opens a one-time URL + code. Enter it in any browser.
|
|
1126
|
+
</react_native_1.Text>
|
|
1127
|
+
</react_native_1.View>
|
|
1128
|
+
<react_native_1.Pressable onPress={handleClose} hitSlop={10}>
|
|
1129
|
+
<react_native_1.Text style={runnerAuthModalStyles.close}>×</react_native_1.Text>
|
|
1130
|
+
</react_native_1.Pressable>
|
|
1131
|
+
</react_native_1.View>
|
|
1132
|
+
|
|
1133
|
+
{startError ? (<react_native_1.View style={runnerAuthModalStyles.errorBox}>
|
|
1134
|
+
<react_native_1.Text style={runnerAuthModalStyles.errorTitle}>Couldn't start</react_native_1.Text>
|
|
1135
|
+
<react_native_1.Text style={runnerAuthModalStyles.errorBody}>{startError}</react_native_1.Text>
|
|
1136
|
+
</react_native_1.View>) : !session ? (<react_native_1.Text style={runnerAuthModalStyles.dim}>
|
|
1137
|
+
Starting the sign-in flow on the remote machine…
|
|
1138
|
+
</react_native_1.Text>) : session.status === 'completed' ? (<react_native_1.View style={runnerAuthModalStyles.successBox}>
|
|
1139
|
+
<react_native_1.Text style={runnerAuthModalStyles.successTitle}>✓ Signed in</react_native_1.Text>
|
|
1140
|
+
<react_native_1.Text style={runnerAuthModalStyles.successBody}>
|
|
1141
|
+
{session.detail || 'Auth stored on the remote machine.'}
|
|
1142
|
+
</react_native_1.Text>
|
|
1143
|
+
</react_native_1.View>) : session.status === 'failed' || session.status === 'cancelled' ? (<react_native_1.View style={runnerAuthModalStyles.errorBox}>
|
|
1144
|
+
<react_native_1.Text style={runnerAuthModalStyles.errorTitle}>
|
|
1145
|
+
{session.status === 'cancelled' ? 'Cancelled' : 'Failed'}
|
|
1146
|
+
</react_native_1.Text>
|
|
1147
|
+
<react_native_1.Text style={runnerAuthModalStyles.errorBody}>
|
|
1148
|
+
{session.error || session.detail || 'The CLI exited before sign-in completed.'}
|
|
1149
|
+
</react_native_1.Text>
|
|
1150
|
+
</react_native_1.View>) : (<react_native_1.View>
|
|
1151
|
+
{session.openUrl ? (<react_native_1.Pressable onPress={openUrl} style={runnerAuthModalStyles.urlBox}>
|
|
1152
|
+
<react_native_1.Text style={runnerAuthModalStyles.urlText} numberOfLines={2}>
|
|
1153
|
+
↗ {session.openUrl}
|
|
1154
|
+
</react_native_1.Text>
|
|
1155
|
+
</react_native_1.Pressable>) : (<react_native_1.Text style={runnerAuthModalStyles.dim}>
|
|
1156
|
+
Waiting for verification URL from the remote CLI…
|
|
1157
|
+
</react_native_1.Text>)}
|
|
1158
|
+
{session.code ? (<react_native_1.View style={{ marginTop: 12 }}>
|
|
1159
|
+
<react_native_1.Text style={runnerAuthModalStyles.codeLabel}>ENTER THIS CODE</react_native_1.Text>
|
|
1160
|
+
<react_native_1.Pressable onPress={copyCode} style={runnerAuthModalStyles.codeBox}>
|
|
1161
|
+
<react_native_1.Text style={runnerAuthModalStyles.codeText}>{session.code}</react_native_1.Text>
|
|
1162
|
+
<react_native_1.Text style={runnerAuthModalStyles.codeHint}>
|
|
1163
|
+
{copied ? 'copied' : 'tap to copy'}
|
|
1164
|
+
</react_native_1.Text>
|
|
1165
|
+
</react_native_1.Pressable>
|
|
1166
|
+
</react_native_1.View>) : null}
|
|
1167
|
+
<react_native_1.Text style={runnerAuthModalStyles.phishingHint}>
|
|
1168
|
+
Device codes are a common phishing target. Never share this code. This dialog
|
|
1169
|
+
turns green automatically once sign-in completes.
|
|
1170
|
+
</react_native_1.Text>
|
|
1171
|
+
</react_native_1.View>)}
|
|
1172
|
+
</react_native_1.View>
|
|
1173
|
+
</react_native_1.View>
|
|
1174
|
+
</react_native_1.Modal>);
|
|
1175
|
+
};
|
|
1176
|
+
const runnerAuthRowStyles = react_native_1.StyleSheet.create({
|
|
1177
|
+
container: {
|
|
1178
|
+
flexDirection: 'row',
|
|
1179
|
+
gap: 8,
|
|
1180
|
+
marginTop: 8,
|
|
1181
|
+
flexWrap: 'wrap',
|
|
1182
|
+
},
|
|
1183
|
+
button: {
|
|
1184
|
+
flexGrow: 1,
|
|
1185
|
+
flexBasis: 0,
|
|
1186
|
+
minWidth: 120,
|
|
1187
|
+
paddingHorizontal: 12,
|
|
1188
|
+
paddingVertical: 10,
|
|
1189
|
+
borderRadius: 10,
|
|
1190
|
+
borderWidth: 1,
|
|
1191
|
+
borderColor: 'rgba(148,163,184,0.22)',
|
|
1192
|
+
backgroundColor: 'rgba(15,23,42,0.6)',
|
|
1193
|
+
},
|
|
1194
|
+
buttonPressed: { opacity: 0.7 },
|
|
1195
|
+
buttonDisabled: { opacity: 0.4 },
|
|
1196
|
+
buttonLabel: {
|
|
1197
|
+
fontSize: 10,
|
|
1198
|
+
color: '#94a3b8',
|
|
1199
|
+
textTransform: 'uppercase',
|
|
1200
|
+
letterSpacing: 0.8,
|
|
1201
|
+
},
|
|
1202
|
+
buttonName: {
|
|
1203
|
+
marginTop: 2,
|
|
1204
|
+
fontSize: 14,
|
|
1205
|
+
fontWeight: '600',
|
|
1206
|
+
color: '#f1f5f9',
|
|
1207
|
+
},
|
|
1208
|
+
});
|
|
1209
|
+
const runnerAuthModalStyles = react_native_1.StyleSheet.create({
|
|
1210
|
+
overlay: {
|
|
1211
|
+
flex: 1,
|
|
1212
|
+
justifyContent: 'center',
|
|
1213
|
+
alignItems: 'center',
|
|
1214
|
+
backgroundColor: 'rgba(2,6,23,0.75)',
|
|
1215
|
+
padding: 16,
|
|
1216
|
+
},
|
|
1217
|
+
card: {
|
|
1218
|
+
width: '100%',
|
|
1219
|
+
maxWidth: 420,
|
|
1220
|
+
backgroundColor: '#0f172a',
|
|
1221
|
+
borderRadius: 14,
|
|
1222
|
+
borderWidth: 1,
|
|
1223
|
+
borderColor: 'rgba(148,163,184,0.18)',
|
|
1224
|
+
padding: 18,
|
|
1225
|
+
},
|
|
1226
|
+
header: {
|
|
1227
|
+
flexDirection: 'row',
|
|
1228
|
+
alignItems: 'flex-start',
|
|
1229
|
+
marginBottom: 12,
|
|
1230
|
+
},
|
|
1231
|
+
title: { color: '#f1f5f9', fontSize: 16, fontWeight: '600' },
|
|
1232
|
+
subtitle: { color: '#94a3b8', fontSize: 11, marginTop: 2 },
|
|
1233
|
+
close: { color: '#94a3b8', fontSize: 22, lineHeight: 22, paddingHorizontal: 4 },
|
|
1234
|
+
dim: {
|
|
1235
|
+
color: '#94a3b8',
|
|
1236
|
+
fontSize: 12,
|
|
1237
|
+
padding: 12,
|
|
1238
|
+
borderRadius: 10,
|
|
1239
|
+
borderWidth: 1,
|
|
1240
|
+
borderColor: 'rgba(148,163,184,0.2)',
|
|
1241
|
+
backgroundColor: 'rgba(15,23,42,0.6)',
|
|
1242
|
+
},
|
|
1243
|
+
errorBox: {
|
|
1244
|
+
padding: 12,
|
|
1245
|
+
borderRadius: 10,
|
|
1246
|
+
borderWidth: 1,
|
|
1247
|
+
borderColor: 'rgba(248,113,113,0.35)',
|
|
1248
|
+
backgroundColor: 'rgba(248,113,113,0.1)',
|
|
1249
|
+
},
|
|
1250
|
+
errorTitle: { color: '#fca5a5', fontWeight: '600', marginBottom: 4, fontSize: 13 },
|
|
1251
|
+
errorBody: { color: '#fca5a5', fontSize: 12 },
|
|
1252
|
+
successBox: {
|
|
1253
|
+
padding: 14,
|
|
1254
|
+
borderRadius: 10,
|
|
1255
|
+
borderWidth: 1,
|
|
1256
|
+
borderColor: 'rgba(34,197,94,0.35)',
|
|
1257
|
+
backgroundColor: 'rgba(34,197,94,0.1)',
|
|
1258
|
+
},
|
|
1259
|
+
successTitle: { color: '#4ade80', fontSize: 14, fontWeight: '600', marginBottom: 4 },
|
|
1260
|
+
successBody: { color: '#86efac', fontSize: 12 },
|
|
1261
|
+
urlBox: {
|
|
1262
|
+
padding: 12,
|
|
1263
|
+
borderRadius: 10,
|
|
1264
|
+
borderWidth: 1,
|
|
1265
|
+
borderColor: 'rgba(99,102,241,0.35)',
|
|
1266
|
+
backgroundColor: 'rgba(99,102,241,0.1)',
|
|
1267
|
+
},
|
|
1268
|
+
urlText: { color: '#c7d2fe', fontSize: 13 },
|
|
1269
|
+
codeLabel: {
|
|
1270
|
+
fontSize: 10,
|
|
1271
|
+
fontWeight: '600',
|
|
1272
|
+
color: '#94a3b8',
|
|
1273
|
+
letterSpacing: 0.8,
|
|
1274
|
+
marginBottom: 4,
|
|
1275
|
+
},
|
|
1276
|
+
codeBox: {
|
|
1277
|
+
padding: 14,
|
|
1278
|
+
borderRadius: 10,
|
|
1279
|
+
borderWidth: 1,
|
|
1280
|
+
borderColor: 'rgba(148,163,184,0.22)',
|
|
1281
|
+
backgroundColor: 'rgba(15,23,42,0.8)',
|
|
1282
|
+
alignItems: 'center',
|
|
1283
|
+
},
|
|
1284
|
+
codeText: {
|
|
1285
|
+
color: '#f1f5f9',
|
|
1286
|
+
fontSize: 22,
|
|
1287
|
+
letterSpacing: 6,
|
|
1288
|
+
fontFamily: 'Menlo',
|
|
1289
|
+
},
|
|
1290
|
+
codeHint: { color: '#64748b', fontSize: 10, marginTop: 4, textTransform: 'uppercase' },
|
|
1291
|
+
phishingHint: {
|
|
1292
|
+
color: '#475569',
|
|
1293
|
+
fontSize: 10,
|
|
1294
|
+
marginTop: 12,
|
|
1295
|
+
lineHeight: 14,
|
|
1296
|
+
},
|
|
1297
|
+
});
|
package/dist/LoginScreen.js
CHANGED
|
@@ -179,14 +179,18 @@ const YaverLoginScreen = ({ onLoggedIn, onCancel, initialInviteCode, }) => {
|
|
|
179
179
|
return (<react_native_1.SafeAreaView style={styles.safeArea}>
|
|
180
180
|
<react_native_1.KeyboardAvoidingView style={{ flex: 1 }} behavior={react_native_1.Platform.OS === 'ios' ? 'padding' : undefined}>
|
|
181
181
|
<react_native_1.ScrollView contentContainerStyle={styles.scrollContainer} keyboardShouldPersistTaps="handled">
|
|
182
|
-
<react_native_1.View style={styles.
|
|
183
|
-
<react_native_1.
|
|
184
|
-
<react_native_1.Text style={styles.subtitle}>Sign in to send feedback</react_native_1.Text>
|
|
182
|
+
<react_native_1.View style={styles.topBar}>
|
|
183
|
+
<react_native_1.View style={styles.topBarSpacer}/>
|
|
185
184
|
{onCancel && (<react_native_1.Pressable onPress={onCancel} style={styles.cancel}>
|
|
186
185
|
<react_native_1.Text style={styles.cancelText}>Cancel</react_native_1.Text>
|
|
187
186
|
</react_native_1.Pressable>)}
|
|
188
187
|
</react_native_1.View>
|
|
189
188
|
|
|
189
|
+
<react_native_1.View style={styles.header}>
|
|
190
|
+
<react_native_1.Text style={styles.logo}>Yaver</react_native_1.Text>
|
|
191
|
+
<react_native_1.Text style={styles.subtitle}>Sign in to send feedback</react_native_1.Text>
|
|
192
|
+
</react_native_1.View>
|
|
193
|
+
|
|
190
194
|
<react_native_1.View style={styles.buttons}>
|
|
191
195
|
{react_native_1.Platform.OS === 'ios'
|
|
192
196
|
? renderProvider('apple', 'Continue with Apple', handleApple)
|
|
@@ -252,11 +256,21 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
252
256
|
paddingHorizontal: 24,
|
|
253
257
|
justifyContent: 'center',
|
|
254
258
|
},
|
|
259
|
+
topBar: {
|
|
260
|
+
minHeight: 32,
|
|
261
|
+
marginBottom: 24,
|
|
262
|
+
flexDirection: 'row',
|
|
263
|
+
alignItems: 'center',
|
|
264
|
+
justifyContent: 'space-between',
|
|
265
|
+
},
|
|
266
|
+
topBarSpacer: {
|
|
267
|
+
width: 56,
|
|
268
|
+
},
|
|
255
269
|
header: { alignItems: 'center', marginBottom: 40 },
|
|
256
270
|
logo: { fontSize: 44, fontWeight: '800', color: '#e0e0e0', letterSpacing: -1 },
|
|
257
271
|
subtitle: { fontSize: 15, color: '#9ca3af', marginTop: 6 },
|
|
258
|
-
cancel: {
|
|
259
|
-
cancelText: { color: '#9ca3af', fontSize: 14 },
|
|
272
|
+
cancel: { minWidth: 56, alignItems: 'flex-end', paddingVertical: 8 },
|
|
273
|
+
cancelText: { color: '#9ca3af', fontSize: 14, fontWeight: '500' },
|
|
260
274
|
buttons: { gap: 12 },
|
|
261
275
|
button: {
|
|
262
276
|
backgroundColor: 'rgba(255,255,255,0.06)',
|
|
@@ -53,6 +53,7 @@ const YaverMachinePickerScreen = ({ token, currentDeviceId, onPick, onCancel, })
|
|
|
53
53
|
const [error, setError] = (0, react_1.useState)(null);
|
|
54
54
|
const [list, setList] = (0, react_1.useState)({ owned: [], shared: [] });
|
|
55
55
|
const [pairingDevice, setPairingDevice] = (0, react_1.useState)(null);
|
|
56
|
+
const [reachability, setReachability] = (0, react_1.useState)({});
|
|
56
57
|
const load = (0, react_1.useCallback)(async (silent = false) => {
|
|
57
58
|
if (!silent)
|
|
58
59
|
setLoading(true);
|
|
@@ -60,6 +61,23 @@ const YaverMachinePickerScreen = ({ token, currentDeviceId, onPick, onCancel, })
|
|
|
60
61
|
try {
|
|
61
62
|
const result = await (0, auth_1.listReachableDevices)(token);
|
|
62
63
|
setList(result);
|
|
64
|
+
setReachability({});
|
|
65
|
+
void (async () => {
|
|
66
|
+
const devices = [...result.owned, ...result.shared];
|
|
67
|
+
const settled = await Promise.allSettled(devices.map(async (device) => ({
|
|
68
|
+
deviceId: device.deviceId,
|
|
69
|
+
result: await (0, auth_1.probeDeviceReachability)(device),
|
|
70
|
+
})));
|
|
71
|
+
setReachability((prev) => {
|
|
72
|
+
const next = { ...prev };
|
|
73
|
+
for (const entry of settled) {
|
|
74
|
+
if (entry.status === 'fulfilled') {
|
|
75
|
+
next[entry.value.deviceId] = entry.value.result;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return next;
|
|
79
|
+
});
|
|
80
|
+
})();
|
|
63
81
|
if (result.owned.length === 0 && result.shared.length === 0) {
|
|
64
82
|
setError('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.');
|
|
65
83
|
}
|
|
@@ -85,11 +103,18 @@ const YaverMachinePickerScreen = ({ token, currentDeviceId, onPick, onCancel, })
|
|
|
85
103
|
setPairingDevice(device);
|
|
86
104
|
return;
|
|
87
105
|
}
|
|
106
|
+
const direct = await (0, auth_1.probeDeviceReachability)(device);
|
|
107
|
+
if (!direct.reachable && !device.needsAuth) {
|
|
108
|
+
setError('Selected machine is not responding. Start `yaver serve` on it and try again.');
|
|
109
|
+
setReachability((prev) => ({ ...prev, [device.deviceId]: direct }));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
88
112
|
await (0, auth_1.saveSelectedDeviceId)(device.deviceId);
|
|
89
113
|
onPick(device);
|
|
90
114
|
};
|
|
91
115
|
const renderDevice = (device) => {
|
|
92
116
|
const selected = device.deviceId === currentDeviceId;
|
|
117
|
+
const probe = reachability[device.deviceId];
|
|
93
118
|
// Trust Convex's `isOnline` — the backend already gates it on a
|
|
94
119
|
// fresh 90 s heartbeat (see backend/convex/devices.ts
|
|
95
120
|
// deriveIsOnline). Re-checking on the client produced false
|
|
@@ -100,20 +125,33 @@ const YaverMachinePickerScreen = ({ token, currentDeviceId, onPick, onCancel, })
|
|
|
100
125
|
// healthy — a separate concern from "can I reach this machine?"
|
|
101
126
|
// Mobile app surfaces runner issues via a separate badge, not
|
|
102
127
|
// this dot. Picker's job is reachability, nothing more.
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
128
|
+
const effectivelyReachable = probe?.reachable === true;
|
|
129
|
+
const explicitlyOffline = probe?.reachable === false;
|
|
130
|
+
const healthColor = device.needsAuth
|
|
131
|
+
? '#f59e0b'
|
|
132
|
+
: effectivelyReachable
|
|
133
|
+
? '#22c55e'
|
|
134
|
+
: explicitlyOffline || !device.isOnline
|
|
135
|
+
? '#ef4444'
|
|
136
|
+
: '#22c55e';
|
|
108
137
|
// Derive a single short status phrase the user can act on.
|
|
109
138
|
let statusLine = device.platform;
|
|
110
|
-
if (
|
|
139
|
+
if (probe === undefined) {
|
|
140
|
+
statusLine = 'Checking connection…';
|
|
141
|
+
}
|
|
142
|
+
else if (!device.isOnline && effectivelyReachable) {
|
|
143
|
+
statusLine = 'Reachable now — waiting for cloud status to refresh';
|
|
144
|
+
}
|
|
145
|
+
else if (!device.isOnline) {
|
|
111
146
|
statusLine = 'Offline — start `yaver serve` on the Mac';
|
|
112
147
|
}
|
|
113
148
|
else if (device.needsAuth) {
|
|
114
149
|
statusLine =
|
|
115
150
|
'Needs pairing — open the Yaver app to adopt this machine';
|
|
116
151
|
}
|
|
152
|
+
else if (explicitlyOffline) {
|
|
153
|
+
statusLine = 'Agent not responding on this machine';
|
|
154
|
+
}
|
|
117
155
|
else if (device.runnerDown) {
|
|
118
156
|
statusLine = 'Runner down — restart the coding agent on the Mac';
|
|
119
157
|
}
|
package/dist/P2PClient.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FeedbackBundle, TestSession, VoiceCapability } from './types';
|
|
1
|
+
import { FeedbackBundle, RunnerBrowserAuthSession, TestSession, VoiceCapability } from './types';
|
|
2
2
|
export interface FeedbackEvent {
|
|
3
3
|
type: string;
|
|
4
4
|
timestamp: string;
|
|
@@ -21,11 +21,32 @@ export interface ReloadAck {
|
|
|
21
21
|
export declare class P2PClient {
|
|
22
22
|
private baseUrl;
|
|
23
23
|
private authToken;
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Shared relay password. Required when baseUrl points through the
|
|
26
|
+
* Yaver managed relay (e.g. https://public.yaver.io/d/<deviceId>) —
|
|
27
|
+
* the relay rejects unauthenticated requests with 401. Attached as
|
|
28
|
+
* X-Relay-Password on every agent request.
|
|
29
|
+
*/
|
|
30
|
+
private relayPassword;
|
|
31
|
+
constructor(baseUrl: string, authToken: string, relayPassword?: string);
|
|
25
32
|
/** Update the base URL (e.g. after re-discovery). */
|
|
26
33
|
setBaseUrl(url: string): void;
|
|
27
34
|
/** Update the auth token. */
|
|
28
35
|
setAuthToken(token: string): void;
|
|
36
|
+
/** Update the relay password (used for managed-relay baseUrls). */
|
|
37
|
+
setRelayPassword(password: string): void;
|
|
38
|
+
/** Merge in Authorization + (optional) X-Relay-Password on top of a header block. */
|
|
39
|
+
private authHeaders;
|
|
40
|
+
/**
|
|
41
|
+
* Start a remote browser-style sign-in for a runner (codex --device-auth
|
|
42
|
+
* / claude auth login --console). Returns a session id; callers poll
|
|
43
|
+
* getRunnerBrowserAuthStatus to surface the verification URL + one-time
|
|
44
|
+
* code. No API keys involved — the CLI writes its own auth.json once
|
|
45
|
+
* the user completes the flow in any browser.
|
|
46
|
+
*/
|
|
47
|
+
startRunnerBrowserAuth(runner: string): Promise<RunnerBrowserAuthSession>;
|
|
48
|
+
getRunnerBrowserAuthStatus(sessionId: string): Promise<RunnerBrowserAuthSession>;
|
|
49
|
+
cancelRunnerBrowserAuth(sessionId: string): Promise<void>;
|
|
29
50
|
/** Health check — returns true if the agent is reachable. */
|
|
30
51
|
health(): Promise<boolean>;
|
|
31
52
|
/** Get agent info (hostname, version, platform). */
|