yaver-feedback-react-native 0.8.13 → 0.9.0

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.
@@ -25,7 +25,13 @@ import {
25
25
  // stopVideoRecording,
26
26
  } from './capture';
27
27
  import { uploadFeedback } from './upload';
28
- import { DeviceInfo, FeedbackBundle } from './types';
28
+ import {
29
+ DeviceInfo,
30
+ FeedbackBundle,
31
+ OpenCodeConfigSummary,
32
+ OpenCodeProviderSummary,
33
+ RunnerAuthStatusRow,
34
+ } from './types';
29
35
  import { AuthOverlay } from './AuthOverlay';
30
36
  import { QuickActionIcon } from './QuickActionIcon';
31
37
  import { VibeChatScreen } from './VibeChatScreen';
@@ -34,6 +40,10 @@ import { listReachableDevices, RemoteDevice } from './auth';
34
40
  import {
35
41
  QUICK_ICON_COLOR_PRESETS,
36
42
  QuickIconColorPreset,
43
+ getPreferredModel,
44
+ getPreferredRunner,
45
+ setPreferredModel,
46
+ setPreferredRunner,
37
47
  } from './preferences';
38
48
 
39
49
  /**
@@ -64,6 +74,111 @@ type MachineCardState = {
64
74
  detail: string;
65
75
  };
66
76
 
77
+ type RunnerTone = 'ok' | 'warning' | 'error' | 'neutral';
78
+
79
+ type RunnerCardState = {
80
+ id: string;
81
+ name: string;
82
+ installed: boolean;
83
+ authConfigured: boolean;
84
+ ready: boolean;
85
+ version?: string;
86
+ tone: RunnerTone;
87
+ statusLine: string;
88
+ detail?: string;
89
+ actionLabel?: string;
90
+ actionRunner?: string;
91
+ };
92
+
93
+ type ProviderEditorState = {
94
+ mode: 'add' | 'edit';
95
+ id: string;
96
+ name: string;
97
+ baseUrl: string;
98
+ apiKey: string;
99
+ };
100
+
101
+ const PRIMARY_RUNNER_IDS = ['claude', 'codex', 'opencode'] as const;
102
+
103
+ function normalizeRunnerStatusRows(rows: RunnerAuthStatusRow[]): RunnerCardState[] {
104
+ const byId = new Map<string, RunnerAuthStatusRow>();
105
+ for (const row of rows) {
106
+ const raw = String(row.id || '').trim().toLowerCase();
107
+ if (!raw) continue;
108
+ const normalized = raw === 'claude-code' ? 'claude' : raw;
109
+ if (!PRIMARY_RUNNER_IDS.includes(normalized as (typeof PRIMARY_RUNNER_IDS)[number])) continue;
110
+ byId.set(normalized, { ...row, id: normalized });
111
+ }
112
+
113
+ return PRIMARY_RUNNER_IDS.map((id) => {
114
+ const baseName =
115
+ id === 'claude' ? 'Claude Code' : id === 'codex' ? 'OpenAI Codex' : 'OpenCode';
116
+ const row = byId.get(id);
117
+ if (!row) {
118
+ return {
119
+ id,
120
+ name: baseName,
121
+ installed: false,
122
+ authConfigured: false,
123
+ ready: false,
124
+ tone: 'warning',
125
+ statusLine: 'Not installed on the selected machine',
126
+ };
127
+ }
128
+
129
+ const versionPrefix = row.version?.trim() ? `${row.version.trim()} · ` : '';
130
+ const detail = row.error?.trim() || row.warning?.trim() || row.detail?.trim() || undefined;
131
+
132
+ if (!row.installed) {
133
+ return {
134
+ id,
135
+ name: row.name || baseName,
136
+ installed: false,
137
+ authConfigured: false,
138
+ ready: false,
139
+ version: row.version,
140
+ tone: 'warning',
141
+ statusLine: 'Not installed on the selected machine',
142
+ detail,
143
+ };
144
+ }
145
+
146
+ if (id === 'opencode') {
147
+ const configured = row.authConfigured || row.ready;
148
+ return {
149
+ id,
150
+ name: row.name || baseName,
151
+ installed: row.installed,
152
+ authConfigured: row.authConfigured,
153
+ ready: row.ready,
154
+ version: row.version,
155
+ tone: configured ? 'ok' : 'warning',
156
+ statusLine: configured
157
+ ? `${versionPrefix}Configured on the selected machine`
158
+ : `${versionPrefix}Needs provider config on the selected machine`,
159
+ detail,
160
+ };
161
+ }
162
+
163
+ const authed = row.authConfigured || row.ready;
164
+ return {
165
+ id,
166
+ name: row.name || baseName,
167
+ installed: row.installed,
168
+ authConfigured: row.authConfigured,
169
+ ready: row.ready,
170
+ version: row.version,
171
+ tone: authed ? 'ok' : 'warning',
172
+ statusLine: authed
173
+ ? `${versionPrefix}Signed in on the selected machine`
174
+ : `${versionPrefix}Not signed in on the selected machine`,
175
+ detail,
176
+ actionLabel: authed ? 'Re-auth' : 'Sign in',
177
+ actionRunner: id,
178
+ };
179
+ });
180
+ }
181
+
67
182
  export const FeedbackModal: React.FC = () => {
68
183
  const { width: winW, height: winH } = useWindowDimensions();
69
184
  const isTablet = Math.min(winW, winH) >= 600;
@@ -101,6 +216,14 @@ export const FeedbackModal: React.FC = () => {
101
216
  title: 'No machine selected',
102
217
  detail: 'Pick a remote dev machine before using the feedback actions.',
103
218
  });
219
+ const [runnerCards, setRunnerCards] = useState<RunnerCardState[]>(() =>
220
+ normalizeRunnerStatusRows([]),
221
+ );
222
+ const [runnerStatusLoading, setRunnerStatusLoading] = useState(false);
223
+ const [runnerStatusError, setRunnerStatusError] = useState<string | null>(null);
224
+ const [preferredRunner, setPreferredRunnerState] = useState<string | null>(null);
225
+ const [preferredModel, setPreferredModelState] = useState('');
226
+ const [showOpenCodeConfig, setShowOpenCodeConfig] = useState(false);
104
227
  const mountedRef = useRef(true);
105
228
 
106
229
  const loadSelectedMachine = useCallback(async () => {
@@ -204,6 +327,69 @@ export const FeedbackModal: React.FC = () => {
204
327
  }
205
328
  }, []);
206
329
 
330
+ const loadRunnerStatuses = useCallback(async () => {
331
+ const cfg = YaverFeedback.getConfig();
332
+ if (!cfg?.authToken) {
333
+ if (mountedRef.current) {
334
+ setRunnerCards(normalizeRunnerStatusRows([]));
335
+ setRunnerStatusError('Sign in to inspect coding-agent status.');
336
+ setRunnerStatusLoading(false);
337
+ }
338
+ return;
339
+ }
340
+ if (!cfg.preferredDeviceId) {
341
+ if (mountedRef.current) {
342
+ setRunnerCards(normalizeRunnerStatusRows([]));
343
+ setRunnerStatusError('Pick a machine to inspect coding-agent status.');
344
+ setRunnerStatusLoading(false);
345
+ }
346
+ return;
347
+ }
348
+
349
+ if (mountedRef.current) {
350
+ setRunnerStatusLoading(true);
351
+ setRunnerStatusError(null);
352
+ }
353
+
354
+ try {
355
+ let client = YaverFeedback.getP2PClient();
356
+ if (!client) {
357
+ const ok = await YaverFeedback.reconnect();
358
+ if (ok) client = YaverFeedback.getP2PClient();
359
+ }
360
+ if (!client) {
361
+ throw new Error('Not connected to the selected machine yet.');
362
+ }
363
+ const rows = await client.getRunnerAuthStatus();
364
+ if (mountedRef.current) {
365
+ setRunnerCards(normalizeRunnerStatusRows(rows));
366
+ }
367
+ } catch (err) {
368
+ if (mountedRef.current) {
369
+ setRunnerCards(normalizeRunnerStatusRows([]));
370
+ setRunnerStatusError(err instanceof Error ? err.message : String(err));
371
+ }
372
+ } finally {
373
+ if (mountedRef.current) setRunnerStatusLoading(false);
374
+ }
375
+ }, []);
376
+
377
+ const loadRoutingPrefs = useCallback(async () => {
378
+ try {
379
+ const [runner, model] = await Promise.all([
380
+ getPreferredRunner(),
381
+ getPreferredModel(),
382
+ ]);
383
+ if (!mountedRef.current) return;
384
+ setPreferredRunnerState(runner);
385
+ setPreferredModelState(model ?? '');
386
+ } catch {
387
+ if (!mountedRef.current) return;
388
+ setPreferredRunnerState(null);
389
+ setPreferredModelState('');
390
+ }
391
+ }, []);
392
+
207
393
  useEffect(() => {
208
394
  mountedRef.current = true;
209
395
  const sub = DeviceEventEmitter.addListener('yaverFeedback:startReport', () => {
@@ -229,6 +415,7 @@ export const FeedbackModal: React.FC = () => {
229
415
  })
230
416
  .catch(() => {});
231
417
  void loadSelectedMachine();
418
+ void loadRunnerStatuses();
232
419
  }
233
420
  });
234
421
  // Agent streams build / compile progress through the BlackBox
@@ -257,15 +444,16 @@ export const FeedbackModal: React.FC = () => {
257
444
  sub.remove();
258
445
  statusSub.remove();
259
446
  };
260
- }, [loadSelectedMachine]);
447
+ }, [loadRunnerStatuses, loadSelectedMachine]);
261
448
 
262
449
  useEffect(() => {
263
450
  if (!visible) return;
264
451
  const interval = setInterval(() => {
265
452
  void loadSelectedMachine();
453
+ void loadRunnerStatuses();
266
454
  }, 5000);
267
455
  return () => clearInterval(interval);
268
- }, [loadSelectedMachine, visible]);
456
+ }, [loadRunnerStatuses, loadSelectedMachine, visible]);
269
457
 
270
458
  useEffect(() => {
271
459
  if (!visible) {
@@ -301,6 +489,7 @@ export const FeedbackModal: React.FC = () => {
301
489
  setAction('idle');
302
490
  setShowVibeInput(false);
303
491
  setVibePrompt('');
492
+ setRunnerStatusError(null);
304
493
  }, []);
305
494
 
306
495
  // Helper: run a P2P call; on network failure, ask YaverFeedback to
@@ -552,6 +741,9 @@ export const FeedbackModal: React.FC = () => {
552
741
  const [activeVibe, setActiveVibe] = useState<{
553
742
  taskId: string;
554
743
  initialPrompt: string;
744
+ project?: string;
745
+ runner?: string;
746
+ model?: string;
555
747
  } | null>(null);
556
748
  const [includeScreenshot, setIncludeScreenshot] = useState<boolean>(true);
557
749
 
@@ -614,7 +806,13 @@ export const FeedbackModal: React.FC = () => {
614
806
  setLastVibeTaskId(result.taskId);
615
807
  // Hand off to VibeChatScreen — it streams the SSE transcript,
616
808
  // accepts follow-ups, and surfaces a Reload button.
617
- setActiveVibe({ taskId: result.taskId, initialPrompt: promptText });
809
+ setActiveVibe({
810
+ taskId: result.taskId,
811
+ initialPrompt: promptText,
812
+ project: identity.projectName,
813
+ runner: preferredRunner ?? undefined,
814
+ model: preferredModel ?? undefined,
815
+ });
618
816
  setVibePrompt('');
619
817
  setShowVibeInput(false);
620
818
  } catch (err: unknown) {
@@ -631,6 +829,11 @@ export const FeedbackModal: React.FC = () => {
631
829
  */
