yaver-feedback-react-native 0.8.4 → 0.8.7
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/README.md +1 -1
- package/dist/AuthOverlay.js +45 -18
- package/dist/FeedbackModal.js +281 -0
- package/dist/P2PClient.d.ts +40 -2
- package/dist/P2PClient.js +115 -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/types.d.ts +76 -0
- package/package.json +2 -2
- package/src/AuthOverlay.tsx +48 -19
- package/src/FeedbackModal.tsx +320 -0
- package/src/P2PClient.ts +130 -5
- package/src/QuickActionIcon.tsx +37 -2
- package/src/YaverFeedback.ts +83 -34
- package/src/__tests__/AuthDevices.test.ts +93 -0
- package/src/types.ts +81 -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
|
|
@@ -809,6 +810,41 @@ export const FeedbackModal: React.FC = () => {
|
|
|
809
810
|
busy={action === 'capturing'}
|
|
810
811
|
/>
|
|
811
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
|
+
|
|
812
848
|
{progress !== null && (
|
|
813
849
|
<View style={styles.progressTrack}>
|
|
814
850
|
<View
|
|
@@ -839,6 +875,12 @@ export const FeedbackModal: React.FC = () => {
|
|
|
839
875
|
</Pressable>
|
|
840
876
|
</Modal>
|
|
841
877
|
)}
|
|
878
|
+
{runnerAuthModal ? (
|
|
879
|
+
<RunnerAuthNativeModal
|
|
880
|
+
runner={runnerAuthModal}
|
|
881
|
+
onClose={() => setRunnerAuthModal(null)}
|
|
882
|
+
/>
|
|
883
|
+
) : null}
|
|
842
884
|
</>
|
|
843
885
|
);
|
|
844
886
|
};
|
|
@@ -1195,3 +1237,281 @@ const styles = StyleSheet.create({
|
|
|
1195
1237
|
opacity: 0.7,
|
|
1196
1238
|
},
|
|
1197
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/P2PClient.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { Platform } from 'react-native';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
CapabilitySnapshot,
|
|
4
|
+
FeedbackBundle,
|
|
5
|
+
IncidentEvent,
|
|
6
|
+
OperationState,
|
|
7
|
+
RunnerBrowserAuthSession,
|
|
8
|
+
TestSession,
|
|
9
|
+
VoiceCapability,
|
|
10
|
+
} from './types';
|
|
3
11
|
|
|
4
12
|
export interface FeedbackEvent {
|
|
5
13
|
type: string;
|
|
@@ -117,10 +125,18 @@ function friendlyReloadError(status: number, body: string): string {
|
|
|
117
125
|
export class P2PClient {
|
|
118
126
|
private baseUrl: string;
|
|
119
127
|
private authToken: string;
|
|
128
|
+
/**
|
|
129
|
+
* Shared relay password. Required when baseUrl points through the
|
|
130
|
+
* Yaver managed relay (e.g. https://public.yaver.io/d/<deviceId>) —
|
|
131
|
+
* the relay rejects unauthenticated requests with 401. Attached as
|
|
132
|
+
* X-Relay-Password on every agent request.
|
|
133
|
+
*/
|
|
134
|
+
private relayPassword: string;
|
|
120
135
|
|
|
121
|
-
constructor(baseUrl: string, authToken: string) {
|
|
136
|
+
constructor(baseUrl: string, authToken: string, relayPassword: string = '') {
|
|
122
137
|
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
123
138
|
this.authToken = authToken;
|
|
139
|
+
this.relayPassword = relayPassword;
|
|
124
140
|
}
|
|
125
141
|
|
|
126
142
|
/** Update the base URL (e.g. after re-discovery). */
|
|
@@ -133,6 +149,117 @@ export class P2PClient {
|
|
|
133
149
|
this.authToken = token;
|
|
134
150
|
}
|
|
135
151
|
|
|
152
|
+
/** Update the relay password (used for managed-relay baseUrls). */
|
|
153
|
+
setRelayPassword(password: string): void {
|
|
154
|
+
this.relayPassword = password;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Merge in Authorization + (optional) X-Relay-Password on top of a header block. */
|
|
158
|
+
private authHeaders(extra: Record<string, string> = {}): Record<string, string> {
|
|
159
|
+
const h: Record<string, string> = { ...extra };
|
|
160
|
+
if (this.authToken) h.Authorization = `Bearer ${this.authToken}`;
|
|
161
|
+
if (this.relayPassword) h['X-Relay-Password'] = this.relayPassword;
|
|
162
|
+
return h;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Start a remote browser-style sign-in for a runner (codex --device-auth
|
|
167
|
+
* / claude auth login --console). Returns a session id; callers poll
|
|
168
|
+
* getRunnerBrowserAuthStatus to surface the verification URL + one-time
|
|
169
|
+
* code. No API keys involved — the CLI writes its own auth.json once
|
|
170
|
+
* the user completes the flow in any browser.
|
|
171
|
+
*/
|
|
172
|
+
async startRunnerBrowserAuth(runner: string): Promise<RunnerBrowserAuthSession> {
|
|
173
|
+
const resp = await fetch(`${this.baseUrl}/runner-auth/browser/start`, {
|
|
174
|
+
method: 'POST',
|
|
175
|
+
headers: this.authHeaders({ 'Content-Type': 'application/json' }),
|
|
176
|
+
body: JSON.stringify({ runner }),
|
|
177
|
+
});
|
|
178
|
+
if (!resp.ok) {
|
|
179
|
+
const text = await resp.text().catch(() => '');
|
|
180
|
+
throw new Error(`startRunnerBrowserAuth(${runner}) HTTP ${resp.status}: ${text}`);
|
|
181
|
+
}
|
|
182
|
+
const data = await resp.json();
|
|
183
|
+
return data.session as RunnerBrowserAuthSession;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async getRunnerBrowserAuthStatus(sessionId: string): Promise<RunnerBrowserAuthSession> {
|
|
187
|
+
const url = `${this.baseUrl}/runner-auth/browser/status?id=${encodeURIComponent(sessionId)}`;
|
|
188
|
+
const resp = await fetch(url, { headers: this.authHeaders() });
|
|
189
|
+
if (!resp.ok) {
|
|
190
|
+
const text = await resp.text().catch(() => '');
|
|
191
|
+
throw new Error(`getRunnerBrowserAuthStatus HTTP ${resp.status}: ${text}`);
|
|
192
|
+
}
|
|
193
|
+
const data = await resp.json();
|
|
194
|
+
return data.session as RunnerBrowserAuthSession;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async cancelRunnerBrowserAuth(sessionId: string): Promise<void> {
|
|
198
|
+
const url = `${this.baseUrl}/runner-auth/browser/cancel?id=${encodeURIComponent(sessionId)}`;
|
|
199
|
+
try { await fetch(url, { method: 'POST', headers: this.authHeaders() }); } catch { /* best-effort */ }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async capabilitySnapshot(): Promise<CapabilitySnapshot | null> {
|
|
203
|
+
try {
|
|
204
|
+
const resp = await fetch(`${this.baseUrl}/capabilities/snapshot`, { headers: this.authHeaders() });
|
|
205
|
+
if (!resp.ok) return null;
|
|
206
|
+
const data = await resp.json().catch(() => ({} as Record<string, unknown>));
|
|
207
|
+
return (data.snapshot ?? null) as CapabilitySnapshot | null;
|
|
208
|
+
} catch {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async incidents(opts: {
|
|
214
|
+
category?: string;
|
|
215
|
+
severity?: string;
|
|
216
|
+
code?: string;
|
|
217
|
+
deviceId?: string;
|
|
218
|
+
projectPath?: string;
|
|
219
|
+
includeResolved?: boolean;
|
|
220
|
+
limit?: number;
|
|
221
|
+
} = {}): Promise<IncidentEvent[]> {
|
|
222
|
+
try {
|
|
223
|
+
const url = new URL(`${this.baseUrl}/incidents`);
|
|
224
|
+
if (opts.category) url.searchParams.set('category', opts.category);
|
|
225
|
+
if (opts.severity) url.searchParams.set('severity', opts.severity);
|
|
226
|
+
if (opts.code) url.searchParams.set('code', opts.code);
|
|
227
|
+
if (opts.deviceId) url.searchParams.set('device', opts.deviceId);
|
|
228
|
+
if (opts.projectPath) url.searchParams.set('projectPath', opts.projectPath);
|
|
229
|
+
if (opts.includeResolved) url.searchParams.set('includeResolved', '1');
|
|
230
|
+
if (typeof opts.limit === 'number') url.searchParams.set('limit', String(opts.limit));
|
|
231
|
+
const resp = await fetch(url.toString(), { headers: this.authHeaders() });
|
|
232
|
+
if (!resp.ok) return [];
|
|
233
|
+
const data = await resp.json().catch(() => ({} as Record<string, unknown>));
|
|
234
|
+
return Array.isArray(data.incidents) ? (data.incidents as IncidentEvent[]) : [];
|
|
235
|
+
} catch {
|
|
236
|
+
return [];
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async operations(opts: {
|
|
241
|
+
kind?: string;
|
|
242
|
+
status?: string;
|
|
243
|
+
deviceId?: string;
|
|
244
|
+
projectPath?: string;
|
|
245
|
+
limit?: number;
|
|
246
|
+
} = {}): Promise<OperationState[]> {
|
|
247
|
+
try {
|
|
248
|
+
const url = new URL(`${this.baseUrl}/operations`);
|
|
249
|
+
if (opts.kind) url.searchParams.set('kind', opts.kind);
|
|
250
|
+
if (opts.status) url.searchParams.set('status', opts.status);
|
|
251
|
+
if (opts.deviceId) url.searchParams.set('device', opts.deviceId);
|
|
252
|
+
if (opts.projectPath) url.searchParams.set('projectPath', opts.projectPath);
|
|
253
|
+
if (typeof opts.limit === 'number') url.searchParams.set('limit', String(opts.limit));
|
|
254
|
+
const resp = await fetch(url.toString(), { headers: this.authHeaders() });
|
|
255
|
+
if (!resp.ok) return [];
|
|
256
|
+
const data = await resp.json().catch(() => ({} as Record<string, unknown>));
|
|
257
|
+
return Array.isArray(data.operations) ? (data.operations as OperationState[]) : [];
|
|
258
|
+
} catch {
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
136
263
|
/** Health check — returns true if the agent is reachable. */
|
|
137
264
|
async health(): Promise<boolean> {
|
|
138
265
|
try {
|
|
@@ -682,9 +809,7 @@ export class P2PClient {
|
|
|
682
809
|
private async request(method: string, path: string): Promise<Response> {
|
|
683
810
|
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
684
811
|
method,
|
|
685
|
-
headers:
|
|
686
|
-
Authorization: `Bearer ${this.authToken}`,
|
|
687
|
-
},
|
|
812
|
+
headers: this.authHeaders(),
|
|
688
813
|
});
|
|
689
814
|
|
|
690
815
|
if (!response.ok) {
|
package/src/QuickActionIcon.tsx
CHANGED
|
@@ -133,6 +133,7 @@ export const QuickActionIcon: React.FC<QuickActionIconProps> = ({
|
|
|
133
133
|
const [shakenThisSession, setShakenThisSession] = useState(false);
|
|
134
134
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
135
135
|
const [hostSuppressed] = useState<boolean>(() => isRunningInsideYaverHost());
|
|
136
|
+
const [launching, setLaunching] = useState(false);
|
|
136
137
|
|
|
137
138
|
// Load the persisted disable flag once on mount. Until it resolves we
|
|
138
139
|
// render nothing — a one-frame flash of the icon before hiding would
|
|
@@ -194,6 +195,37 @@ export const QuickActionIcon: React.FC<QuickActionIconProps> = ({
|
|
|
194
195
|
};
|
|
195
196
|
}, []);
|
|
196
197
|
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
const launchSub = DeviceEventEmitter.addListener(
|
|
200
|
+
'yaverFeedback:reportLaunch',
|
|
201
|
+
(event?: { state?: string }) => {
|
|
202
|
+
if (event?.state === 'starting') {
|
|
203
|
+
setLaunching(true);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
setLaunching(false);
|
|
207
|
+
},
|
|
208
|
+
);
|
|
209
|
+
const reportSub = DeviceEventEmitter.addListener(
|
|
210
|
+
'yaverFeedback:startReport',
|
|
211
|
+
() => setLaunching(false),
|
|
212
|
+
);
|
|
213
|
+
const loginSub = DeviceEventEmitter.addListener(
|
|
214
|
+
'yaverFeedback:startLogin',
|
|
215
|
+
() => setLaunching(false),
|
|
216
|
+
);
|
|
217
|
+
const pickerSub = DeviceEventEmitter.addListener(
|
|
218
|
+
'yaverFeedback:startMachinePicker',
|
|
219
|
+
() => setLaunching(false),
|
|
220
|
+
);
|
|
221
|
+
return () => {
|
|
222
|
+
launchSub.remove();
|
|
223
|
+
reportSub.remove();
|
|
224
|
+
loginSub.remove();
|
|
225
|
+
pickerSub.remove();
|
|
226
|
+
};
|
|
227
|
+
}, []);
|
|
228
|
+
|
|
197
229
|
const panResponder = useRef(
|
|
198
230
|
PanResponder.create({
|
|
199
231
|
onStartShouldSetPanResponder: () => true,
|
|
@@ -231,9 +263,10 @@ export const QuickActionIcon: React.FC<QuickActionIconProps> = ({
|
|
|
231
263
|
).current;
|
|
232
264
|
|
|
233
265
|
const openFeedback = useCallback(() => {
|
|
266
|
+
if (launching) return;
|
|
234
267
|
setMenuOpen(false);
|
|
235
268
|
void YaverFeedback.startReport();
|
|
236
|
-
}, []);
|
|
269
|
+
}, [launching]);
|
|
237
270
|
|
|
238
271
|
const hideForever = useCallback(() => {
|
|
239
272
|
setMenuOpen(false);
|
|
@@ -275,10 +308,12 @@ export const QuickActionIcon: React.FC<QuickActionIconProps> = ({
|
|
|
275
308
|
didDrag.current = false;
|
|
276
309
|
return;
|
|
277
310
|
}
|
|
311
|
+
if (launching) return;
|
|
278
312
|
openFeedback();
|
|
279
313
|
}}
|
|
280
314
|
onLongPress={() => {
|
|
281
315
|
if (didDrag.current) return;
|
|
316
|
+
if (launching) return;
|
|
282
317
|
setMenuOpen((m) => !m);
|
|
283
318
|
}}
|
|
284
319
|
delayLongPress={LONG_PRESS_MS}
|
|
@@ -294,7 +329,7 @@ export const QuickActionIcon: React.FC<QuickActionIconProps> = ({
|
|
|
294
329
|
backgroundColor: presetColors?.backgroundColor ?? backgroundColor,
|
|
295
330
|
borderColor: presetColors?.borderColor ?? borderColor,
|
|
296
331
|
shadowColor: presetColors?.shadowColor ?? shadowColor,
|
|
297
|
-
opacity: pressed ? 0.85 : 1,
|
|
332
|
+
opacity: launching ? 0.62 : pressed ? 0.85 : 1,
|
|
298
333
|
},
|
|
299
334
|
]}
|
|
300
335
|
>
|