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
@@ -25,15 +25,28 @@ import {
25
25
  // stopVideoRecording,
26
26
  } from './capture';
27
27
  import { uploadFeedback } from './upload';
28
- import { DeviceInfo, FeedbackBundle } from './types';
28
+ import { resolveReportIdentity } from './P2PClient';
29
+ import {
30
+ DeviceInfo,
31
+ FeedbackBundle,
32
+ OpenCodeConfigSummary,
33
+ OpenCodeProviderSummary,
34
+ RunnerAuthStatusRow,
35
+ } from './types';
29
36
  import { AuthOverlay } from './AuthOverlay';
30
37
  import { QuickActionIcon } from './QuickActionIcon';
31
38
  import { VibeChatScreen } from './VibeChatScreen';
32
39
  import { DeployPanel } from './DeployPanel';
33
40
  import { listReachableDevices, RemoteDevice } from './auth';
41
+ import { reloadActions } from './reloadActions';
42
+ import type { DevServerSnapshot, ReloadAction } from './reloadActions';
34
43
  import {
35
44
  QUICK_ICON_COLOR_PRESETS,
36
45
  QuickIconColorPreset,
46
+ getPreferredModel,
47
+ getPreferredRunner,
48
+ setPreferredModel,
49
+ setPreferredRunner,
37
50
  } from './preferences';
38
51
 
