yaver-feedback-react-native 0.8.13 → 0.9.2

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.
Files changed (76) hide show
  1. package/app.plugin.js +126 -9
  2. package/dist/BlackBox.js +9 -1
  3. package/dist/DeployPanel.js +65 -0
  4. package/dist/Discovery.js +13 -2
  5. package/dist/FeedbackModal.js +477 -66
  6. package/dist/MachinePickerScreen.js +14 -5
  7. package/dist/P2PClient.d.ts +94 -1
  8. package/dist/P2PClient.js +281 -2
  9. package/dist/ShakeDetector.js +2 -0
  10. package/dist/VibeChatScreen.d.ts +6 -1
  11. package/dist/VibeChatScreen.js +204 -1
  12. package/dist/YaverFeedback.d.ts +98 -0
  13. package/dist/YaverFeedback.js +509 -46
  14. package/dist/__tests__/BlackBox.relayPassword.test.d.ts +1 -0
  15. package/dist/__tests__/BlackBox.relayPassword.test.js +105 -0
  16. package/dist/__tests__/BlackBoxAutoStart.test.d.ts +1 -0
  17. package/dist/__tests__/BlackBoxAutoStart.test.js +91 -0
  18. package/dist/__tests__/BlackBoxAutoStartColdStart.test.d.ts +1 -0
  19. package/dist/__tests__/BlackBoxAutoStartColdStart.test.js +156 -0
  20. package/dist/__tests__/BrowserLaneIcon.test.d.ts +3 -0
  21. package/dist/__tests__/BrowserLaneIcon.test.js +80 -0
  22. package/dist/__tests__/P2PClient.test.js +2 -2
  23. package/dist/__tests__/ReportIdentity.test.d.ts +1 -0
  24. package/dist/__tests__/ReportIdentity.test.js +168 -0
  25. package/dist/__tests__/SDKToken.test.js +1 -1
  26. package/dist/__tests__/ShakeToggle.test.d.ts +1 -0
  27. package/dist/__tests__/ShakeToggle.test.js +121 -0
  28. package/dist/__tests__/pickTargetDevice.test.d.ts +1 -0
  29. package/dist/__tests__/pickTargetDevice.test.js +89 -0
  30. package/dist/__tests__/reloadActions.test.d.ts +1 -0
  31. package/dist/__tests__/reloadActions.test.js +129 -0
  32. package/dist/__tests__/reloadActionsParity.test.d.ts +1 -0
  33. package/dist/__tests__/reloadActionsParity.test.js +42 -0
  34. package/dist/__tests__/types.test.js +5 -5
  35. package/dist/_core/device.d.ts +19 -9
  36. package/dist/_core/device.js +20 -14
  37. package/dist/capture.d.ts +6 -0
  38. package/dist/capture.js +53 -0
  39. package/dist/index.d.ts +4 -0
  40. package/dist/index.js +11 -1
  41. package/dist/reloadActions.d.ts +88 -0
  42. package/dist/reloadActions.js +200 -0
  43. package/dist/storeShots.d.ts +67 -0
  44. package/dist/storeShots.js +137 -0
  45. package/dist/types.d.ts +215 -3
  46. package/dist/voice.d.ts +61 -0
  47. package/dist/voice.js +246 -0
  48. package/package.json +14 -3
  49. package/src/BlackBox.ts +10 -1
  50. package/src/DeployPanel.tsx +74 -0
  51. package/src/Discovery.ts +13 -2
  52. package/src/FeedbackModal.tsx +563 -87
  53. package/src/MachinePickerScreen.tsx +12 -3
  54. package/src/P2PClient.ts +314 -2
  55. package/src/ShakeDetector.ts +1 -0
  56. package/src/VibeChatScreen.tsx +219 -0
  57. package/src/YaverFeedback.ts +498 -46
  58. package/src/__tests__/BlackBox.relayPassword.test.ts +129 -0
  59. package/src/__tests__/BlackBoxAutoStart.test.ts +111 -0
  60. package/src/__tests__/BlackBoxAutoStartColdStart.test.ts +191 -0
  61. package/src/__tests__/BrowserLaneIcon.test.ts +85 -0
  62. package/src/__tests__/P2PClient.test.ts +2 -2
  63. package/src/__tests__/ReportIdentity.test.ts +203 -0
  64. package/src/__tests__/SDKToken.test.ts +1 -1
  65. package/src/__tests__/ShakeToggle.test.ts +153 -0
  66. package/src/__tests__/pickTargetDevice.test.ts +101 -0
  67. package/src/__tests__/reloadActions.test.ts +171 -0
  68. package/src/__tests__/reloadActionsParity.test.ts +49 -0
  69. package/src/__tests__/types.test.ts +5 -5
  70. package/src/_core/device.ts +20 -14
  71. package/src/capture.ts +51 -0
  72. package/src/index.ts +21 -0
  73. package/src/reloadActions.ts +273 -0
  74. package/src/storeShots.ts +189 -0
  75. package/src/types.ts +217 -3
  76. package/src/voice.ts +270 -0
@@ -39,12 +39,89 @@ const react_native_1 = require("react-native");
39
39
  const YaverFeedback_1 = require("./YaverFeedback");