632
830
 
633
831
  const busy = action !== 'idle';
832
+ const readyRunnerCount = runnerCards.filter((row) => row.ready || row.authConfigured).length;
833
+ const missingRunnerCount = runnerCards.filter((row) => !row.installed).length;
834
+ const needsAuthRunnerCount = runnerCards.filter(
835
+ (row) => row.installed && !row.authConfigured && !row.ready,
836
+ ).length;
634
837
 
635
838
  // Once the user fires off a vibe task, swap the entire modal body
636
839
  // for the live chat screen. The chat manages its own SSE
@@ -653,6 +856,9 @@ export const FeedbackModal: React.FC = () => {
653
856
  client={client}
654
857
  initialTaskId={activeVibe.taskId}
655
858
  initialUserPrompt={activeVibe.initialPrompt}
859
+ project={activeVibe.project}
860
+ runner={activeVibe.runner}
861
+ model={activeVibe.model}
656
862
  onClose={() => setActiveVibe(null)}
657
863
  onReload={async () => {
658
864
  const c = YaverFeedback.getP2PClient();
@@ -766,6 +972,80 @@ export const FeedbackModal: React.FC = () => {
766
972
  <Text style={styles.machineMeta}>{machineCard.detail}</Text>
767
973
  </Pressable>
768
974
 
975
+ <View style={styles.runnerSection}>
976
+ <View style={styles.runnerSectionHeader}>
977
+ <View style={{ flex: 1 }}>
978
+ <Text style={styles.runnerSectionTitle}>Coding Agents</Text>
979
+ <Text style={styles.runnerSectionSubtitle}>
980
+ {runnerStatusLoading
981
+ ? 'Refreshing runner status on the selected machine…'
982
+ : `${readyRunnerCount} ready · ${needsAuthRunnerCount} need sign-in · ${missingRunnerCount} missing`}
983
+ </Text>
984
+ </View>
985
+ <Pressable
986
+ onPress={() => void loadRunnerStatuses()}
987
+ style={({ pressed }) => [
988
+ styles.runnerRefreshBtn,
989
+ pressed && styles.buttonPressed,
990
+ ]}
991
+ accessibilityRole="button"
992
+ accessibilityLabel="Refresh coding-agent status"
993
+ >
994
+ <Text style={styles.runnerRefreshBtnText}>
995
+ {runnerStatusLoading ? 'Refreshing…' : 'Refresh'}
996
+ </Text>
997
+ </Pressable>
998
+ </View>
999
+
1000
+ {runnerCards.map((row) => (
1001
+ <View
1002
+ key={row.id}
1003
+ style={[
1004
+ styles.runnerCard,
1005
+ row.tone === 'ok' && styles.runnerCardOk,
1006
+ row.tone === 'warning' && styles.runnerCardWarning,
1007
+ row.tone === 'error' && styles.runnerCardError,
1008
+ ]}
1009
+ >
1010
+ <View style={styles.runnerCardTop}>
1011
+ <View style={{ flex: 1 }}>
1012
+ <Text style={styles.runnerCardTitle}>{row.name}</Text>
1013
+ <Text
1014
+ style={[
1015
+ styles.runnerCardStatus,
1016
+ row.tone === 'ok' && styles.runnerCardStatusOk,
1017
+ row.tone === 'warning' && styles.runnerCardStatusWarning,
1018
+ row.tone === 'error' && styles.runnerCardStatusError,
1019
+ ]}
1020
+ >
1021
+ {row.statusLine}
1022
+ </Text>
1023
+ </View>
1024
+ {row.actionRunner ? (
1025
+ <Pressable
1026
+ onPress={() => setRunnerAuthModal(row.actionRunner ?? null)}
1027
+ style={({ pressed }) => [
1028
+ styles.runnerActionBtn,
1029
+ pressed && styles.buttonPressed,
1030
+ ]}
1031
+ accessibilityRole="button"
1032
+ accessibilityLabel={`${row.actionLabel} ${row.name}`}
1033
+ >
1034
+ <Text style={styles.runnerActionBtnText}>{row.actionLabel}</Text>
1035
+ </Pressable>
1036
+ ) : null}
1037
+ </View>
1038
+ {row.detail ? (
1039
+ <Text style={styles.runnerCardDetail}>{row.detail}</Text>
1040
+ ) : null}
1041
+ </View>
1042
+ ))}
1043
+
1044
+ {runnerStatusError ? (
1045
+ <Text style={styles.runnerSectionError}>{runnerStatusError}</Text>
1046
+ ) : null}
1047
+ </View>
1048
+
769
1049
  {quickIconHidden && (
770
1050
  <View style={styles.quickIconNote}>
771
1051
  <Text style={styles.quickIconNoteText}>
@@ -931,41 +1211,6 @@ export const FeedbackModal: React.FC = () => {
931
1211
  <DeployPanel onClose={() => setShowDeploy(false)} />
932
1212
  )}
933
1213
 
934
- {/* Remote sign-in buttons — trigger codex/claude device-auth
935
- on the selected agent without leaving the app. Opens a
936
- small native modal showing the verification URL + 8-char
937
- code the user enters in any browser. No API keys. */}
938
- <View style={runnerAuthRowStyles.container}>
939
- <Pressable
940
- onPress={() => setRunnerAuthModal('codex')}
941
- disabled={busy}
942
- style={({ pressed }) => [
943
- runnerAuthRowStyles.button,
944
- pressed && runnerAuthRowStyles.buttonPressed,
945
- busy && runnerAuthRowStyles.buttonDisabled,
946
- ]}
947
- accessibilityRole="button"
948
- accessibilityLabel="Remote sign-in Codex"
949
- >
950
- <Text style={runnerAuthRowStyles.buttonLabel}>Remote sign-in</Text>
951
- <Text style={runnerAuthRowStyles.buttonName}>Codex</Text>
952
- </Pressable>
953
- <Pressable
954
- onPress={() => setRunnerAuthModal('claude')}
955
- disabled={busy}
956
- style={({ pressed }) => [
957
- runnerAuthRowStyles.button,
958
- pressed && runnerAuthRowStyles.buttonPressed,
959
- busy && runnerAuthRowStyles.buttonDisabled,
960
- ]}
961
- accessibilityRole="button"
962
- accessibilityLabel="Remote sign-in Claude"
963
- >
964
- <Text style={runnerAuthRowStyles.buttonLabel}>Remote sign-in</Text>
965
- <Text style={runnerAuthRowStyles.buttonName}>Claude</Text>
966
- </Pressable>
967
- </View>
968
-
969
1214
  {progress !== null && (
970
1215
  <View style={styles.progressTrack}>
971
1216
  <View
@@ -999,7 +1244,10 @@ export const FeedbackModal: React.FC = () => {
999
1244
  {runnerAuthModal ? (
1000
1245
  <RunnerAuthNativeModal
1001
1246
  runner={runnerAuthModal}
1002
- onClose={() => setRunnerAuthModal(null)}
1247
+ onClose={() => {
1248
+ setRunnerAuthModal(null);
1249
+ void loadRunnerStatuses();
1250
+ }}
1003
1251
  />
1004
1252
  ) : null}
1005
1253
  </>
@@ -1227,6 +1475,106 @@ const styles = StyleSheet.create({
1227
1475
  marginTop: 4,
1228
1476
  lineHeight: 17,
1229
1477
  },
1478
+ runnerSection: {
1479
+ marginTop: 2,
1480
+ gap: 10,
1481
+ },
1482
+ runnerSectionHeader: {
1483
+ flexDirection: 'row',
1484
+ alignItems: 'center',
1485
+ gap: 10,
1486
+ },
1487
+ runnerSectionTitle: {
1488
+ color: '#f8fafc',
1489
+ fontSize: 16,
1490
+ fontWeight: '700',
1491
+ },
1492
+ runnerSectionSubtitle: {
1493
+ marginTop: 2,
1494
+ color: '#94a3b8',
1495
+ fontSize: 12,
1496
+ },
1497
+ runnerRefreshBtn: {
1498
+ borderRadius: 10,
1499
+ borderWidth: 1,
1500
+ borderColor: 'rgba(148,163,184,0.22)',
1501
+ backgroundColor: 'rgba(15,23,42,0.65)',
1502
+ paddingHorizontal: 10,
1503
+ paddingVertical: 8,
1504
+ },
1505
+ runnerRefreshBtnText: {
1506
+ color: '#cbd5e1',
1507
+ fontSize: 12,
1508
+ fontWeight: '600',
1509
+ },
1510
+ runnerCard: {
1511
+ borderRadius: 12,
1512
+ borderWidth: 1,
1513
+ borderColor: 'rgba(148,163,184,0.14)',
1514
+ backgroundColor: 'rgba(15,23,42,0.45)',
1515
+ paddingHorizontal: 12,
1516
+ paddingVertical: 11,
1517
+ gap: 6,
1518
+ },
1519
+ runnerCardOk: {
1520
+ borderColor: 'rgba(34,197,94,0.28)',
1521
+ backgroundColor: 'rgba(20,83,45,0.20)',
1522
+ },
1523
+ runnerCardWarning: {
1524
+ borderColor: 'rgba(251,191,36,0.28)',
1525
+ backgroundColor: 'rgba(120,53,15,0.18)',
1526
+ },
1527
+ runnerCardError: {
1528
+ borderColor: 'rgba(248,113,113,0.28)',
1529
+ backgroundColor: 'rgba(127,29,29,0.18)',
1530
+ },
1531
+ runnerCardTop: {
1532
+ flexDirection: 'row',
1533
+ alignItems: 'center',
1534
+ gap: 10,
1535
+ },
1536
+ runnerCardTitle: {
1537
+ color: '#f8fafc',
1538
+ fontSize: 14,
1539
+ fontWeight: '700',
1540
+ },
1541
+ runnerCardStatus: {
1542
+ marginTop: 2,
1543
+ fontSize: 12,
1544
+ color: '#cbd5e1',
1545
+ },
1546
+ runnerCardStatusOk: {
1547
+ color: '#86efac',
1548
+ },
1549
+ runnerCardStatusWarning: {
1550
+ color: '#fcd34d',
1551
+ },
1552
+ runnerCardStatusError: {
1553
+ color: '#fca5a5',
1554
+ },
1555
+ runnerCardDetail: {
1556
+ color: '#94a3b8',
1557
+ fontSize: 11,
1558
+ lineHeight: 16,
1559
+ },
1560
+ runnerActionBtn: {
1561
+ borderRadius: 10,
1562
+ borderWidth: 1,
1563
+ borderColor: 'rgba(129,140,248,0.35)',
1564
+ backgroundColor: 'rgba(67,56,202,0.22)',
1565
+ paddingHorizontal: 12,
1566
+ paddingVertical: 8,
1567
+ },
1568
+ runnerActionBtnText: {
1569
+ color: '#c7d2fe',
1570
+ fontSize: 12,
1571
+ fontWeight: '700',
1572
+ },
1573
+ runnerSectionError: {
1574
+ color: '#fca5a5',
1575
+ fontSize: 12,
1576
+ lineHeight: 18,
1577
+ },
1230
1578
  captureChoices: {
1231
1579
  gap: 10,
1232
1580
  },
@@ -1604,40 +1952,6 @@ const RunnerAuthNativeModal: React.FC<{
1604
1952
  );
1605
1953
  };
1606
1954
 
1607
- const runnerAuthRowStyles = StyleSheet.create({
1608
- container: {
1609
- flexDirection: 'row',
1610
- gap: 8,
1611
- marginTop: 8,
1612
- flexWrap: 'wrap',
1613
- },
1614
- button: {
1615
- flexGrow: 1,
1616
- flexBasis: 0,
1617
- minWidth: 120,
1618
- paddingHorizontal: 12,
1619
- paddingVertical: 10,
1620
- borderRadius: 10,
1621
- borderWidth: 1,
1622
- borderColor: 'rgba(148,163,184,0.22)',
1623
- backgroundColor: 'rgba(15,23,42,0.6)',
1624
- },
1625
- buttonPressed: { opacity: 0.7 },
1626
- buttonDisabled: { opacity: 0.4 },
1627
- buttonLabel: {
1628
- fontSize: 10,
1629
- color: '#94a3b8',
1630
- textTransform: 'uppercase',
1631
- letterSpacing: 0.8,
1632
- },
1633
- buttonName: {
1634
- marginTop: 2,
1635
- fontSize: 14,
1636
- fontWeight: '600',
1637
- color: '#f1f5f9',
1638
- },
1639
- });
1640
-
1641
1955
  const runnerAuthModalStyles = StyleSheet.create({
1642
1956
  overlay: {
1643
1957
  flex: 1,
@@ -102,7 +102,14 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
102
102
  return;
103
103
  }
104
104
  const direct = await probeDeviceReachability(device);
105
- if (!direct.reachable && !device.needsAuth) {
105
+ // Do not hard-block selection just because the LAN /health probe
106
+ // failed. The standalone SDK can still reach a healthy machine via
107
+ // the normal selected-device discovery path (including relay), and
108
+ // the Yaver host path may already be proving the machine works.
109
+ // Only treat the machine as unpickable when BOTH:
110
+ // 1. Convex says it is offline, and
111
+ // 2. the direct probe also failed.
112
+ if (!device.isOnline && !direct.reachable && !device.needsAuth) {
106
113
  setError('Selected machine is not responding. Start `yaver serve` on it and try again.');
107
114
  setReachability((prev) => ({ ...prev, [device.deviceId]: direct }));
108
115
  return;
@@ -130,7 +137,9 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
130
137
  ? '#f59e0b'
131
138
  : effectivelyReachable
132
139
  ? '#22c55e'
133
- : explicitlyOffline || !device.isOnline
140
+ : device.isOnline
141
+ ? '#f59e0b'
142
+ : explicitlyOffline || !device.isOnline
134
143
  ? '#ef4444'
135
144
  : '#22c55e';
136
145
  // Derive a single short status phrase the user can act on.
@@ -145,7 +154,7 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
145
154
  statusLine =
146
155
  'Needs pairing — open the Yaver app to adopt this machine';
147
156
  } else if (explicitlyOffline) {
148
- statusLine = 'Agent not responding on this machine';
157
+ statusLine = 'Online, but direct probe failed — relay / selected-machine path may still work';
149
158
  } else if (device.runnerDown) {
150
159
  statusLine = 'Runner down — restart the coding agent on the Mac';
151
160
  } else {