39
52
  /**
@@ -64,6 +77,111 @@ type MachineCardState = {
64
77
  detail: string;
65
78
  };
66
79
 
80
+ type RunnerTone = 'ok' | 'warning' | 'error' | 'neutral';
81
+
82
+ type RunnerCardState = {
83
+ id: string;
84
+ name: string;
85
+ installed: boolean;
86
+ authConfigured: boolean;
87
+ ready: boolean;
88
+ version?: string;
89
+ tone: RunnerTone;
90
+ statusLine: string;
91
+ detail?: string;
92
+ actionLabel?: string;
93
+ actionRunner?: string;
94
+ };
95
+
96
+ type ProviderEditorState = {
97
+ mode: 'add' | 'edit';
98
+ id: string;
99
+ name: string;
100
+ baseUrl: string;
101
+ apiKey: string;
102
+ };
103
+
104
+ const PRIMARY_RUNNER_IDS = ['claude', 'codex', 'opencode'] as const;
105
+
106
+ function normalizeRunnerStatusRows(rows: RunnerAuthStatusRow[]): RunnerCardState[] {
107
+ const byId = new Map<string, RunnerAuthStatusRow>();
108
+ for (const row of rows) {
109
+ const raw = String(row.id || '').trim().toLowerCase();
110
+ if (!raw) continue;
111
+ const normalized = raw === 'claude-code' ? 'claude' : raw;
112
+ if (!PRIMARY_RUNNER_IDS.includes(normalized as (typeof PRIMARY_RUNNER_IDS)[number])) continue;
113
+ byId.set(normalized, { ...row, id: normalized });
114
+ }
115
+
116
+ return PRIMARY_RUNNER_IDS.map((id) => {
117
+ const baseName =
118
+ id === 'claude' ? 'Claude Code' : id === 'codex' ? 'OpenAI Codex' : 'OpenCode';
119
+ const row = byId.get(id);
120
+ if (!row) {
121
+ return {
122
+ id,
123
+ name: baseName,
124
+ installed: false,
125
+ authConfigured: false,
126
+ ready: false,
127
+ tone: 'warning',
128
+ statusLine: 'Not installed on the selected machine',
129
+ };
130
+ }
131
+
132
+ const versionPrefix = row.version?.trim() ? `${row.version.trim()} · ` : '';
133
+ const detail = row.error?.trim() || row.warning?.trim() || row.detail?.trim() || undefined;
134
+
135
+ if (!row.installed) {
136
+ return {
137
+ id,
138
+ name: row.name || baseName,
139
+ installed: false,
140
+ authConfigured: false,
141
+ ready: false,
142
+ version: row.version,
143
+ tone: 'warning',
144
+ statusLine: 'Not installed on the selected machine',
145
+ detail,
146
+ };
147
+ }
148
+
149
+ if (id === 'opencode') {
150
+ const configured = row.authConfigured || row.ready;
151
+ return {
152
+ id,
153
+ name: row.name || baseName,
154
+ installed: row.installed,
155
+ authConfigured: row.authConfigured,
156
+ ready: row.ready,
157
+ version: row.version,
158
+ tone: configured ? 'ok' : 'warning',
159
+ statusLine: configured
160
+ ? `${versionPrefix}Configured on the selected machine`
161
+ : `${versionPrefix}Needs provider config on the selected machine`,
162
+ detail,
163
+ };
164
+ }
165
+
166
+ const authed = row.authConfigured || row.ready;
167
+ return {
168
+ id,
169
+ name: row.name || baseName,
170
+ installed: row.installed,
171
+ authConfigured: row.authConfigured,
172
+ ready: row.ready,
173
+ version: row.version,
174
+ tone: authed ? 'ok' : 'warning',
175
+ statusLine: authed
176
+ ? `${versionPrefix}Signed in on the selected machine`
177
+ : `${versionPrefix}Not signed in on the selected machine`,
178
+ detail,
179
+ actionLabel: authed ? 'Re-auth' : 'Sign in',
180
+ actionRunner: id,
181
+ };
182
+ });
183
+ }
184
+
67
185
  export const FeedbackModal: React.FC = () => {
68
186
  const { width: winW, height: winH } = useWindowDimensions();
69
187
  const isTablet = Math.min(winW, winH) >= 600;
@@ -80,6 +198,11 @@ export const FeedbackModal: React.FC = () => {
80
198
  // is our guaranteed UI for bringing the icon back — we surface a
81
199
  // small "Show quick icon" row when this is true.
82
200
  const [quickIconHidden, setQuickIconHidden] = useState(false);
201
+ // Dev-server snapshot behind the reload buttons. `null` means "we have not
202
+ // been able to ask" — rendered as "not connected", never as "no dev server".
203
+ const [devSnapshot, setDevSnapshot] = useState<DevServerSnapshot | null>(null);
204
+ // Which reload action is in flight, so only that button spins.
205
+ const [reloadingId, setReloadingId] = useState<string | null>(null);
83
206
  const [runnerAuthModal, setRunnerAuthModal] = useState<string | null>(null);
84
207
  // Vibing-input mode: same expand-on-tap pattern as email login.
85
208
  // Tap "Vibing" once → the button reveals an input + Send; that lets
@@ -101,8 +224,38 @@ export const FeedbackModal: React.FC = () => {
101
224
  title: 'No machine selected',
102
225
  detail: 'Pick a remote dev machine before using the feedback actions.',
103
226
  });
227
+ const [runnerCards, setRunnerCards] = useState<RunnerCardState[]>(() =>
228
+ normalizeRunnerStatusRows([]),
229
+ );
230
+ const [runnerStatusLoading, setRunnerStatusLoading] = useState(false);
231
+ const [runnerStatusError, setRunnerStatusError] = useState<string | null>(null);
232
+ const [preferredRunner, setPreferredRunnerState] = useState<string | null>(null);
233
+ const [preferredModel, setPreferredModelState] = useState('');
234
+ const [showOpenCodeConfig, setShowOpenCodeConfig] = useState(false);
104
235
  const mountedRef = useRef(true);
105
236
 
237
+ /**
238
+ * Ask the machine what its dev server is doing, so the reload actions can
239
+ * be enabled/disabled against reality rather than against a guess.
240
+ *
241
+ * Best-effort and deliberately null-on-failure: null means "we could not
242
+ * ask", which the seam renders as "not connected to a machine yet" — a
243
+ * different sentence from "no dev server is running", because they have
244
+ * different fixes.
245
+ */
246
+ const refreshDevSnapshot = useCallback(async () => {
247
+ try {
248
+ const client = YaverFeedback.getP2PClient();
249
+ if (!client) {
250
+ setDevSnapshot(null);
251
+ return;
252
+ }
253
+ setDevSnapshot(await client.getDevServerStatus());
254
+ } catch {
255
+ setDevSnapshot(null);
256
+ }
257
+ }, []);
258
+
106
259
  const loadSelectedMachine = useCallback(async () => {
107
260
  const cfg = YaverFeedback.getConfig();
108
261
  if (!cfg?.authToken) {
@@ -204,6 +357,69 @@ export const FeedbackModal: React.FC = () => {
204
357
  }
205
358
  }, []);
206
359
 
360
+ const loadRunnerStatuses = useCallback(async () => {
361
+ const cfg = YaverFeedback.getConfig();
362
+ if (!cfg?.authToken) {
363
+ if (mountedRef.current) {
364
+ setRunnerCards(normalizeRunnerStatusRows([]));
365
+ setRunnerStatusError('Sign in to inspect coding-agent status.');
366
+ setRunnerStatusLoading(false);
367
+ }
368
+ return;
369
+ }
370
+ if (!cfg.preferredDeviceId) {
371
+ if (mountedRef.current) {
372
+ setRunnerCards(normalizeRunnerStatusRows([]));
373
+ setRunnerStatusError('Pick a machine to inspect coding-agent status.');
374
+ setRunnerStatusLoading(false);
375
+ }
376
+ return;
377
+ }
378
+
379
+ if (mountedRef.current) {
380
+ setRunnerStatusLoading(true);
381
+ setRunnerStatusError(null);
382
+ }
383
+
384
+ try {
385
+ let client = YaverFeedback.getP2PClient();
386
+ if (!client) {
387
+ const ok = await YaverFeedback.reconnect();
388
+ if (ok) client = YaverFeedback.getP2PClient();
389
+ }
390
+ if (!client) {
391
+ throw new Error('Not connected to the selected machine yet.');
392
+ }
393
+ const rows = await client.getRunnerAuthStatus();
394
+ if (mountedRef.current) {
395
+ setRunnerCards(normalizeRunnerStatusRows(rows));
396
+ }
397
+ } catch (err) {
398
+ if (mountedRef.current) {
399
+ setRunnerCards(normalizeRunnerStatusRows([]));
400
+ setRunnerStatusError(err instanceof Error ? err.message : String(err));
401
+ }
402
+ } finally {
403
+ if (mountedRef.current) setRunnerStatusLoading(false);
404
+ }
405
+ }, []);
406
+
407
+ const loadRoutingPrefs = useCallback(async () => {
408
+ try {
409
+ const [runner, model] = await Promise.all([
410
+ getPreferredRunner(),
411
+ getPreferredModel(),
412
+ ]);
413
+ if (!mountedRef.current) return;
414
+ setPreferredRunnerState(runner);
415
+ setPreferredModelState(model ?? '');
416
+ } catch {
417
+ if (!mountedRef.current) return;
418
+ setPreferredRunnerState(null);
419
+ setPreferredModelState('');
420
+ }
421
+ }, []);
422
+
207
423
  useEffect(() => {
208
424
  mountedRef.current = true;
209
425
  const sub = DeviceEventEmitter.addListener('yaverFeedback:startReport', () => {
@@ -229,6 +445,7 @@ export const FeedbackModal: React.FC = () => {
229
445
  })
230
446
  .catch(() => {});
231
447
  void loadSelectedMachine();
448
+ void loadRunnerStatuses();
232
449
  }
233
450
  });
234
451
  // Agent streams build / compile progress through the BlackBox
@@ -257,15 +474,22 @@ export const FeedbackModal: React.FC = () => {
257
474
  sub.remove();
258
475
  statusSub.remove();
259
476
  };
260
- }, [loadSelectedMachine]);
477
+ }, [loadRunnerStatuses, loadSelectedMachine]);
261
478
 