40
40
  const capture_1 = require("./capture");
41
41
  const upload_1 = require("./upload");
42
+ const P2PClient_1 = require("./P2PClient");
42
43
  const AuthOverlay_1 = require("./AuthOverlay");
43
44
  const QuickActionIcon_1 = require("./QuickActionIcon");
44
45
  const VibeChatScreen_1 = require("./VibeChatScreen");
45
46
  const DeployPanel_1 = require("./DeployPanel");
46
47
  const auth_1 = require("./auth");
48
+ const reloadActions_1 = require("./reloadActions");
47
49
  const preferences_1 = require("./preferences");
50
+ const PRIMARY_RUNNER_IDS = ['claude', 'codex', 'opencode'];
51
+ function normalizeRunnerStatusRows(rows) {
52
+ const byId = new Map();
53
+ for (const row of rows) {
54
+ const raw = String(row.id || '').trim().toLowerCase();
55
+ if (!raw)
56
+ continue;
57
+ const normalized = raw === 'claude-code' ? 'claude' : raw;
58
+ if (!PRIMARY_RUNNER_IDS.includes(normalized))
59
+ continue;
60
+ byId.set(normalized, { ...row, id: normalized });
61
+ }
62
+ return PRIMARY_RUNNER_IDS.map((id) => {
63
+ const baseName = id === 'claude' ? 'Claude Code' : id === 'codex' ? 'OpenAI Codex' : 'OpenCode';
64
+ const row = byId.get(id);
65
+ if (!row) {
66
+ return {
67
+ id,
68
+ name: baseName,
69
+ installed: false,
70
+ authConfigured: false,
71
+ ready: false,
72
+ tone: 'warning',
73
+ statusLine: 'Not installed on the selected machine',
74
+ };
75
+ }
76
+ const versionPrefix = row.version?.trim() ? `${row.version.trim()} · ` : '';
77
+ const detail = row.error?.trim() || row.warning?.trim() || row.detail?.trim() || undefined;
78
+ if (!row.installed) {
79
+ return {
80
+ id,
81
+ name: row.name || baseName,
82
+ installed: false,
83
+ authConfigured: false,
84
+ ready: false,
85
+ version: row.version,
86
+ tone: 'warning',
87
+ statusLine: 'Not installed on the selected machine',
88
+ detail,
89
+ };
90
+ }
91
+ if (id === 'opencode') {
92
+ const configured = row.authConfigured || row.ready;
93
+ return {
94
+ id,
95
+ name: row.name || baseName,
96
+ installed: row.installed,
97
+ authConfigured: row.authConfigured,
98
+ ready: row.ready,
99
+ version: row.version,
100
+ tone: configured ? 'ok' : 'warning',
101
+ statusLine: configured
102
+ ? `${versionPrefix}Configured on the selected machine`
103
+ : `${versionPrefix}Needs provider config on the selected machine`,
104
+ detail,
105
+ };
106
+ }
107
+ const authed = row.authConfigured || row.ready;
108
+ return {
109
+ id,
110
+ name: row.name || baseName,
111
+ installed: row.installed,
112
+ authConfigured: row.authConfigured,
113
+ ready: row.ready,
114
+ version: row.version,
115
+ tone: authed ? 'ok' : 'warning',
116
+ statusLine: authed
117
+ ? `${versionPrefix}Signed in on the selected machine`
118
+ : `${versionPrefix}Not signed in on the selected machine`,
119
+ detail,
120
+ actionLabel: authed ? 'Re-auth' : 'Sign in',
121
+ actionRunner: id,
122
+ };
123
+ });
124
+ }
48
125
  const FeedbackModal = () => {
49
126
  const { width: winW, height: winH } = (0, react_native_1.useWindowDimensions)();
50
127
  const isTablet = Math.min(winW, winH) >= 600;
@@ -61,6 +138,11 @@ const FeedbackModal = () => {
61
138
  // is our guaranteed UI for bringing the icon back — we surface a
62
139
  // small "Show quick icon" row when this is true.
63
140
  const [quickIconHidden, setQuickIconHidden] = (0, react_1.useState)(false);
141
+ // Dev-server snapshot behind the reload buttons. `null` means "we have not
142
+ // been able to ask" — rendered as "not connected", never as "no dev server".
143
+ const [devSnapshot, setDevSnapshot] = (0, react_1.useState)(null);
144
+ // Which reload action is in flight, so only that button spins.
145
+ const [reloadingId, setReloadingId] = (0, react_1.useState)(null);
64
146
  const [runnerAuthModal, setRunnerAuthModal] = (0, react_1.useState)(null);
65
147
  // Vibing-input mode: same expand-on-tap pattern as email login.
66
148
  // Tap "Vibing" once → the button reveals an input + Send; that lets
@@ -81,7 +163,35 @@ const FeedbackModal = () => {
81
163
  title: 'No machine selected',
82
164
  detail: 'Pick a remote dev machine before using the feedback actions.',
83
165
  });
166
+ const [runnerCards, setRunnerCards] = (0, react_1.useState)(() => normalizeRunnerStatusRows([]));
167
+ const [runnerStatusLoading, setRunnerStatusLoading] = (0, react_1.useState)(false);
168
+ const [runnerStatusError, setRunnerStatusError] = (0, react_1.useState)(null);
169
+ const [preferredRunner, setPreferredRunnerState] = (0, react_1.useState)(null);
170
+ const [preferredModel, setPreferredModelState] = (0, react_1.useState)('');
171
+ const [showOpenCodeConfig, setShowOpenCodeConfig] = (0, react_1.useState)(false);
84
172
  const mountedRef = (0, react_1.useRef)(true);
173
+ /**
174
+ * Ask the machine what its dev server is doing, so the reload actions can
175
+ * be enabled/disabled against reality rather than against a guess.
176
+ *
177
+ * Best-effort and deliberately null-on-failure: null means "we could not
178
+ * ask", which the seam renders as "not connected to a machine yet" — a
179
+ * different sentence from "no dev server is running", because they have
180
+ * different fixes.
181
+ */
182
+ const refreshDevSnapshot = (0, react_1.useCallback)(async () => {
183
+ try {
184
+ const client = YaverFeedback_1.YaverFeedback.getP2PClient();
185
+ if (!client) {
186
+ setDevSnapshot(null);
187
+ return;
188
+ }
189
+ setDevSnapshot(await client.getDevServerStatus());
190
+ }
191
+ catch {
192
+ setDevSnapshot(null);
193
+ }
194
+ }, []);
85
195
  const loadSelectedMachine = (0, react_1.useCallback)(async () => {
86
196
  const cfg = YaverFeedback_1.YaverFeedback.getConfig();
87
197
  if (!cfg?.authToken) {
@@ -178,6 +288,72 @@ const FeedbackModal = () => {
178
288
  }
179
289
  }
180
290
  }, []);
291
+ const loadRunnerStatuses = (0, react_1.useCallback)(async () => {
292
+ const cfg = YaverFeedback_1.YaverFeedback.getConfig();
293
+ if (!cfg?.authToken) {
294
+ if (mountedRef.current) {
295
+ setRunnerCards(normalizeRunnerStatusRows([]));
296
+ setRunnerStatusError('Sign in to inspect coding-agent status.');
297
+ setRunnerStatusLoading(false);
298
+ }
299
+ return;
300
+ }
301
+ if (!cfg.preferredDeviceId) {
302
+ if (mountedRef.current) {
303
+ setRunnerCards(normalizeRunnerStatusRows([]));
304
+ setRunnerStatusError('Pick a machine to inspect coding-agent status.');
305
+ setRunnerStatusLoading(false);
306
+ }
307
+ return;
308
+ }
309
+ if (mountedRef.current) {
310
+ setRunnerStatusLoading(true);
311
+ setRunnerStatusError(null);
312
+ }
313
+ try {
314
+ let client = YaverFeedback_1.YaverFeedback.getP2PClient();
315
+ if (!client) {
316
+ const ok = await YaverFeedback_1.YaverFeedback.reconnect();
317
+ if (ok)
318
+ client = YaverFeedback_1.YaverFeedback.getP2PClient();
319
+ }
320
+ if (!client) {
321
+ throw new Error('Not connected to the selected machine yet.');
322
+ }
323
+ const rows = await client.getRunnerAuthStatus();
324
+ if (mountedRef.current) {
325
+ setRunnerCards(normalizeRunnerStatusRows(rows));
326
+ }
327
+ }
328
+ catch (err) {
329
+ if (mountedRef.current) {
330
+ setRunnerCards(normalizeRunnerStatusRows([]));
331
+ setRunnerStatusError(err instanceof Error ? err.message : String(err));
332
+ }
333
+ }
334
+ finally {
335
+ if (mountedRef.current)
336
+ setRunnerStatusLoading(false);
337
+ }
338
+ }, []);
339
+ const loadRoutingPrefs = (0, react_1.useCallback)(async () => {
340
+ try {
341
+ const [runner, model] = await Promise.all([
342
+ (0, preferences_1.getPreferredRunner)(),
343
+ (0, preferences_1.getPreferredModel)(),
344
+ ]);
345
+ if (!mountedRef.current)
346
+ return;
347
+ setPreferredRunnerState(runner);
348
+ setPreferredModelState(model ?? '');
349
+ }
350
+ catch {
351
+ if (!mountedRef.current)
352
+ return;
353
+ setPreferredRunnerState(null);
354
+ setPreferredModelState('');
355
+ }
356
+ }, []);
181
357
  (0, react_1.useEffect)(() => {
182
358
  mountedRef.current = true;
183
359
  const sub = react_native_1.DeviceEventEmitter.addListener('yaverFeedback:startReport', () => {
@@ -205,6 +381,7 @@ const FeedbackModal = () => {
205
381
  })
206
382
  .catch(() => { });
207
383
  void loadSelectedMachine();
384
+ void loadRunnerStatuses();
208
385
  }
209
386
  });
210
387
  // Agent streams build / compile progress through the BlackBox
@@ -232,15 +409,22 @@ const FeedbackModal = () => {
232
409
  sub.remove();
233
410
  statusSub.remove();
234
411
  };
235
- }, [loadSelectedMachine]);
412
+ }, [loadRunnerStatuses, loadSelectedMachine]);
236
413
  (0, react_1.useEffect)(() => {
237
414
  if (!visible)
238
415
  return;
416
+ // Poll the dev server alongside the machine + runners. A reload button
417
+ // whose enabled state was decided once, when the sheet opened, goes
418
+ // stale the moment the user starts Metro from another surface — and a
419
+ // stale "no dev server is running" reads as the product being broken.
420
+ void refreshDevSnapshot();
239
421
  const interval = setInterval(() => {
240
422
  void loadSelectedMachine();
423
+ void loadRunnerStatuses();
424
+ void refreshDevSnapshot();
241
425
  }, 5000);
242
426
  return () => clearInterval(interval);
243
- }, [loadSelectedMachine, visible]);
427
+ }, [loadRunnerStatuses, loadSelectedMachine, refreshDevSnapshot, visible]);
244
428
  (0, react_1.useEffect)(() => {
245
429
  if (!visible) {
246
430
  setKeyboardInset(0);
@@ -273,6 +457,7 @@ const FeedbackModal = () => {
273
457
  setAction('idle');
274
458
  setShowVibeInput(false);
275
459
  setVibePrompt('');
460
+ setRunnerStatusError(null);
276
461
  }, []);
277
462
  // Helper: run a P2P call; on network failure, ask YaverFeedback to
278
463
  // re-query Convex for the fresh IP and retry once. Solves the common
@@ -323,7 +508,81 @@ const FeedbackModal = () => {
323
508
  await fn(fresh);
324
509
  }
325
510
  }, []);
326
- // ─── 1. Hot reload ─────────────────────────────────────────────────
511
+ // ─── 1. Reload ─────────────────────────────────────────────────────
512
+ //
513
+ // Three actions now, not one: Hot Reload (mode=fast), Full Reload
514
+ // (mode=full — Flutter's hot RESTART), and Rebuild Bundle
515
+ // (/dev/reload-app, the only one that works with Metro down).
516
+ //
517
+ // WHICH of them render, and which are disabled with what reason, is
518
+ // decided by the pure `reloadActions()` seam — never inline here, so the
519
+ // same policy holds on web, Flutter, Unity, Swift and Kotlin. In
520
+ // particular: a production build (`__DEV__ === false`) gets NONE of them.
521
+ const availableReloadActions = (0, reloadActions_1.reloadActions)(devSnapshot, {
522
+ // __DEV__ is React Native's own build flag. A release bundle sets it
523
+ // false, so a shipped app renders no reload UI at all — which is the
524
+ // point, and is what reloadActions.test.ts pins.
525
+ isDevBuild: typeof __DEV__ !== 'undefined' && __DEV__ === true,
526
+ connected: devSnapshot !== null,
527
+ machineLabel: machineCard.device?.name || undefined,
528
+ includeRebuild: true,
529
+ });
530
+ const handleReloadAction = (0, react_1.useCallback)(async (reloadAction) => {
531
+ if (!reloadAction.enabled) {
532
+ // Pressing a disabled action must SAY why. A row that does nothing
533
+ // is the same defect as a spinner that never resolves.
534
+ setToast(reloadAction.disabledReason || 'Reload is unavailable right now.');
535
+ setError(reloadAction.disabledReason || null);
536
+ return;
537
+ }
538
+ setReloadingId(reloadAction.id);
539
+ setAction('hot-reloading');
540
+ setError(null);
541
+ setProgress(0);
542
+ setToast(`${reloadAction.label}…`);
543
+ try {
544
+ await loadSelectedMachine();
545
+ const selected = await YaverFeedback_1.YaverFeedback.getSelectedRemoteDevice();
546
+ if (!selected) {
547
+ YaverFeedback_1.YaverFeedback.showMachinePicker();
548
+ throw new Error('No machine selected. Pick a machine and try again.');
549
+ }
550
+ if (selected.needsAuth) {
551
+ YaverFeedback_1.YaverFeedback.showMachinePicker();
552
+ throw new Error('Selected machine needs pairing again.');
553
+ }
554
+ if (!selected.isOnline) {
555
+ throw new Error('Selected machine is offline. Start `yaver serve` on it first.');
556
+ }
557
+ let ackMessage = `${reloadAction.label} requested.`;
558
+ await runWithReconnect(async (client) => {
559
+ const ack = await client.reloadWithMode(reloadAction.mode, devSnapshot);
560
+ ackMessage = ack.message;
561
+ setToast(ack.message);
562
+ setProgress(0.2);
563
+ });
564
+ setToast(ackMessage);
565
+ if (reloadAction.mode === 'bundle')
566
+ closeSoon(2500);
567
+ }
568
+ catch (err) {
569
+ const message = err instanceof Error ? err.message : String(err);
570
+ setError(message);
571
+ setToast(message.toLowerCase().indexOf('session expired') >= 0
572
+ ? 'Session expired. Sign in again.'
573
+ : message);
574
+ await loadSelectedMachine();
575
+ setProgress(null);
576
+ }
577
+ finally {
578
+ if (mountedRef.current) {
579
+ setAction('idle');
580
+ setReloadingId(null);
581
+ void refreshDevSnapshot();
582
+ }
583
+ }
584
+ }, [closeSoon, devSnapshot, loadSelectedMachine, refreshDevSnapshot, runWithReconnect]);
585
+ /** Kept for the legacy one-tap path (BlackBox command, chat Reload button). */
327
586
  const handleHotReload = (0, react_1.useCallback)(async () => {
328
587
  setAction('hot-reloading');
329
588
  setError(null);
@@ -429,19 +688,36 @@ const FeedbackModal = () => {
429
688
  try {
430
689
  const { Dimensions } = require('react-native');
431
690
  const { width, height } = Dimensions.get('window');
691
+ const cfg = YaverFeedback_1.YaverFeedback.getConfig();
692
+ const identity = (0, P2PClient_1.resolveReportIdentity)({
693
+ projectName: cfg?.projectName,
694
+ bundleId: cfg?.bundleId,
695
+ surface: cfg?.surface,
696
+ surfaces: cfg?.surfaces,
697
+ stack: cfg?.stack,
698
+ stacks: cfg?.stacks,
699
+ testSurfaces: cfg?.testSurfaces,
700
+ feedbackSdk: cfg?.feedbackSdk,
701
+ feedbackTransport: cfg?.feedbackTransport,
702
+ voiceCapabilities: cfg?.voiceCapabilities,
703
+ sttProvider: cfg?.sttProvider,
704
+ ttsProvider: cfg?.ttsProvider,
705
+ });
432
706
  const deviceInfo = {
433
707
  platform: react_native_1.Platform.OS,
434
708
  osVersion: String(react_native_1.Platform.Version),
435
709
  model: react_native_1.Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
436
710
  screenWidth: width,
437
711
  screenHeight: height,
712
+ appName: identity.appName,
438
713
  };
439
714
  const capturedErrors = YaverFeedback_1.YaverFeedback.getCapturedErrors();
440
715
  const bundle = {
441
716
  metadata: {
442
717
  timestamp: new Date().toISOString(),
443
- device: deviceInfo,
444
- app: {},
718
+ deviceInfo,
719
+ app: identity.app,
720
+ project: identity.project,
445
721
  userNote: '[Screenshot + Fix]',
446
722
  },
447
723
  screenshots: [path],
@@ -557,7 +833,13 @@ const FeedbackModal = () => {
557
833
  setLastVibeTaskId(result.taskId);
558
834
  // Hand off to VibeChatScreen — it streams the SSE transcript,
559
835
  // accepts follow-ups, and surfaces a Reload button.
560
- setActiveVibe({ taskId: result.taskId, initialPrompt: promptText });
836
+ setActiveVibe({
837
+ taskId: result.taskId,
838
+ initialPrompt: promptText,
839
+ project: identity.projectName,
840
+ runner: preferredRunner ?? undefined,
841
+ model: preferredModel ?? undefined,
842
+ });
561
843
  setVibePrompt('');
562
844
  setShowVibeInput(false);
563
845
  }
@@ -575,6 +857,9 @@ const FeedbackModal = () => {
575
857
  }, [closeSoon, isRecordingVideo, lastVideo]);
576
858
  */
577
859
  const busy = action !== 'idle';
860
+ const readyRunnerCount = runnerCards.filter((row) => row.ready || row.authConfigured).length;
861
+ const missingRunnerCount = runnerCards.filter((row) => !row.installed).length;
862
+ const needsAuthRunnerCount = runnerCards.filter((row) => row.installed && !row.authConfigured && !row.ready).length;
578
863
  // Once the user fires off a vibe task, swap the entire modal body
579
864
  // for the live chat screen. The chat manages its own SSE
580
865
  // subscription, multi-turn follow-ups, and Reload button. Closing
@@ -585,7 +870,7 @@ const FeedbackModal = () => {
585
870
  <AuthOverlay_1.AuthOverlay />
586
871
  <QuickActionIcon_1.QuickActionIcon />
587
872
  <react_native_1.Modal visible={visible} animationType="slide" transparent onRequestClose={() => setActiveVibe(null)}>
588
- {client ? (<VibeChatScreen_1.VibeChatScreen client={client} initialTaskId={activeVibe.taskId} initialUserPrompt={activeVibe.initialPrompt} onClose={() => setActiveVibe(null)} onReload={async () => {
873
+ {client ? (<VibeChatScreen_1.VibeChatScreen client={client} initialTaskId={activeVibe.taskId} initialUserPrompt={activeVibe.initialPrompt} project={activeVibe.project} runner={activeVibe.runner} model={activeVibe.model} onClose={() => setActiveVibe(null)} onReload={async () => {
589
874
  const c = YaverFeedback_1.YaverFeedback.getP2PClient();
590
875
  if (!c)
591
876
  throw new Error('Not connected');
@@ -664,6 +949,57 @@ const FeedbackModal = () => {
664
949
  <react_native_1.Text style={styles.machineMeta}>{machineCard.detail}</react_native_1.Text>
665
950
  </react_native_1.Pressable>
666
951
 
952
+ <react_native_1.View style={styles.runnerSection}>
953
+ <react_native_1.View style={styles.runnerSectionHeader}>
954
+ <react_native_1.View style={{ flex: 1 }}>
955
+ <react_native_1.Text style={styles.runnerSectionTitle}>Coding Agents</react_native_1.Text>
956
+ <react_native_1.Text style={styles.runnerSectionSubtitle}>
957
+ {runnerStatusLoading
958
+ ? 'Refreshing runner status on the selected machine…'
959
+ : `${readyRunnerCount} ready · ${needsAuthRunnerCount} need sign-in · ${missingRunnerCount} missing`}
960
+ </react_native_1.Text>
961
+ </react_native_1.View>
962
+ <react_native_1.Pressable onPress={() => void loadRunnerStatuses()} style={({ pressed }) => [
963
+ styles.runnerRefreshBtn,
964
+ pressed && styles.buttonPressed,
965
+ ]} accessibilityRole="button" accessibilityLabel="Refresh coding-agent status">
966
+ <react_native_1.Text style={styles.runnerRefreshBtnText}>
967
+ {runnerStatusLoading ? 'Refreshing…' : 'Refresh'}
968
+ </react_native_1.Text>
969
+ </react_native_1.Pressable>
970
+ </react_native_1.View>
971
+
972
+ {runnerCards.map((row) => (<react_native_1.View key={row.id} style={[
973
+ styles.runnerCard,
974
+ row.tone === 'ok' && styles.runnerCardOk,
975
+ row.tone === 'warning' && styles.runnerCardWarning,
976
+ row.tone === 'error' && styles.runnerCardError,
977
+ ]}>
978
+ <react_native_1.View style={styles.runnerCardTop}>
979
+ <react_native_1.View style={{ flex: 1 }}>
980
+ <react_native_1.Text style={styles.runnerCardTitle}>{row.name}</react_native_1.Text>
981
+ <react_native_1.Text style={[
982
+ styles.runnerCardStatus,
983
+ row.tone === 'ok' && styles.runnerCardStatusOk,
984
+ row.tone === 'warning' && styles.runnerCardStatusWarning,
985
+ row.tone === 'error' && styles.runnerCardStatusError,
986
+ ]}>
987
+ {row.statusLine}
988
+ </react_native_1.Text>
989
+ </react_native_1.View>
990
+ {row.actionRunner ? (<react_native_1.Pressable onPress={() => setRunnerAuthModal(row.actionRunner ?? null)} style={({ pressed }) => [
991
+ styles.runnerActionBtn,
992
+ pressed && styles.buttonPressed,
993
+ ]} accessibilityRole="button" accessibilityLabel={`${row.actionLabel} ${row.name}`}>
994
+ <react_native_1.Text style={styles.runnerActionBtnText}>{row.actionLabel}</react_native_1.Text>
995
+ </react_native_1.Pressable>) : null}
996
+ </react_native_1.View>
997
+ {row.detail ? (<react_native_1.Text style={styles.runnerCardDetail}>{row.detail}</react_native_1.Text>) : null}
998
+ </react_native_1.View>))}
999
+
1000
+ {runnerStatusError ? (<react_native_1.Text style={styles.runnerSectionError}>{runnerStatusError}</react_native_1.Text>) : null}
1001
+ </react_native_1.View>
1002
+
667
1003
  {quickIconHidden && (<react_native_1.View style={styles.quickIconNote}>
668
1004
  <react_native_1.Text style={styles.quickIconNoteText}>
669
1005
  Quick access icon is hidden. Shake the phone if you want feedback back fast.
@@ -716,8 +1052,27 @@ const FeedbackModal = () => {
716
1052
  </react_native_1.View>
717
1053
  </react_native_1.View>
718
1054
 
719
- {/* 1. Hot Reload — the common path */}
720
- <ActionRow label={action === 'hot-reloading' ? 'Reloading…' : 'Hot Reload'} tint="#fbbf24" onPress={handleHotReload} disabled={busy} busy={action === 'hot-reloading'}/>
1055
+ {/* 1. Reload — Hot / Full / Rebuild Bundle.
1056
+ Rendered from the shared decision seam, so a production
1057
+ build (__DEV__ false) renders nothing here at all, and a
1058
+ blocked action shows greyed WITH its reason underneath
1059
+ rather than vanishing. */}
1060
+ {availableReloadActions.map((reloadAction) => (<react_native_1.View key={reloadAction.id} style={styles.reloadRow}>
1061
+ <ActionRow label={reloadingId === reloadAction.id
1062
+ ? `${reloadAction.label}…`
1063
+ : reloadAction.label} tint={reloadAction.id === 'rebuild' ? '#38bdf8' : '#fbbf24'} onPress={() => {
1064
+ void handleReloadAction(reloadAction);
1065
+ }}
1066
+ // Never `disabled` at the Pressable level for a blocked
1067
+ // action: we WANT the tap so we can say why. Only a
1068
+ // genuinely busy modal blocks the press.
1069
+ disabled={busy && reloadingId !== reloadAction.id} busy={reloadingId === reloadAction.id}/>
1070
+ <react_native_1.Text style={styles.reloadHint}>
1071
+ {reloadAction.enabled
1072
+ ? reloadAction.hint
1073
+ : reloadAction.disabledReason}
1074
+ </react_native_1.Text>
1075
+ </react_native_1.View>))}
721
1076
 
722
1077
  {/* 3. Vibing — expands to an input box on first tap
723
1078
  so the user says WHAT they want to vibe on, just
@@ -755,29 +1110,6 @@ const FeedbackModal = () => {
755
1110
  platform smarts here. */}
756
1111
  {!showDeploy ? (<ActionRow label="Deploy" tint="#7f8cf7" onPress={() => setShowDeploy(true)} disabled={busy}/>) : (<DeployPanel_1.DeployPanel onClose={() => setShowDeploy(false)}/>)}
757
1112
 
758
- {/* Remote sign-in buttons — trigger codex/claude device-auth
759
- on the selected agent without leaving the app. Opens a
760
- small native modal showing the verification URL + 8-char
761
- code the user enters in any browser. No API keys. */}
762
- <react_native_1.View style={runnerAuthRowStyles.container}>
763
- <react_native_1.Pressable onPress={() => setRunnerAuthModal('codex')} disabled={busy} style={({ pressed }) => [
764
- runnerAuthRowStyles.button,
765
- pressed && runnerAuthRowStyles.buttonPressed,
766
- busy && runnerAuthRowStyles.buttonDisabled,
767
- ]} accessibilityRole="button" accessibilityLabel="Remote sign-in Codex">
768
- <react_native_1.Text style={runnerAuthRowStyles.buttonLabel}>Remote sign-in</react_native_1.Text>
769
- <react_native_1.Text style={runnerAuthRowStyles.buttonName}>Codex</react_native_1.Text>
770
- </react_native_1.Pressable>
771
- <react_native_1.Pressable onPress={() => setRunnerAuthModal('claude')} disabled={busy} style={({ pressed }) => [
772
- runnerAuthRowStyles.button,
773
- pressed && runnerAuthRowStyles.buttonPressed,
774
- busy && runnerAuthRowStyles.buttonDisabled,
775
- ]} accessibilityRole="button" accessibilityLabel="Remote sign-in Claude">
776
- <react_native_1.Text style={runnerAuthRowStyles.buttonLabel}>Remote sign-in</react_native_1.Text>
777
- <react_native_1.Text style={runnerAuthRowStyles.buttonName}>Claude</react_native_1.Text>
778
- </react_native_1.Pressable>
779
- </react_native_1.View>
780
-
781
1113
  {progress !== null && (<react_native_1.View style={styles.progressTrack}>
782
1114
  <react_native_1.View style={[
783
1115
  styles.progressFill,
@@ -798,7 +1130,10 @@ const FeedbackModal = () => {
798
1130
  </react_native_1.KeyboardAvoidingView>
799
1131
  </react_native_1.Pressable>
800
1132
  </react_native_1.Modal>)}
801
- {runnerAuthModal ? (<RunnerAuthNativeModal runner={runnerAuthModal} onClose={() => setRunnerAuthModal(null)}/>) : null}
1133
+ {runnerAuthModal ? (<RunnerAuthNativeModal runner={runnerAuthModal} onClose={() => {
1134
+ setRunnerAuthModal(null);
1135
+ void loadRunnerStatuses();
1136
+ }}/>) : null}
802
1137
  </>);
803
1138
  };
804
1139
  exports.FeedbackModal = FeedbackModal;
@@ -814,6 +1149,15 @@ const ActionRow = ({ label, tint, onPress, disabled, busy, }) => (<react_native_
814
1149
  {busy ? (<react_native_1.ActivityIndicator color={tint} size="small"/>) : (<react_native_1.Text style={[styles.actionText, { color: tint }]}>{label}</react_native_1.Text>)}
815
1150
  </react_native_1.Pressable>);
816
1151
  const styles = react_native_1.StyleSheet.create({
1152
+ reloadRow: {
1153
+ gap: 4,
1154
+ },
1155
+ reloadHint: {
1156
+ color: '#8b8b93',
1157
+ fontSize: 11,
1158
+ lineHeight: 15,
1159
+ paddingHorizontal: 4,
1160
+ },
817
1161
  vibeInputRow: {
818
1162
  backgroundColor: 'rgba(129,140,248,0.08)',
819
1163
  borderColor: 'rgba(129,140,248,0.4)',
@@ -996,6 +1340,106 @@ const styles = react_native_1.StyleSheet.create({
996
1340
  marginTop: 4,
997
1341
  lineHeight: 17,
998
1342
  },
1343
+ runnerSection: {
1344
+ marginTop: 2,
1345
+ gap: 10,
1346
+ },
1347
+ runnerSectionHeader: {
1348
+ flexDirection: 'row',
1349
+ alignItems: 'center',
1350
+ gap: 10,
1351
+ },
1352
+ runnerSectionTitle: {
1353
+ color: '#f8fafc',
1354
+ fontSize: 16,
1355
+ fontWeight: '700',
1356
+ },
1357
+ runnerSectionSubtitle: {
1358
+ marginTop: 2,
1359
+ color: '#94a3b8',
1360
+ fontSize: 12,
1361
+ },
1362
+ runnerRefreshBtn: {
1363
+ borderRadius: 10,
1364
+ borderWidth: 1,
1365
+ borderColor: 'rgba(148,163,184,0.22)',
1366
+ backgroundColor: 'rgba(15,23,42,0.65)',
1367
+ paddingHorizontal: 10,
1368
+ paddingVertical: 8,
1369
+ },
1370
+ runnerRefreshBtnText: {
1371
+ color: '#cbd5e1',
1372
+ fontSize: 12,
1373
+ fontWeight: '600',
1374
+ },
1375
+ runnerCard: {
1376
+ borderRadius: 12,
1377
+ borderWidth: 1,
1378
+ borderColor: 'rgba(148,163,184,0.14)',
1379
+ backgroundColor: 'rgba(15,23,42,0.45)',
1380
+ paddingHorizontal: 12,
1381
+ paddingVertical: 11,
1382
+ gap: 6,
1383
+ },
1384
+ runnerCardOk: {
1385
+ borderColor: 'rgba(34,197,94,0.28)',
1386
+ backgroundColor: 'rgba(20,83,45,0.20)',
1387
+ },
1388
+ runnerCardWarning: {
1389
+ borderColor: 'rgba(251,191,36,0.28)',
1390
+ backgroundColor: 'rgba(120,53,15,0.18)',
1391
+ },
1392
+ runnerCardError: {
1393
+ borderColor: 'rgba(248,113,113,0.28)',
1394
+ backgroundColor: 'rgba(127,29,29,0.18)',
1395
+ },
1396
+ runnerCardTop: {
1397
+ flexDirection: 'row',
1398
+ alignItems: 'center',
1399
+ gap: 10,
1400
+ },
1401
+ runnerCardTitle: {
1402
+ color: '#f8fafc',
1403
+ fontSize: 14,
1404
+ fontWeight: '700',
1405
+ },
1406
+ runnerCardStatus: {
1407
+ marginTop: 2,
1408
+ fontSize: 12,
1409
+ color: '#cbd5e1',
1410
+ },
1411
+ runnerCardStatusOk: {
1412
+ color: '#86efac',
1413
+ },
1414
+ runnerCardStatusWarning: {
1415
+ color: '#fcd34d',
1416
+ },
1417
+ runnerCardStatusError: {
1418
+ color: '#fca5a5',
1419
+ },
1420
+ runnerCardDetail: {
1421
+ color: '#94a3b8',
1422
+ fontSize: 11,
1423
+ lineHeight: 16,
1424
+ },
1425
+ runnerActionBtn: {
1426
+ borderRadius: 10,
1427
+ borderWidth: 1,
1428
+ borderColor: 'rgba(129,140,248,0.35)',
1429
+ backgroundColor: 'rgba(67,56,202,0.22)',
1430
+ paddingHorizontal: 12,
1431
+ paddingVertical: 8,
1432
+ },
1433
+ runnerActionBtnText: {
1434
+ color: '#c7d2fe',
1435
+ fontSize: 12,
1436
+ fontWeight: '700',
1437
+ },
1438
+ runnerSectionError: {
1439
+ color: '#fca5a5',
1440
+ fontSize: 12,
1441
+ lineHeight: 18,
1442
+ },
999
1443
  captureChoices: {
1000
1444
  gap: 10,
1001
1445
  },
@@ -1328,39 +1772,6 @@ const RunnerAuthNativeModal = ({ runner, onClose }) => {
1328
1772
  </react_native_1.View>
1329
1773
  </react_native_1.Modal>);
1330
1774
  };
1331
- const runnerAuthRowStyles = react_native_1.StyleSheet.create({
1332
- container: {
1333
- flexDirection: 'row',
1334
- gap: 8,
1335
- marginTop: 8,
1336
- flexWrap: 'wrap',
1337
- },
1338
- button: {
1339
- flexGrow: 1,
1340
- flexBasis: 0,
1341
- minWidth: 120,
1342
- paddingHorizontal: 12,
1343
- paddingVertical: 10,
1344
- borderRadius: 10,
1345
- borderWidth: 1,
1346
- borderColor: 'rgba(148,163,184,0.22)',
1347
- backgroundColor: 'rgba(15,23,42,0.6)',
1348
- },
1349
- buttonPressed: { opacity: 0.7 },
1350
- buttonDisabled: { opacity: 0.4 },
1351
- buttonLabel: {
1352
- fontSize: 10,
1353
- color: '#94a3b8',
1354
- textTransform: 'uppercase',
1355
- letterSpacing: 0.8,
1356
- },
1357
- buttonName: {
1358
- marginTop: 2,
1359
- fontSize: 14,
1360
- fontWeight: '600',
1361
- color: '#f1f5f9',
1362
- },
1363
- });
1364
1775
  const runnerAuthModalStyles = react_native_1.StyleSheet.create({
1365
1776
  overlay: {
1366
1777
  flex: 1,