262
479
  useEffect(() => {
263
480
  if (!visible) return;
481
+ // Poll the dev server alongside the machine + runners. A reload button
482
+ // whose enabled state was decided once, when the sheet opened, goes
483
+ // stale the moment the user starts Metro from another surface — and a
484
+ // stale "no dev server is running" reads as the product being broken.
485
+ void refreshDevSnapshot();
264
486
  const interval = setInterval(() => {
265
487
  void loadSelectedMachine();
488
+ void loadRunnerStatuses();
489
+ void refreshDevSnapshot();
266
490
  }, 5000);
267
491
  return () => clearInterval(interval);
268
- }, [loadSelectedMachine, visible]);
492
+ }, [loadRunnerStatuses, loadSelectedMachine, refreshDevSnapshot, visible]);
269
493
 
270
494
  useEffect(() => {
271
495
  if (!visible) {
@@ -301,6 +525,7 @@ export const FeedbackModal: React.FC = () => {
301
525
  setAction('idle');
302
526
  setShowVibeInput(false);
303
527
  setVibePrompt('');
528
+ setRunnerStatusError(null);
304
529
  }, []);
305
530
 
306
531
  // Helper: run a P2P call; on network failure, ask YaverFeedback to
@@ -353,7 +578,87 @@ export const FeedbackModal: React.FC = () => {
353
578
  [],
354
579
  );
355
580
 
356
- // ─── 1. Hot reload ─────────────────────────────────────────────────
581
+ // ─── 1. Reload ─────────────────────────────────────────────────────
582
+ //
583
+ // Three actions now, not one: Hot Reload (mode=fast), Full Reload
584
+ // (mode=full — Flutter's hot RESTART), and Rebuild Bundle
585
+ // (/dev/reload-app, the only one that works with Metro down).
586
+ //
587
+ // WHICH of them render, and which are disabled with what reason, is
588
+ // decided by the pure `reloadActions()` seam — never inline here, so the
589
+ // same policy holds on web, Flutter, Unity, Swift and Kotlin. In
590
+ // particular: a production build (`__DEV__ === false`) gets NONE of them.
591
+
592
+ const availableReloadActions = reloadActions(devSnapshot, {
593
+ // __DEV__ is React Native's own build flag. A release bundle sets it
594
+ // false, so a shipped app renders no reload UI at all — which is the
595
+ // point, and is what reloadActions.test.ts pins.
596
+ isDevBuild: typeof __DEV__ !== 'undefined' && __DEV__ === true,
597
+ connected: devSnapshot !== null,
598
+ machineLabel: machineCard.device?.name || undefined,
599
+ includeRebuild: true,
600
+ });
601
+
602
+ const handleReloadAction = useCallback(
603
+ async (reloadAction: ReloadAction) => {
604
+ if (!reloadAction.enabled) {
605
+ // Pressing a disabled action must SAY why. A row that does nothing
606
+ // is the same defect as a spinner that never resolves.
607
+ setToast(reloadAction.disabledReason || 'Reload is unavailable right now.');
608
+ setError(reloadAction.disabledReason || null);
609
+ return;
610
+ }
611
+ setReloadingId(reloadAction.id);
612
+ setAction('hot-reloading');
613
+ setError(null);
614
+ setProgress(0);
615
+ setToast(`${reloadAction.label}…`);
616
+ try {
617
+ await loadSelectedMachine();
618
+ const selected = await YaverFeedback.getSelectedRemoteDevice();
619
+ if (!selected) {
620
+ YaverFeedback.showMachinePicker();
621
+ throw new Error('No machine selected. Pick a machine and try again.');
622
+ }
623
+ if (selected.needsAuth) {
624
+ YaverFeedback.showMachinePicker();
625
+ throw new Error('Selected machine needs pairing again.');
626
+ }
627
+ if (!selected.isOnline) {
628
+ throw new Error('Selected machine is offline. Start `yaver serve` on it first.');
629
+ }
630
+
631
+ let ackMessage = `${reloadAction.label} requested.`;
632
+ await runWithReconnect(async (client) => {
633
+ const ack = await client.reloadWithMode(reloadAction.mode, devSnapshot);
634
+ ackMessage = ack.message;
635
+ setToast(ack.message);
636
+ setProgress(0.2);
637
+ });
638
+ setToast(ackMessage);
639
+ if (reloadAction.mode === 'bundle') closeSoon(2500);
640
+ } catch (err: unknown) {
641
+ const message = err instanceof Error ? err.message : String(err);
642
+ setError(message);
643
+ setToast(
644
+ message.toLowerCase().indexOf('session expired') >= 0
645
+ ? 'Session expired. Sign in again.'
646
+ : message,
647
+ );
648
+ await loadSelectedMachine();
649
+ setProgress(null);
650
+ } finally {
651
+ if (mountedRef.current) {
652
+ setAction('idle');
653
+ setReloadingId(null);
654
+ void refreshDevSnapshot();
655
+ }
656
+ }
657
+ },
658
+ [closeSoon, devSnapshot, loadSelectedMachine, refreshDevSnapshot, runWithReconnect],
659
+ );
660
+
661
+ /** Kept for the legacy one-tap path (BlackBox command, chat Reload button). */
357
662
  const handleHotReload = useCallback(async () => {
358
663
  setAction('hot-reloading');
359
664
  setError(null);
@@ -472,19 +777,36 @@ export const FeedbackModal: React.FC = () => {
472
777
  try {
473
778
  const { Dimensions } = require('react-native');
474
779
  const { width, height } = Dimensions.get('window');
780
+ const cfg = YaverFeedback.getConfig();
781
+ const identity = resolveReportIdentity({
782
+ projectName: cfg?.projectName,
783
+ bundleId: cfg?.bundleId,
784
+ surface: cfg?.surface,
785
+ surfaces: cfg?.surfaces,
786
+ stack: cfg?.stack,
787
+ stacks: cfg?.stacks,
788
+ testSurfaces: cfg?.testSurfaces,
789
+ feedbackSdk: cfg?.feedbackSdk,
790
+ feedbackTransport: cfg?.feedbackTransport,
791
+ voiceCapabilities: cfg?.voiceCapabilities,
792
+ sttProvider: cfg?.sttProvider,
793
+ ttsProvider: cfg?.ttsProvider,
794
+ });
475
795
  const deviceInfo: DeviceInfo = {
476
796
  platform: Platform.OS,
477
797
  osVersion: String(Platform.Version),
478
798
  model: Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
479
799
  screenWidth: width,
480
800
  screenHeight: height,
801
+ appName: identity.appName,
481
802
  };
482
803
  const capturedErrors = YaverFeedback.getCapturedErrors();
483
804
  const bundle: FeedbackBundle = {
484
805
  metadata: {
485
806
  timestamp: new Date().toISOString(),
486
- device: deviceInfo,
487
- app: {},
807
+ deviceInfo,
808
+ app: identity.app,
809
+ project: identity.project,
488
810
  userNote: '[Screenshot + Fix]',
489
811
  },
490
812
  screenshots: [path],
@@ -552,6 +874,9 @@ export const FeedbackModal: React.FC = () => {
552
874
  const [activeVibe, setActiveVibe] = useState<{
553
875
  taskId: string;
554
876
  initialPrompt: string;
877
+ project?: string;
878
+ runner?: string;
879
+ model?: string;
555
880
  } | null>(null);
556
881
  const [includeScreenshot, setIncludeScreenshot] = useState<boolean>(true);
557
882
 
@@ -614,7 +939,13 @@ export const FeedbackModal: React.FC = () => {
614
939
  setLastVibeTaskId(result.taskId);
615
940
  // Hand off to VibeChatScreen — it streams the SSE transcript,
616
941
  // accepts follow-ups, and surfaces a Reload button.
617
- setActiveVibe({ taskId: result.taskId, initialPrompt: promptText });
942
+ setActiveVibe({
943
+ taskId: result.taskId,
944
+ initialPrompt: promptText,
945
+ project: identity.projectName,
946
+ runner: preferredRunner ?? undefined,
947
+ model: preferredModel ?? undefined,
948
+ });
618
949
  setVibePrompt('');
619
950
  setShowVibeInput(false);
620
951
  } catch (err: unknown) {
@@ -631,6 +962,11 @@ export const FeedbackModal: React.FC = () => {
631
962
  */
632
963
 
633
964
  const busy = action !== 'idle';
965
+ const readyRunnerCount = runnerCards.filter((row) => row.ready || row.authConfigured).length;
966
+ const missingRunnerCount = runnerCards.filter((row) => !row.installed).length;
967
+ const needsAuthRunnerCount = runnerCards.filter(
968
+ (row) => row.installed && !row.authConfigured && !row.ready,
969
+ ).length;
634
970
 
635
971
  // Once the user fires off a vibe task, swap the entire modal body
636
972
  // for the live chat screen. The chat manages its own SSE
@@ -653,6 +989,9 @@ export const FeedbackModal: React.FC = () => {
653
989
  client={client}
654
990
  initialTaskId={activeVibe.taskId}
655
991
  initialUserPrompt={activeVibe.initialPrompt}
992
+ project={activeVibe.project}
993
+ runner={activeVibe.runner}
994
+ model={activeVibe.model}
656
995
  onClose={() => setActiveVibe(null)}
657
996
  onReload={async () => {
658
997
  const c = YaverFeedback.getP2PClient();
@@ -766,6 +1105,80 @@ export const FeedbackModal: React.FC = () => {
766
1105
  <Text style={styles.machineMeta}>{machineCard.detail}</Text>
767
1106
  </Pressable>
768
1107
 
1108
+ <View style={styles.runnerSection}>
1109
+ <View style={styles.runnerSectionHeader}>
1110
+ <View style={{ flex: 1 }}>
1111
+ <Text style={styles.runnerSectionTitle}>Coding Agents</Text>
1112
+ <Text style={styles.runnerSectionSubtitle}>
1113
+ {runnerStatusLoading
1114
+ ? 'Refreshing runner status on the selected machine…'
1115
+ : `${readyRunnerCount} ready · ${needsAuthRunnerCount} need sign-in · ${missingRunnerCount} missing`}
1116
+ </Text>
1117
+ </View>
1118
+ <Pressable
1119
+ onPress={() => void loadRunnerStatuses()}
1120
+ style={({ pressed }) => [
1121
+ styles.runnerRefreshBtn,
1122
+ pressed && styles.buttonPressed,
1123
+ ]}
1124
+ accessibilityRole="button"
1125
+ accessibilityLabel="Refresh coding-agent status"
1126
+ >
1127
+ <Text style={styles.runnerRefreshBtnText}>
1128
+ {runnerStatusLoading ? 'Refreshing…' : 'Refresh'}
1129
+ </Text>
1130
+ </Pressable>
1131
+ </View>
1132
+
1133
+ {runnerCards.map((row) => (
1134
+ <View
1135
+ key={row.id}
1136
+ style={[
1137
+ styles.runnerCard,
1138
+ row.tone === 'ok' && styles.runnerCardOk,
1139
+ row.tone === 'warning' && styles.runnerCardWarning,
1140
+ row.tone === 'error' && styles.runnerCardError,
1141
+ ]}
1142
+ >
1143
+ <View style={styles.runnerCardTop}>
1144
+ <View style={{ flex: 1 }}>
1145
+ <Text style={styles.runnerCardTitle}>{row.name}</Text>
1146
+ <Text
1147
+ style={[
1148
+ styles.runnerCardStatus,
1149
+ row.tone === 'ok' && styles.runnerCardStatusOk,
1150
+ row.tone === 'warning' && styles.runnerCardStatusWarning,
1151
+ row.tone === 'error' && styles.runnerCardStatusError,
1152
+ ]}
1153
+ >
1154
+ {row.statusLine}
1155
+ </Text>
1156
+ </View>
1157
+ {row.actionRunner ? (
1158
+ <Pressable
1159
+ onPress={() => setRunnerAuthModal(row.actionRunner ?? null)}
1160
+ style={({ pressed }) => [
1161
+ styles.runnerActionBtn,
1162
+ pressed && styles.buttonPressed,
1163
+ ]}
1164
+ accessibilityRole="button"
1165
+ accessibilityLabel={`${row.actionLabel} ${row.name}`}
1166
+ >
1167
+ <Text style={styles.runnerActionBtnText}>{row.actionLabel}</Text>
1168
+ </Pressable>
1169
+ ) : null}
1170
+ </View>
1171
+ {row.detail ? (
1172
+ <Text style={styles.runnerCardDetail}>{row.detail}</Text>
1173
+ ) : null}
1174
+ </View>
1175
+ ))}
1176
+
1177
+ {runnerStatusError ? (
1178
+ <Text style={styles.runnerSectionError}>{runnerStatusError}</Text>
1179
+ ) : null}
1180
+ </View>
1181
+
769
1182
  {quickIconHidden && (
770
1183
  <View style={styles.quickIconNote}>
771
1184
  <Text style={styles.quickIconNoteText}>
@@ -835,16 +1248,36 @@ export const FeedbackModal: React.FC = () => {
835
1248
  </View>
836
1249
  </View>
837
1250
 
838
- {/* 1. Hot Reload — the common path */}
839
- <ActionRow
840
- label={
841
- action === 'hot-reloading' ? 'Reloading…' : 'Hot Reload'
842
- }
843
- tint="#fbbf24"
844
- onPress={handleHotReload}
845
- disabled={busy}
846
- busy={action === 'hot-reloading'}
847
- />
1251
+ {/* 1. Reload — Hot / Full / Rebuild Bundle.
1252
+ Rendered from the shared decision seam, so a production
1253
+ build (__DEV__ false) renders nothing here at all, and a
1254
+ blocked action shows greyed WITH its reason underneath
1255
+ rather than vanishing. */}
1256
+ {availableReloadActions.map((reloadAction) => (
1257
+ <View key={reloadAction.id} style={styles.reloadRow}>
1258
+ <ActionRow
1259
+ label={
1260
+ reloadingId === reloadAction.id
1261
+ ? `${reloadAction.label}…`
1262
+ : reloadAction.label
1263
+ }
1264
+ tint={reloadAction.id === 'rebuild' ? '#38bdf8' : '#fbbf24'}
1265
+ onPress={() => {
1266
+ void handleReloadAction(reloadAction);
1267
+ }}
1268
+ // Never `disabled` at the Pressable level for a blocked
1269
+ // action: we WANT the tap so we can say why. Only a
1270
+ // genuinely busy modal blocks the press.
1271
+ disabled={busy && reloadingId !== reloadAction.id}
1272
+ busy={reloadingId === reloadAction.id}
1273
+ />
1274
+ <Text style={styles.reloadHint}>
1275
+ {reloadAction.enabled
1276
+ ? reloadAction.hint
1277
+ : reloadAction.disabledReason}
1278
+ </Text>
1279
+ </View>
1280
+ ))}
848
1281
 
849
1282
  {/* 3. Vibing — expands to an input box on first tap
850
1283
  so the user says WHAT they want to vibe on, just
@@ -931,41 +1364,6 @@ export const FeedbackModal: React.FC = () => {
931
1364
  <DeployPanel onClose={() => setShowDeploy(false)} />
932
1365
  )}
933
1366
 
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
1367
  {progress !== null && (
970
1368
  <View style={styles.progressTrack}>
971
1369
  <View
@@ -999,7 +1397,10 @@ export const FeedbackModal: React.FC = () => {
999
1397
  {runnerAuthModal ? (
1000
1398
  <RunnerAuthNativeModal
1001
1399
  runner={runnerAuthModal}
1002
- onClose={() => setRunnerAuthModal(null)}
1400
+ onClose={() => {
1401
+ setRunnerAuthModal(null);
1402
+ void loadRunnerStatuses();
1403
+ }}
1003
1404
  />
1004
1405
  ) : null}
1005
1406
  </>
@@ -1045,6 +1446,15 @@ const ActionRow: React.FC<ActionRowProps> = ({
1045
1446
  );
1046
1447
 
1047
1448
  const styles = StyleSheet.create({
1449
+ reloadRow: {
1450
+ gap: 4,
1451
+ },
1452
+ reloadHint: {
1453
+ color: '#8b8b93',
1454
+ fontSize: 11,
1455
+ lineHeight: 15,
1456
+ paddingHorizontal: 4,
1457
+ },
1048
1458
  vibeInputRow: {
1049
1459
  backgroundColor: 'rgba(129,140,248,0.08)',
1050
1460
  borderColor: 'rgba(129,140,248,0.4)',
@@ -1227,6 +1637,106 @@ const styles = StyleSheet.create({
1227
1637
  marginTop: 4,
1228
1638
  lineHeight: 17,
1229
1639
  },
1640
+ runnerSection: {
1641
+ marginTop: 2,
1642
+ gap: 10,
1643
+ },
1644
+ runnerSectionHeader: {
1645
+ flexDirection: 'row',
1646
+ alignItems: 'center',
1647
+ gap: 10,
1648
+ },
1649
+ runnerSectionTitle: {
1650
+ color: '#f8fafc',
1651
+ fontSize: 16,
1652
+ fontWeight: '700',
1653
+ },
1654
+ runnerSectionSubtitle: {
1655
+ marginTop: 2,
1656
+ color: '#94a3b8',
1657
+ fontSize: 12,
1658
+ },
1659
+ runnerRefreshBtn: {
1660
+ borderRadius: 10,
1661
+ borderWidth: 1,
1662
+ borderColor: 'rgba(148,163,184,0.22)',
1663
+ backgroundColor: 'rgba(15,23,42,0.65)',
1664
+ paddingHorizontal: 10,
1665
+ paddingVertical: 8,
1666
+ },
1667
+ runnerRefreshBtnText: {
1668
+ color: '#cbd5e1',
1669
+ fontSize: 12,
1670
+ fontWeight: '600',
1671
+ },
1672
+ runnerCard: {
1673
+ borderRadius: 12,
1674
+ borderWidth: 1,
1675
+ borderColor: 'rgba(148,163,184,0.14)',
1676
+ backgroundColor: 'rgba(15,23,42,0.45)',
1677
+ paddingHorizontal: 12,
1678
+ paddingVertical: 11,
1679
+ gap: 6,
1680
+ },
1681
+ runnerCardOk: {
1682
+ borderColor: 'rgba(34,197,94,0.28)',
1683
+ backgroundColor: 'rgba(20,83,45,0.20)',
1684
+ },
1685
+ runnerCardWarning: {
1686
+ borderColor: 'rgba(251,191,36,0.28)',
1687
+ backgroundColor: 'rgba(120,53,15,0.18)',
1688
+ },
1689
+ runnerCardError: {
1690
+ borderColor: 'rgba(248,113,113,0.28)',
1691
+ backgroundColor: 'rgba(127,29,29,0.18)',
1692
+ },
1693
+ runnerCardTop: {
1694
+ flexDirection: 'row',
1695
+ alignItems: 'center',
1696
+ gap: 10,
1697
+ },
1698
+ runnerCardTitle: {
1699
+ color: '#f8fafc',
1700
+ fontSize: 14,
1701
+ fontWeight: '700',
1702
+ },
1703
+ runnerCardStatus: {
1704
+ marginTop: 2,
1705
+ fontSize: 12,
1706
+ color: '#cbd5e1',
1707
+ },
1708
+ runnerCardStatusOk: {
1709
+ color: '#86efac',
1710
+ },
1711
+ runnerCardStatusWarning: {
1712
+ color: '#fcd34d',
1713
+ },
1714
+ runnerCardStatusError: {
1715
+ color: '#fca5a5',
1716
+ },
1717
+ runnerCardDetail: {
1718
+ color: '#94a3b8',
1719
+ fontSize: 11,
1720
+ lineHeight: 16,
1721
+ },
1722
+ runnerActionBtn: {
1723
+ borderRadius: 10,
1724
+ borderWidth: 1,
1725
+ borderColor: 'rgba(129,140,248,0.35)',
1726
+ backgroundColor: 'rgba(67,56,202,0.22)',
1727
+ paddingHorizontal: 12,
1728
+ paddingVertical: 8,
1729
+ },
1730
+ runnerActionBtnText: {
1731
+ color: '#c7d2fe',
1732
+ fontSize: 12,
1733
+ fontWeight: '700',
1734
+ },
1735
+ runnerSectionError: {
1736
+ color: '#fca5a5',
1737
+ fontSize: 12,
1738
+ lineHeight: 18,
1739
+ },
1230
1740
  captureChoices: {
1231
1741
  gap: 10,
1232
1742
  },
@@ -1604,40 +2114,6 @@ const RunnerAuthNativeModal: React.FC<{
1604
2114
  );
1605
2115
  };
1606
2116
 
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
2117
  const runnerAuthModalStyles = StyleSheet.create({
1642
2118
  overlay: {
1643
2119
  flex: 1,