yaver-feedback-react-native 0.9.0 → 0.9.3

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 (144) hide show
  1. package/README.md +102 -1
  2. package/app.plugin.js +126 -9
  3. package/dist/AuthOverlay.d.ts +1 -14
  4. package/dist/AuthOverlay.js +9 -62
  5. package/dist/BlackBox.js +9 -1
  6. package/dist/DeployPanel.js +65 -0
  7. package/dist/Discovery.js +13 -8
  8. package/dist/DogfoodRuntime.d.ts +124 -0
  9. package/dist/DogfoodRuntime.js +273 -0
  10. package/dist/FeedbackModal.js +505 -65
  11. package/dist/LoginScreen.d.ts +1 -5
  12. package/dist/LoginScreen.js +2 -6
  13. package/dist/MachinePickerScreen.d.ts +1 -3
  14. package/dist/MachinePickerScreen.js +6 -18
  15. package/dist/P2PClient.d.ts +178 -2
  16. package/dist/P2PClient.js +493 -3
  17. package/dist/P2PDogfoodDriver.d.ts +12 -0
  18. package/dist/P2PDogfoodDriver.js +118 -0
  19. package/dist/PairDeviceModal.d.ts +2 -3
  20. package/dist/ShakeDetector.js +2 -0
  21. package/dist/VibeChatScreen.d.ts +11 -1
  22. package/dist/VibeChatScreen.js +200 -41
  23. package/dist/YaverFeedback.d.ts +132 -0
  24. package/dist/YaverFeedback.js +633 -65
  25. package/dist/YaverModeBadge.d.ts +22 -0
  26. package/dist/YaverModeBadge.js +219 -0
  27. package/dist/__tests__/AuthDevices.test.js +1 -48
  28. package/dist/__tests__/BlackBox.relayPassword.test.d.ts +1 -0
  29. package/dist/__tests__/BlackBox.relayPassword.test.js +105 -0
  30. package/dist/__tests__/BlackBoxAutoStart.test.d.ts +1 -0
  31. package/dist/__tests__/BlackBoxAutoStart.test.js +91 -0
  32. package/dist/__tests__/BlackBoxAutoStartColdStart.test.d.ts +1 -0
  33. package/dist/__tests__/BlackBoxAutoStartColdStart.test.js +156 -0
  34. package/dist/__tests__/BrowserLaneIcon.test.d.ts +3 -0
  35. package/dist/__tests__/BrowserLaneIcon.test.js +80 -0
  36. package/dist/__tests__/DogfoodRuntime.test.d.ts +1 -0
  37. package/dist/__tests__/DogfoodRuntime.test.js +116 -0
  38. package/dist/__tests__/P2PClient.test.js +2 -2
  39. package/dist/__tests__/P2PDogfoodDriver.test.d.ts +1 -0
  40. package/dist/__tests__/P2PDogfoodDriver.test.js +64 -0
  41. package/dist/__tests__/ReportIdentity.test.d.ts +25 -0
  42. package/dist/__tests__/ReportIdentity.test.js +180 -0
  43. package/dist/__tests__/SDKToken.test.js +1 -1
  44. package/dist/__tests__/ShakeToggle.test.d.ts +1 -0
  45. package/dist/__tests__/ShakeToggle.test.js +121 -0
  46. package/dist/__tests__/YaverFeedback.test.js +13 -3
  47. package/dist/__tests__/deviceDogfood.test.d.ts +1 -0
  48. package/dist/__tests__/deviceDogfood.test.js +86 -0
  49. package/dist/__tests__/dogfoodPolicy.test.d.ts +1 -0
  50. package/dist/__tests__/dogfoodPolicy.test.js +33 -0
  51. package/dist/__tests__/pickTargetDevice.test.d.ts +1 -0
  52. package/dist/__tests__/pickTargetDevice.test.js +89 -0
  53. package/dist/__tests__/reloadActions.test.d.ts +1 -0
  54. package/dist/__tests__/reloadActions.test.js +129 -0
  55. package/dist/__tests__/reloadActionsParity.test.d.ts +1 -0
  56. package/dist/__tests__/reloadActionsParity.test.js +42 -0
  57. package/dist/__tests__/types.test.js +5 -5
  58. package/dist/_core/ansi.d.ts +117 -0
  59. package/dist/_core/ansi.js +468 -0
  60. package/dist/_core/ansi.test.d.ts +1 -0
  61. package/dist/_core/ansi.test.js +225 -0
  62. package/dist/_core/buildFeedbackPrompt.d.ts +4 -9
  63. package/dist/_core/buildFeedbackPrompt.js +18 -72
  64. package/dist/_core/constants.d.ts +19 -6
  65. package/dist/_core/constants.js +20 -7
  66. package/dist/_core/device.d.ts +1 -5
  67. package/dist/_core/device.js +5 -14
  68. package/dist/_core/endpoints.d.ts +0 -7
  69. package/dist/_core/endpoints.js +0 -7
  70. package/dist/_core/index.d.ts +4 -0
  71. package/dist/_core/index.js +4 -0
  72. package/dist/_core/remoteless.d.ts +44 -0
  73. package/dist/_core/remoteless.js +75 -0
  74. package/dist/_core/trace.d.ts +47 -0
  75. package/dist/_core/trace.js +38 -0
  76. package/dist/_core/trace.test.d.ts +1 -0
  77. package/dist/_core/trace.test.js +60 -0
  78. package/dist/auth.d.ts +3 -60
  79. package/dist/auth.js +6 -88
  80. package/dist/deviceDogfood.d.ts +53 -0
  81. package/dist/deviceDogfood.js +137 -0
  82. package/dist/dogfoodPolicy.d.ts +25 -0
  83. package/dist/dogfoodPolicy.js +24 -0
  84. package/dist/index.d.ts +17 -4
  85. package/dist/index.js +33 -7
  86. package/dist/reloadActions.d.ts +88 -0
  87. package/dist/reloadActions.js +200 -0
  88. package/dist/storeShots.d.ts +67 -0
  89. package/dist/storeShots.js +137 -0
  90. package/dist/types.d.ts +208 -8
  91. package/package.json +13 -4
  92. package/src/AuthOverlay.tsx +20 -106
  93. package/src/BlackBox.ts +10 -1
  94. package/src/DeployPanel.tsx +74 -0
  95. package/src/Discovery.ts +13 -8
  96. package/src/DogfoodRuntime.ts +373 -0
  97. package/src/FeedbackModal.tsx +619 -79
  98. package/src/LoginScreen.tsx +2 -22
  99. package/src/MachinePickerScreen.tsx +6 -21
  100. package/src/P2PClient.ts +580 -4
  101. package/src/P2PDogfoodDriver.ts +132 -0
  102. package/src/PairDeviceModal.tsx +2 -3
  103. package/src/ShakeDetector.ts +1 -0
  104. package/src/VibeChatScreen.tsx +232 -42
  105. package/src/YaverFeedback.ts +631 -68
  106. package/src/YaverModeBadge.tsx +234 -0
  107. package/src/__tests__/AuthDevices.test.ts +1 -52
  108. package/src/__tests__/BlackBox.relayPassword.test.ts +129 -0
  109. package/src/__tests__/BlackBoxAutoStart.test.ts +111 -0
  110. package/src/__tests__/BlackBoxAutoStartColdStart.test.ts +191 -0
  111. package/src/__tests__/BrowserLaneIcon.test.ts +85 -0
  112. package/src/__tests__/DogfoodRuntime.test.ts +135 -0
  113. package/src/__tests__/P2PClient.test.ts +2 -2
  114. package/src/__tests__/P2PDogfoodDriver.test.ts +80 -0
  115. package/src/__tests__/ReportIdentity.test.ts +212 -0
  116. package/src/__tests__/SDKToken.test.ts +1 -1
  117. package/src/__tests__/ShakeToggle.test.ts +153 -0
  118. package/src/__tests__/YaverFeedback.test.ts +15 -3
  119. package/src/__tests__/deviceDogfood.test.ts +77 -0
  120. package/src/__tests__/dogfoodPolicy.test.ts +34 -0
  121. package/src/__tests__/pickTargetDevice.test.ts +101 -0
  122. package/src/__tests__/reloadActions.test.ts +171 -0
  123. package/src/__tests__/reloadActionsParity.test.ts +49 -0
  124. package/src/__tests__/types.test.ts +5 -5
  125. package/src/_core/ansi.test.ts +250 -0
  126. package/src/_core/ansi.ts +475 -0
  127. package/src/_core/buildFeedbackPrompt.ts +18 -95
  128. package/src/_core/constants.ts +20 -6
  129. package/src/_core/device.ts +5 -16
  130. package/src/_core/endpoints.ts +0 -7
  131. package/src/_core/index.ts +4 -0
  132. package/src/_core/remoteless.ts +110 -0
  133. package/src/_core/trace.test.ts +58 -0
  134. package/src/_core/trace.ts +75 -0
  135. package/src/auth.ts +7 -156
  136. package/src/deviceDogfood.ts +171 -0
  137. package/src/dogfoodPolicy.ts +40 -0
  138. package/src/index.ts +61 -11
  139. package/src/reloadActions.ts +273 -0
  140. package/src/storeShots.ts +189 -0
  141. package/src/types.ts +209 -8
  142. package/dist/GuestOnboardingScreen.d.ts +0 -8
  143. package/dist/GuestOnboardingScreen.js +0 -282
  144. package/src/GuestOnboardingScreen.tsx +0 -307
@@ -4,6 +4,7 @@ import {
4
4
  DeviceEventEmitter,
5
5
  Keyboard,
6
6
  KeyboardAvoidingView,
7
+ Linking,
7
8
  Modal,
8
9
  Platform,
9
10
  Pressable,
@@ -25,6 +26,7 @@ import {
25
26
  // stopVideoRecording,
26
27
  } from './capture';
27
28
  import { uploadFeedback } from './upload';
29
+ import { resolveReportIdentity } from './P2PClient';
28
30
  import {
29
31
  DeviceInfo,
30
32
  FeedbackBundle,
@@ -34,9 +36,12 @@ import {
34
36
  } from './types';
35
37
  import { AuthOverlay } from './AuthOverlay';
36
38
  import { QuickActionIcon } from './QuickActionIcon';
39
+ import { YaverModeBadge } from './YaverModeBadge';
37
40
  import { VibeChatScreen } from './VibeChatScreen';
38
41
  import { DeployPanel } from './DeployPanel';
39
42
  import { listReachableDevices, RemoteDevice } from './auth';
43
+ import { reloadActions } from './reloadActions';
44
+ import type { DevServerSnapshot, ReloadAction } from './reloadActions';
40
45
  import {
41
46
  QUICK_ICON_COLOR_PRESETS,
42
47
  QuickIconColorPreset,
@@ -45,6 +50,14 @@ import {
45
50
  setPreferredModel,
46
51
  setPreferredRunner,
47
52
  } from './preferences';
53
+ import {
54
+ DogfoodController,
55
+ defaultDogfoodLane,
56
+ dogfoodLaneOptions,
57
+ type DogfoodLane,
58
+ type DogfoodSnapshot,
59
+ } from './DogfoodRuntime';
60
+ import { createP2PDogfoodDriver } from './P2PDogfoodDriver';
48
61
 
49
62
  /**
50
63
  * Simplified feedback modal — launch scope is 3 actions:
@@ -88,6 +101,7 @@ type RunnerCardState = {
88
101
  detail?: string;
89
102
  actionLabel?: string;
90
103
  actionRunner?: string;
104
+ models?: Array<{ id: string; name?: string; provider?: string; isDefault?: boolean }>;
91
105
  };
92
106
 
93
107
  type ProviderEditorState = {
@@ -98,6 +112,14 @@ type ProviderEditorState = {
98
112
  apiKey: string;
99
113
  };
100
114
 
115
+ type DogfoodProjectChoice = {
116
+ name: string;
117
+ path: string;
118
+ framework?: string;
119
+ frameworks?: string[];
120
+ surfaces?: string[];
121
+ };
122
+
101
123
  const PRIMARY_RUNNER_IDS = ['claude', 'codex', 'opencode'] as const;
102
124
 
103
125
  function normalizeRunnerStatusRows(rows: RunnerAuthStatusRow[]): RunnerCardState[] {
@@ -139,6 +161,7 @@ function normalizeRunnerStatusRows(rows: RunnerAuthStatusRow[]): RunnerCardState
139
161
  version: row.version,
140
162
  tone: 'warning',
141
163
  statusLine: 'Not installed on the selected machine',
164
+ models: row.models,
142
165
  detail,
143
166
  };
144
167
  }
@@ -156,6 +179,7 @@ function normalizeRunnerStatusRows(rows: RunnerAuthStatusRow[]): RunnerCardState
156
179
  statusLine: configured
157
180
  ? `${versionPrefix}Configured on the selected machine`
158
181
  : `${versionPrefix}Needs provider config on the selected machine`,
182
+ models: row.models,
159
183
  detail,
160
184
  };
161
185
  }
@@ -172,6 +196,7 @@ function normalizeRunnerStatusRows(rows: RunnerAuthStatusRow[]): RunnerCardState
172
196
  statusLine: authed
173
197
  ? `${versionPrefix}Signed in on the selected machine`
174
198
  : `${versionPrefix}Not signed in on the selected machine`,
199
+ models: row.models,
175
200
  detail,
176
201
  actionLabel: authed ? 'Re-auth' : 'Sign in',
177
202
  actionRunner: id,
@@ -179,6 +204,36 @@ function normalizeRunnerStatusRows(rows: RunnerAuthStatusRow[]): RunnerCardState
179
204
  });
180
205
  }
181
206
 
207
+
208
+ /**
209
+ * Renders the "inside Yaver" mark unless the app opted out.
210
+ *
211
+ * Lives here rather than in YaverModeBadge so the component stays a dumb view
212
+ * and the DEFAULT (`modeBadge !== false` → on) is expressed in exactly one
213
+ * place. Mounted from FeedbackModal because that is the component integrating
214
+ * apps already render — a default that required a new mount point would not be
215
+ * a default, it would be a feature request.
216
+ */
217
+ const YaverModeBadgeGate: React.FC = () => {
218
+ const [, refresh] = useState(0);
219
+ useEffect(() => {
220
+ const sub = DeviceEventEmitter.addListener('yaverFeedback:dogfoodChanged', () => refresh((n) => n + 1));
221
+ return () => sub.remove();
222
+ }, []);
223
+ const cfg = YaverFeedback.getConfig();
224
+ if (cfg?.modeBadge === false) return null;
225
+ const dogfood = YaverFeedback.getDogfoodStatus();
226
+ return (
227
+ <YaverModeBadge
228
+ position={cfg?.modeBadgePosition ?? 'bottom-left'}
229
+ force={dogfood.active}
230
+ dogfood={dogfood.active}
231
+ dogfoodLabel={dogfood.label}
232
+ onExitDogfood={() => YaverFeedback.exitDogfoodMode()}
233
+ />
234
+ );
235
+ };
236
+
182
237
  export const FeedbackModal: React.FC = () => {
183
238
  const { width: winW, height: winH } = useWindowDimensions();
184
239
  const isTablet = Math.min(winW, winH) >= 600;
@@ -186,6 +241,7 @@ export const FeedbackModal: React.FC = () => {
186
241
  // looks empty on a 1024pt iPad. Mobile keeps 3-col.
187
242
  const iconOptionWidthOverride = isTablet ? '18%' : undefined;
188
243
  const [visible, setVisible] = useState(false);
244
+ const [dogfoodActive, setDogfoodActive] = useState(() => YaverFeedback.getDogfoodStatus().active);
189
245
  const [action, setAction] = useState<ActionState>('idle');
190
246
  const [error, setError] = useState<string | null>(null);
191
247
  const [toast, setToast] = useState<string | null>(null);
@@ -195,6 +251,11 @@ export const FeedbackModal: React.FC = () => {
195
251
  // is our guaranteed UI for bringing the icon back — we surface a
196
252
  // small "Show quick icon" row when this is true.
197
253
  const [quickIconHidden, setQuickIconHidden] = useState(false);
254
+ // Dev-server snapshot behind the reload buttons. `null` means "we have not
255
+ // been able to ask" — rendered as "not connected", never as "no dev server".
256
+ const [devSnapshot, setDevSnapshot] = useState<DevServerSnapshot | null>(null);
257
+ // Which reload action is in flight, so only that button spins.
258
+ const [reloadingId, setReloadingId] = useState<string | null>(null);
198
259
  const [runnerAuthModal, setRunnerAuthModal] = useState<string | null>(null);
199
260
  // Vibing-input mode: same expand-on-tap pattern as email login.
200
261
  // Tap "Vibing" once → the button reveals an input + Send; that lets
@@ -223,9 +284,108 @@ export const FeedbackModal: React.FC = () => {
223
284
  const [runnerStatusError, setRunnerStatusError] = useState<string | null>(null);
224
285
  const [preferredRunner, setPreferredRunnerState] = useState<string | null>(null);
225
286
  const [preferredModel, setPreferredModelState] = useState('');
287
+ const [activeTab, setActiveTab] = useState<'chat' | 'settings'>('chat');
226
288
  const [showOpenCodeConfig, setShowOpenCodeConfig] = useState(false);
289
+ const scrollRef = useRef<ScrollView>(null);
290
+ const [dogfoodEnrollment, setDogfoodEnrollment] = useState<{
291
+ status: string;
292
+ installationId?: string;
293
+ error?: string;
294
+ } | null>(null);
295
+ const [dogfoodProjects, setDogfoodProjects] = useState<DogfoodProjectChoice[]>([]);
296
+ const [dogfoodProject, setDogfoodProject] = useState<DogfoodProjectChoice | null>(null);
297
+ const [dogfoodLane, setDogfoodLane] = useState<DogfoodLane>('browser');
298
+ const [dogfoodNativeAvailable, setDogfoodNativeAvailable] = useState(false);
299
+ const [dogfoodRuntime, setDogfoodRuntime] = useState<DogfoodSnapshot | null>(null);
300
+ const [dogfoodSetupLoading, setDogfoodSetupLoading] = useState(false);
301
+ const dogfoodControllerRef = useRef<DogfoodController | null>(null);
227
302
  const mountedRef = useRef(true);
228
303
 
304
+ const loadDogfoodOnboarding = useCallback(async () => {
305
+ const onboarding = YaverFeedback.getDogfoodOnboarding();
306
+ if (!onboarding) return;
307
+ setDogfoodSetupLoading(true);
308
+ setDogfoodEnrollment({ status: 'checking' });
309
+ try {
310
+ const enrolled = await YaverFeedback.enableDeviceDogfood(onboarding);
311
+ if (!mountedRef.current) return;
312
+ setDogfoodEnrollment({ status: enrolled.status, installationId: enrolled.installationId });
313
+ if (enrolled.status !== 'active') return;
314
+ let client = YaverFeedback.getP2PClient();
315
+ if (!client && await YaverFeedback.reconnect()) client = YaverFeedback.getP2PClient();
316
+ if (!client) throw new Error('The selected Yaver machine is not reachable yet.');
317
+ const projects = await client.listDogfoodProjects();
318
+ if (!mountedRef.current) return;
319
+ setDogfoodProjects(projects);
320
+ const hint = String(onboarding.projectName || '').trim().toLowerCase();
321
+ const preferred = projects.find((item) => item.name.toLowerCase() === hint)
322
+ || projects.find((item) => hint && item.name.toLowerCase().includes(hint))
323
+ || projects[0]
324
+ || null;
325
+ setDogfoodProject((current) => current && projects.some((item) => item.path === current.path) ? current : preferred);
326
+ if (preferred) setDogfoodLane(defaultDogfoodLane(preferred.framework || onboarding.framework || 'expo'));
327
+ } catch (cause) {
328
+ if (mountedRef.current) setDogfoodEnrollment({ status: 'failed', error: cause instanceof Error ? cause.message : String(cause) });
329
+ } finally {
330
+ if (mountedRef.current) setDogfoodSetupLoading(false);
331
+ }
332
+ }, []);
333
+
334
+ useEffect(() => {
335
+ let cancelled = false;
336
+ setDogfoodNativeAvailable(false);
337
+ const client = YaverFeedback.getP2PClient();
338
+ if (!client || !dogfoodProject) return () => { cancelled = true; };
339
+ const framework = dogfoodProject.framework || YaverFeedback.getDogfoodOnboarding()?.framework || 'expo';
340
+ void client.getDogfoodRemoteRuntimeCapabilities(dogfoodProject.path, framework)
341
+ .then((value) => {
342
+ if (!cancelled) setDogfoodNativeAvailable(value.targets.some((target) => target.enabled && target.id !== 'browser-window'));
343
+ })
344
+ .catch(() => {});
345
+ return () => { cancelled = true; };
346
+ }, [dogfoodProject]);
347
+
348
+ const startDogfoodRuntime = useCallback(async () => {
349
+ const client = YaverFeedback.getP2PClient();
350
+ const onboarding = YaverFeedback.getDogfoodOnboarding();
351
+ if (!client || !dogfoodProject || !onboarding) return;
352
+ await dogfoodControllerRef.current?.stop().catch(() => {});
353
+ const framework = dogfoodProject.framework || onboarding.framework || 'expo';
354
+ const controller = new DogfoodController({
355
+ name: dogfoodProject.name,
356
+ workDir: dogfoodProject.path,
357
+ framework,
358
+ lane: dogfoodLane,
359
+ }, createP2PDogfoodDriver(client), {
360
+ onChange: (snapshot) => { if (mountedRef.current) setDogfoodRuntime(snapshot); },
361
+ });
362
+ dogfoodControllerRef.current = controller;
363
+ setDogfoodRuntime(controller.snapshot());
364
+ await controller.trigger().catch(() => {});
365
+ }, [dogfoodLane, dogfoodProject]);
366
+
367
+ /**
368
+ * Ask the machine what its dev server is doing, so the reload actions can
369
+ * be enabled/disabled against reality rather than against a guess.
370
+ *
371
+ * Best-effort and deliberately null-on-failure: null means "we could not
372
+ * ask", which the seam renders as "not connected to a machine yet" — a
373
+ * different sentence from "no dev server is running", because they have
374
+ * different fixes.
375
+ */
376
+ const refreshDevSnapshot = useCallback(async () => {
377
+ try {
378
+ const client = YaverFeedback.getP2PClient();
379
+ if (!client) {
380
+ setDevSnapshot(null);
381
+ return;
382
+ }
383
+ setDevSnapshot(await client.getDevServerStatus());
384
+ } catch {
385
+ setDevSnapshot(null);
386
+ }
387
+ }, []);
388
+
229
389
  const loadSelectedMachine = useCallback(async () => {
230
390
  const cfg = YaverFeedback.getConfig();
231
391
  if (!cfg?.authToken) {
@@ -261,9 +421,8 @@ export const FeedbackModal: React.FC = () => {
261
421
 
262
422
  try {
263
423
  const devices = await listReachableDevices(cfg.authToken);
264
- const all = [...devices.owned, ...devices.shared];
265
424
  const device =
266
- all.find((candidate) => candidate.deviceId === cfg.preferredDeviceId) ?? null;
425
+ devices.owned.find((candidate) => candidate.deviceId === cfg.preferredDeviceId) ?? null;
267
426
 
268
427
  if (!device) {
269
428
  if (mountedRef.current) {
@@ -285,9 +444,8 @@ export const FeedbackModal: React.FC = () => {
285
444
  reachable = await client.health();
286
445
  }
287
446
 
288
- const hostHint = device.hostEmail ? ` via ${device.hostEmail}` : '';
289
447
  let status: MachineCardState['status'] = 'live';
290
- let detail = `${device.platform}${hostHint}`;
448
+ let detail = device.platform;
291
449
 
292
450
  if (!device.isOnline) {
293
451
  status = 'offline';
@@ -360,7 +518,7 @@ export const FeedbackModal: React.FC = () => {
360
518
  if (!client) {
361
519
  throw new Error('Not connected to the selected machine yet.');
362
520
  }
363
- const rows = await client.getRunnerAuthStatus();
521
+ const rows = await client.getAvailableRunners();
364
522
  if (mountedRef.current) {
365
523
  setRunnerCards(normalizeRunnerStatusRows(rows));
366
524
  }
@@ -394,13 +552,20 @@ export const FeedbackModal: React.FC = () => {
394
552
  mountedRef.current = true;
395
553
  const sub = DeviceEventEmitter.addListener('yaverFeedback:startReport', () => {
396
554
  if (YaverFeedback.isEnabled()) {
555
+ const onboarding = YaverFeedback.getDogfoodOnboarding();
556
+ const directDogfood = YaverFeedback.getDogfoodStatus().active;
557
+ setDogfoodActive(directDogfood);
397
558
  setVisible(true);
398
559
  setError(null);
399
560
  setToast(null);
400
561
  setProgress(null);
401
562
  setAction('idle');
402
- setShowVibeInput(false);
563
+ setShowVibeInput(directDogfood);
403
564
  setVibePrompt('');
565
+ if (onboarding) {
566
+ setActiveTab('settings');
567
+ void loadDogfoodOnboarding();
568
+ }
404
569
  // Re-read the "user hid the quick icon" flag on every open so
405
570
  // the re-enable row reflects the latest preference (the user
406
571
  // might have hidden or shown it between opens).
@@ -418,6 +583,14 @@ export const FeedbackModal: React.FC = () => {
418
583
  void loadRunnerStatuses();
419
584
  }
420
585
  });
586
+ const dogfoodSub = DeviceEventEmitter.addListener(
587
+ 'yaverFeedback:dogfoodChanged',
588
+ (payload: { active?: boolean; exited?: boolean }) => {
589
+ if (!mountedRef.current) return;
590
+ setDogfoodActive(payload?.active === true);
591
+ if (payload?.exited === true) setVisible(false);
592
+ },
593
+ );
421
594
  // Agent streams build / compile progress through the BlackBox
422
595
  // SSE command channel as `command: "status"`; YaverFeedback re-emits
423
596
  // it as `yaverFeedback:status`. Show the most recent message in the
@@ -442,18 +615,25 @@ export const FeedbackModal: React.FC = () => {
442
615
  return () => {
443
616
  mountedRef.current = false;
444
617
  sub.remove();
618
+ dogfoodSub.remove();
445
619
  statusSub.remove();
446
620
  };
447
- }, [loadRunnerStatuses, loadSelectedMachine]);
621
+ }, [loadDogfoodOnboarding, loadRunnerStatuses, loadSelectedMachine]);
448
622
 
449
623
  useEffect(() => {
450
624
  if (!visible) return;
625
+ // Poll the dev server alongside the machine + runners. A reload button
626
+ // whose enabled state was decided once, when the sheet opened, goes
627
+ // stale the moment the user starts Metro from another surface — and a
628
+ // stale "no dev server is running" reads as the product being broken.
629
+ void refreshDevSnapshot();
451
630
  const interval = setInterval(() => {
452
631
  void loadSelectedMachine();
453
632
  void loadRunnerStatuses();
633
+ void refreshDevSnapshot();
454
634
  }, 5000);
455
635
  return () => clearInterval(interval);
456
- }, [loadRunnerStatuses, loadSelectedMachine, visible]);
636
+ }, [loadRunnerStatuses, loadSelectedMachine, refreshDevSnapshot, visible]);
457
637
 
458
638
  useEffect(() => {
459
639
  if (!visible) {
@@ -475,6 +655,12 @@ export const FeedbackModal: React.FC = () => {
475
655
  };
476
656
  }, [visible]);
477
657
 
658
+ useEffect(() => {
659
+ if (!visible || !showVibeInput) return;
660
+ const timer = setTimeout(() => scrollRef.current?.scrollToEnd({ animated: true }), keyboardInset > 0 ? 120 : 40);
661
+ return () => clearTimeout(timer);
662
+ }, [keyboardInset, showVibeInput, visible]);
663
+
478
664
  const closeSoon = useCallback((delayMs = 1200) => {
479
665
  setTimeout(() => {
480
666
  if (mountedRef.current) setVisible(false);
@@ -490,6 +676,9 @@ export const FeedbackModal: React.FC = () => {
490
676
  setShowVibeInput(false);
491
677
  setVibePrompt('');
492
678
  setRunnerStatusError(null);
679
+ void dogfoodControllerRef.current?.stop().catch(() => {});
680
+ dogfoodControllerRef.current = null;
681
+ YaverFeedback.clearDogfoodOnboarding();
493
682
  }, []);
494
683
 
495
684
  // Helper: run a P2P call; on network failure, ask YaverFeedback to
@@ -542,7 +731,89 @@ export const FeedbackModal: React.FC = () => {
542
731
  [],
543
732
  );
544
733
 
545
- // ─── 1. Hot reload ─────────────────────────────────────────────────
734
+ // ─── 1. Reload ─────────────────────────────────────────────────────
735
+ //
736
+ // Three actions now, not one: Hot Reload (mode=fast), Full Reload
737
+ // (mode=full — Flutter's hot RESTART), and Rebuild Bundle
738
+ // (/dev/reload-app, the only one that works with Metro down).
739
+ //
740
+ // WHICH of them render, and which are disabled with what reason, is
741
+ // decided by the pure `reloadActions()` seam — never inline here, so the
742
+ // same policy holds on web, Flutter, Unity, Swift and Kotlin. In
743
+ // particular: a production build (`__DEV__ === false`) gets NONE of them.
744
+
745
+ const availableReloadActions = reloadActions(devSnapshot, {
746
+ // __DEV__ is React Native's own build flag. A release bundle sets it
747
+ // false, so a shipped app renders no reload UI at all — which is the
748
+ // point, and is what reloadActions.test.ts pins.
749
+ isDevBuild: typeof __DEV__ !== 'undefined' && __DEV__ === true,
750
+ connected: devSnapshot !== null,
751
+ machineLabel: machineCard.device?.name || undefined,
752
+ includeRebuild: true,
753
+ });
754
+
755
+ const handleReloadAction = useCallback(
756
+ async (reloadAction: ReloadAction) => {
757
+ if (!reloadAction.enabled) {
758
+ // Pressing a disabled action must SAY why. A row that does nothing
759
+ // is the same defect as a spinner that never resolves.
760
+ setToast(reloadAction.disabledReason || 'Reload is unavailable right now.');
761
+ setError(reloadAction.disabledReason || null);
762
+ return;
763
+ }
764
+ setReloadingId(reloadAction.id);
765
+ setAction('hot-reloading');
766
+ setError(null);
767
+ setProgress(0);
768
+ setToast(`${reloadAction.label}…`);
769
+ try {
770
+ await loadSelectedMachine();
771
+ const selected = await YaverFeedback.getSelectedRemoteDevice();
772
+ if (!selected) {
773
+ YaverFeedback.showMachinePicker();
774
+ throw new Error('No machine selected. Pick a machine and try again.');
775
+ }
776
+ if (selected.needsAuth) {
777
+ YaverFeedback.showMachinePicker();
778
+ throw new Error('Selected machine needs pairing again.');
779
+ }
780
+ if (!selected.isOnline) {
781
+ throw new Error('Selected machine is offline. Start `yaver serve` on it first.');
782
+ }
783
+
784
+ let ackMessage = `${reloadAction.label} requested.`;
785
+ await runWithReconnect(async () => {
786
+ const renderClient = YaverFeedback.getRenderP2PClient();
787
+ if (!renderClient) throw new Error('Render machine is not connected yet.');
788
+ const ack = await renderClient.reloadWithMode(reloadAction.mode, devSnapshot);
789
+ ackMessage = ack.message;
790
+ setToast(ack.message);
791
+ setProgress(0.2);
792
+ });
793
+ setToast(ackMessage);
794
+ if (reloadAction.mode === 'bundle') closeSoon(2500);
795
+ } catch (err: unknown) {
796
+ const message = err instanceof Error ? err.message : String(err);
797
+ setError(message);
798
+ setToast(
799
+ message.toLowerCase().indexOf('session expired') >= 0
800
+ ? 'Session expired. Sign in again.'
801
+ : message,
802
+ );
803
+ await loadSelectedMachine();
804
+ setProgress(null);
805
+ } finally {
806
+ if (mountedRef.current) {
807
+ setAction('idle');
808
+ setReloadingId(null);
809
+ void refreshDevSnapshot();
810
+ }
811
+ }
812
+ },
813
+ [closeSoon, devSnapshot, loadSelectedMachine, refreshDevSnapshot, runWithReconnect],
814
+ );
815
+
816
+ /** Kept for the legacy one-tap path (BlackBox command, chat Reload button). */
546
817
  const handleHotReload = useCallback(async () => {
547
818
  setAction('hot-reloading');
548
819
  setError(null);
@@ -569,8 +840,10 @@ export const FeedbackModal: React.FC = () => {
569
840
  // map this app to its MobileProject scan entry without needing
570
841
  // `yaver dev start` to have been run.
571
842
  let ackMessage = 'Reload request acknowledged.';
572
- await runWithReconnect(async (client) => {
573
- const ack = await client.reloadApp('bundle');
843
+ await runWithReconnect(async () => {
844
+ const renderClient = YaverFeedback.getRenderP2PClient();
845
+ if (!renderClient) throw new Error('Render machine is not connected yet.');
846
+ const ack = await renderClient.reloadApp('bundle');
574
847
  ackMessage = ack.message;
575
848
  setToast(ack.message);
576
849
  setProgress(0.2);
@@ -661,19 +934,36 @@ export const FeedbackModal: React.FC = () => {
661
934
  try {
662
935
  const { Dimensions } = require('react-native');
663
936
  const { width, height } = Dimensions.get('window');
937
+ const cfg = YaverFeedback.getConfig();
938
+ const identity = resolveReportIdentity({
939
+ projectName: cfg?.projectName,
940
+ bundleId: cfg?.bundleId,
941
+ surface: cfg?.surface,
942
+ surfaces: cfg?.surfaces,
943
+ stack: cfg?.stack,
944
+ stacks: cfg?.stacks,
945
+ testSurfaces: cfg?.testSurfaces,
946
+ feedbackSdk: cfg?.feedbackSdk,
947
+ feedbackTransport: cfg?.feedbackTransport,
948
+ voiceCapabilities: cfg?.voiceCapabilities,
949
+ sttProvider: cfg?.sttProvider,
950
+ ttsProvider: cfg?.ttsProvider,
951
+ });
664
952
  const deviceInfo: DeviceInfo = {
665
953
  platform: Platform.OS,
666
954
  osVersion: String(Platform.Version),
667
955
  model: Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
668
956
  screenWidth: width,
669
957
  screenHeight: height,
958
+ appName: identity.appName,
670
959
  };
671
960
  const capturedErrors = YaverFeedback.getCapturedErrors();
672
961
  const bundle: FeedbackBundle = {
673
962
  metadata: {
674
963
  timestamp: new Date().toISOString(),
675
- device: deviceInfo,
676
- app: {},
964
+ deviceInfo,
965
+ app: identity.app,
966
+ project: identity.project,
677
967
  userNote: '[Screenshot + Fix]',
678
968
  },
679
969
  screenshots: [path],
@@ -742,6 +1032,7 @@ export const FeedbackModal: React.FC = () => {
742
1032
  taskId: string;
743
1033
  initialPrompt: string;
744
1034
  project?: string;
1035
+ projectPath?: string;
745
1036
  runner?: string;
746
1037
  model?: string;
747
1038
  } | null>(null);
@@ -810,6 +1101,7 @@ export const FeedbackModal: React.FC = () => {
810
1101
  taskId: result.taskId,
811
1102
  initialPrompt: promptText,
812
1103
  project: identity.projectName,
1104
+ projectPath: identity.projectPath,
813
1105
  runner: preferredRunner ?? undefined,
814
1106
  model: preferredModel ?? undefined,
815
1107
  });
@@ -829,6 +1121,7 @@ export const FeedbackModal: React.FC = () => {
829
1121
  */
830
1122
 
831
1123
  const busy = action !== 'idle';
1124
+ const machineRouting = YaverFeedback.getMachineRouting();
832
1125
  const readyRunnerCount = runnerCards.filter((row) => row.ready || row.authConfigured).length;
833
1126
  const missingRunnerCount = runnerCards.filter((row) => !row.installed).length;
834
1127
  const needsAuthRunnerCount = runnerCards.filter(
@@ -839,17 +1132,18 @@ export const FeedbackModal: React.FC = () => {
839
1132
  // for the live chat screen. The chat manages its own SSE
840
1133
  // subscription, multi-turn follow-ups, and Reload button. Closing
841
1134
  // the chat returns to idle and clears the active vibe.
842
- if (visible && activeVibe) {
1135
+ if (activeVibe) {
843
1136
  const client = YaverFeedback.getP2PClient();
844
1137
  return (
845
1138
  <>
846
1139
  <AuthOverlay />
847
1140
  <QuickActionIcon />
1141
+ <YaverModeBadgeGate />
848
1142
  <Modal
849
1143
  visible={visible}
850
1144
  animationType="slide"
851
1145
  transparent
852
- onRequestClose={() => setActiveVibe(null)}
1146
+ onRequestClose={handleClose}
853
1147
  >
854
1148
  {client ? (
855
1149
  <VibeChatScreen
@@ -857,11 +1151,20 @@ export const FeedbackModal: React.FC = () => {
857
1151
  initialTaskId={activeVibe.taskId}
858
1152
  initialUserPrompt={activeVibe.initialPrompt}
859
1153
  project={activeVibe.project}
1154
+ projectPath={activeVibe.projectPath}
860
1155
  runner={activeVibe.runner}
861
1156
  model={activeVibe.model}
1157
+ voiceInputEnabled={YaverFeedback.getConfig()?.voiceInputEnabled === true}
862
1158
  onClose={() => setActiveVibe(null)}
1159
+ onNewTopic={() => {
1160
+ setActiveVibe(null);
1161
+ setShowVibeInput(true);
1162
+ }}
1163
+ onMinimize={handleClose}
1164
+ codingMachine={YaverFeedback.getMachineRouting().codingDeviceId}
1165
+ renderMachine={YaverFeedback.getMachineRouting().renderDeviceId}
863
1166
  onReload={async () => {
864
- const c = YaverFeedback.getP2PClient();
1167
+ const c = YaverFeedback.getRenderP2PClient();
865
1168
  if (!c) throw new Error('Not connected');
866
1169
  await c.reloadApp();
867
1170
  }}
@@ -876,6 +1179,7 @@ export const FeedbackModal: React.FC = () => {
876
1179
  <>
877
1180
  <AuthOverlay />
878
1181
  <QuickActionIcon />
1182
+ <YaverModeBadgeGate />
879
1183
  {visible && (
880
1184
  <Modal
881
1185
  visible={visible}
@@ -912,6 +1216,7 @@ export const FeedbackModal: React.FC = () => {
912
1216
  }}
913
1217
  >
914
1218
  <ScrollView
1219
+ ref={scrollRef}
915
1220
  style={styles.scroll}
916
1221
  contentContainerStyle={[
917
1222
  styles.scrollContent,
@@ -921,9 +1226,13 @@ export const FeedbackModal: React.FC = () => {
921
1226
  ]}
922
1227
  keyboardShouldPersistTaps="handled"
923
1228
  keyboardDismissMode={Platform.OS === 'ios' ? 'interactive' : 'on-drag'}
1229
+ contentInsetAdjustmentBehavior="always"
1230
+ automaticallyAdjustKeyboardInsets={Platform.OS === 'ios'}
924
1231
  >
925
1232
  <View style={styles.header}>
926
- <Text style={styles.title}>Send Feedback</Text>
1233
+ <Text style={styles.title}>
1234
+ {dogfoodActive ? `${YaverFeedback.getDogfoodStatus().label || 'App'} Developer Mode` : 'Send Feedback'}
1235
+ </Text>
927
1236
  <Pressable
928
1237
  onPress={handleClose}
929
1238
  hitSlop={12}
@@ -935,6 +1244,114 @@ export const FeedbackModal: React.FC = () => {
935
1244
  </Pressable>
936
1245
  </View>
937
1246
 
1247
+ <View style={styles.tabs} accessibilityRole="tablist">
1248
+ {(['chat', 'settings'] as const).map((tab) => (
1249
+ <Pressable
1250
+ key={tab}
1251
+ onPress={() => setActiveTab(tab)}
1252
+ style={[styles.tab, activeTab === tab && styles.tabSelected]}
1253
+ accessibilityRole="tab"
1254
+ accessibilityState={{ selected: activeTab === tab }}
1255
+ >
1256
+ <Text style={[styles.tabText, activeTab === tab && styles.tabTextSelected]}>{tab === 'chat' ? 'Chat' : 'Settings'}</Text>
1257
+ </Pressable>
1258
+ ))}
1259
+ </View>
1260
+
1261
+ <View style={[styles.tabContent, activeTab !== 'settings' && styles.hidden]}>
1262
+ <>
1263
+ {YaverFeedback.getDogfoodOnboarding() ? (
1264
+ <View style={styles.dogfoodWizard}>
1265
+ <Text style={styles.dogfoodWizardTitle}>Dogfood this app</Text>
1266
+ <Text style={styles.dogfoodWizardHint}>
1267
+ OAuth ✓ · machine {machineCard.device ? '✓' : 'required'} · installation {dogfoodEnrollment?.status || 'checking'}
1268
+ </Text>
1269
+ {dogfoodEnrollment?.installationId ? (
1270
+ <Text selectable style={styles.dogfoodInstallationId}>
1271
+ This device · {dogfoodEnrollment.installationId}
1272
+ </Text>
1273
+ ) : null}
1274
+ {dogfoodEnrollment?.status !== 'active' ? (
1275
+ <View style={styles.dogfoodPendingBox}>
1276
+ <Text style={styles.dogfoodPendingText}>
1277
+ {dogfoodEnrollment?.error
1278
+ || 'Approve this installation from Yaver → Settings → Third-party app testing. A UUID alone never grants access.'}
1279
+ </Text>
1280
+ <Pressable
1281
+ onPress={() => void loadDogfoodOnboarding()}
1282
+ style={({ pressed }) => [styles.runnerRefreshBtn, pressed && styles.buttonPressed]}
1283
+ >
1284
+ <Text style={styles.runnerRefreshBtnText}>{dogfoodSetupLoading ? 'Checking…' : 'Check approval'}</Text>
1285
+ </Pressable>
1286
+ </View>
1287
+ ) : (
1288
+ <>
1289
+ <Text style={styles.dogfoodStepLabel}>Project on selected machine</Text>
1290
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.dogfoodChoiceRow}>
1291
+ {dogfoodProjects.map((project) => (
1292
+ <Pressable
1293
+ key={project.path}
1294
+ onPress={() => {
1295
+ setDogfoodProject(project);
1296
+ setDogfoodLane(defaultDogfoodLane(project.framework || YaverFeedback.getDogfoodOnboarding()?.framework || 'expo'));
1297
+ setDogfoodRuntime(null);
1298
+ }}
1299
+ style={[styles.dogfoodChoice, dogfoodProject?.path === project.path && styles.dogfoodChoiceSelected]}
1300
+ >
1301
+ <Text style={[styles.dogfoodChoiceText, dogfoodProject?.path === project.path && styles.dogfoodChoiceTextSelected]}>{project.name}</Text>
1302
+ </Pressable>
1303
+ ))}
1304
+ </ScrollView>
1305
+ <Text style={styles.dogfoodStepLabel}>Runtime lane</Text>
1306
+ <View style={styles.dogfoodChoiceRow}>
1307
+ {dogfoodLaneOptions(
1308
+ dogfoodProject?.framework || YaverFeedback.getDogfoodOnboarding()?.framework || 'expo',
1309
+ { nativeRuntimeAvailable: dogfoodNativeAvailable },
1310
+ ).map((option) => (
1311
+ <Pressable
1312
+ key={option.lane}
1313
+ onPress={() => option.supported && setDogfoodLane(option.lane)}
1314
+ style={[
1315
+ styles.dogfoodChoice,
1316
+ dogfoodLane === option.lane && styles.dogfoodChoiceSelected,
1317
+ !option.supported && styles.actionBtnDisabled,
1318
+ ]}
1319
+ accessibilityState={{ disabled: !option.supported, selected: dogfoodLane === option.lane }}
1320
+ >
1321
+ <Text style={[styles.dogfoodChoiceText, dogfoodLane === option.lane && styles.dogfoodChoiceTextSelected]}>{option.label}</Text>
1322
+ </Pressable>
1323
+ ))}
1324
+ </View>
1325
+ <Text style={styles.dogfoodWizardHint}>
1326
+ Runner: {[preferredRunner || 'automatic', preferredModel].filter(Boolean).join(' · ')}. Change it under Coding Agents below.
1327
+ </Text>
1328
+ <ActionRow
1329
+ label={dogfoodRuntime && !['idle', 'ready', 'failed', 'stopped'].includes(dogfoodRuntime.phase) ? dogfoodRuntime.message : 'Start Dogfood'}
1330
+ tint="#818cf8"
1331
+ onPress={() => void startDogfoodRuntime()}
1332
+ disabled={!dogfoodProject || !!(dogfoodRuntime && !['idle', 'ready', 'failed', 'stopped'].includes(dogfoodRuntime.phase))}
1333
+ busy={!!dogfoodRuntime && !['idle', 'ready', 'failed', 'stopped'].includes(dogfoodRuntime.phase)}
1334
+ />
1335
+ {dogfoodRuntime ? (
1336
+ <View style={styles.dogfoodConsole}>
1337
+ <Text style={styles.dogfoodConsoleStatus}>{dogfoodRuntime.message}</Text>
1338
+ {dogfoodRuntime.logs.slice(-80).map((line, index) => (
1339
+ <Text key={`${line.at}-${index}`} selectable style={styles.dogfoodConsoleLine}>{line.text}</Text>
1340
+ ))}
1341
+ {dogfoodRuntime.failure ? (
1342
+ <Text style={styles.dogfoodConsoleError}>{dogfoodRuntime.failure.error}{'\n'}{dogfoodRuntime.failure.remedy}</Text>
1343
+ ) : null}
1344
+ {dogfoodRuntime.result?.url ? (
1345
+ <Pressable onPress={() => void Linking.openURL(dogfoodRuntime.result!.url!)} style={styles.dogfoodOpenPreview}>
1346
+ <Text style={styles.dogfoodOpenPreviewText}>Open dogfooded app</Text>
1347
+ </Pressable>
1348
+ ) : null}
1349
+ </View>
1350
+ ) : null}
1351
+ </>
1352
+ )}
1353
+ </View>
1354
+ ) : null}
938
1355
  <Pressable
939
1356
  onPress={() => {
940
1357
  if (!YaverFeedback.isAuthed()) {
@@ -960,7 +1377,7 @@ export const FeedbackModal: React.FC = () => {
960
1377
  machineCard.status === 'offline' && styles.machineDotOffline,
961
1378
  ]}
962
1379
  />
963
- <Text style={styles.machineLabel}>Selected Machine</Text>
1380
+ <Text style={styles.machineLabel}>Coding Machine</Text>
964
1381
  </View>
965
1382
  <Text style={styles.machineAction}>
966
1383
  {machineCard.loading ? 'Refreshing…' : 'Change'}
@@ -972,6 +1389,17 @@ export const FeedbackModal: React.FC = () => {
972
1389
  <Text style={styles.machineMeta}>{machineCard.detail}</Text>
973
1390
  </Pressable>
974
1391
 
1392
+ <View style={styles.machineRoutes}>
1393
+ <View style={styles.machineRouteCard}>
1394
+ <Text style={styles.machineRouteLabel}>Coding machine</Text>
1395
+ <Text style={styles.machineRouteValue} numberOfLines={1}>{machineRouting.codingDeviceId || machineCard.title}</Text>
1396
+ </View>
1397
+ <View style={styles.machineRouteCard}>
1398
+ <Text style={styles.machineRouteLabel}>Render machine</Text>
1399
+ <Text style={styles.machineRouteValue} numberOfLines={1}>{machineRouting.renderDeviceId || machineRouting.codingDeviceId || machineCard.title}</Text>
1400
+ </View>
1401
+ </View>
1402
+
975
1403
  <View style={styles.runnerSection}>
976
1404
  <View style={styles.runnerSectionHeader}>
977
1405
  <View style={{ flex: 1 }}>
@@ -997,6 +1425,13 @@ export const FeedbackModal: React.FC = () => {
997
1425
  </Pressable>
998
1426
  </View>
999
1427
 
1428
+ <View style={styles.routingSummary}>
1429
+ <Text style={styles.routingSummaryLabel}>Vibing uses</Text>
1430
+ <Text style={styles.routingSummaryValue} numberOfLines={1}>
1431
+ {[preferredRunner || 'automatic runner', preferredModel].filter(Boolean).join(' · ')}
1432
+ </Text>
1433
+ </View>
1434
+
1000
1435
  {runnerCards.map((row) => (
1001
1436
  <View
1002
1437
  key={row.id}
@@ -1021,20 +1456,53 @@ export const FeedbackModal: React.FC = () => {
1021
1456
  {row.statusLine}
1022
1457
  </Text>
1023
1458
  </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}
1459
+ <View style={styles.runnerCardActions}>
1460
+ {row.ready ? (
1461
+ <Pressable
1462
+ onPress={() => {
1463
+ const nextModel = row.models?.find((model) => model.isDefault)?.id || row.models?.[0]?.id || '';
1464
+ setPreferredRunnerState(row.id);
1465
+ setPreferredModelState(nextModel);
1466
+ void setPreferredRunner(row.id);
1467
+ void setPreferredModel(nextModel || null);
1468
+ }}
1469
+ style={({ pressed }) => [styles.runnerActionBtn, preferredRunner === row.id && styles.runnerActionBtnSelected, pressed && styles.buttonPressed]}
1470
+ accessibilityRole="button"
1471
+ accessibilityLabel={`Use ${row.name} for Vibing`}
1472
+ >
1473
+ <Text style={styles.runnerActionBtnText}>{preferredRunner === row.id ? 'Using' : 'Use'}</Text>
1474
+ </Pressable>
1475
+ ) : null}
1476
+ {row.actionRunner ? (
1477
+ <Pressable
1478
+ onPress={() => setRunnerAuthModal(row.actionRunner ?? null)}
1479
+ style={({ pressed }) => [styles.runnerActionBtn, pressed && styles.buttonPressed]}
1480
+ accessibilityRole="button"
1481
+ accessibilityLabel={`${row.actionLabel} ${row.name}`}
1482
+ >
1483
+ <Text style={styles.runnerActionBtnText}>{row.actionLabel}</Text>
1484
+ </Pressable>
1485
+ ) : null}
1486
+ </View>
1037
1487
  </View>
1488
+ {preferredRunner === row.id && (row.models?.length || 0) > 0 ? (
1489
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.modelChoiceRow}>
1490
+ {row.models!.map((model) => (
1491
+ <Pressable
1492
+ key={model.id}
1493
+ onPress={() => {
1494
+ setPreferredModelState(model.id);
1495
+ void setPreferredModel(model.id);
1496
+ }}
1497
+ style={[styles.modelChoice, preferredModel === model.id && styles.modelChoiceSelected]}
1498
+ accessibilityRole="button"
1499
+ accessibilityLabel={`Use ${model.name || model.id} model`}
1500
+ >
1501
+ <Text style={[styles.modelChoiceText, preferredModel === model.id && styles.modelChoiceTextSelected]}>{model.name || model.id}</Text>
1502
+ </Pressable>
1503
+ ))}
1504
+ </ScrollView>
1505
+ ) : null}
1038
1506
  {row.detail ? (
1039
1507
  <Text style={styles.runnerCardDetail}>{row.detail}</Text>
1040
1508
  ) : null}
@@ -1115,17 +1583,40 @@ export const FeedbackModal: React.FC = () => {
1115
1583
  </View>
1116
1584
  </View>
1117
1585
 
1118
- {/* 1. Hot Reload — the common path */}
1119
- <ActionRow
1120
- label={
1121
- action === 'hot-reloading' ? 'Reloading…' : 'Hot Reload'
1122
- }
1123
- tint="#fbbf24"
1124
- onPress={handleHotReload}
1125
- disabled={busy}
1126
- busy={action === 'hot-reloading'}
1127
- />
1586
+ {/* 1. Reload — Hot / Full / Rebuild Bundle.
1587
+ Rendered from the shared decision seam, so a production
1588
+ build (__DEV__ false) renders nothing here at all, and a
1589
+ blocked action shows greyed WITH its reason underneath
1590
+ rather than vanishing. */}
1591
+ {availableReloadActions.map((reloadAction) => (
1592
+ <View key={reloadAction.id} style={styles.reloadRow}>
1593
+ <ActionRow
1594
+ label={
1595
+ reloadingId === reloadAction.id
1596
+ ? `${reloadAction.label}…`
1597
+ : reloadAction.label
1598
+ }
1599
+ tint={reloadAction.id === 'rebuild' ? '#38bdf8' : '#fbbf24'}
1600
+ onPress={() => {
1601
+ void handleReloadAction(reloadAction);
1602
+ }}
1603
+ // Never `disabled` at the Pressable level for a blocked
1604
+ // action: we WANT the tap so we can say why. Only a
1605
+ // genuinely busy modal blocks the press.
1606
+ disabled={busy && reloadingId !== reloadAction.id}
1607
+ busy={reloadingId === reloadAction.id}
1608
+ />
1609
+ <Text style={styles.reloadHint}>
1610
+ {reloadAction.enabled
1611
+ ? reloadAction.hint
1612
+ : reloadAction.disabledReason}
1613
+ </Text>
1614
+ </View>
1615
+ ))}
1616
+ </>
1617
+ </View>
1128
1618
 
1619
+ <View style={[styles.tabContent, activeTab !== 'chat' && styles.hidden]}>
1129
1620
  {/* 3. Vibing — expands to an input box on first tap
1130
1621
  so the user says WHAT they want to vibe on, just
1131
1622
  like the Yaver mobile app's Vibing tab. Second
@@ -1186,13 +1677,13 @@ export const FeedbackModal: React.FC = () => {
1186
1677
  )}
1187
1678
 
1188
1679
  {/* Screenshot & Fix */}
1189
- <ActionRow
1680
+ {!dogfoodActive ? <ActionRow
1190
1681
  label={action === 'capturing' ? 'Working…' : 'Screenshot & Fix'}
1191
1682
  tint="#22c55e"
1192
1683
  onPress={handleScreenshotAndFix}
1193
1684
  disabled={busy}
1194
1685
  busy={action === 'capturing'}
1195
- />
1686
+ /> : null}
1196
1687
 
1197
1688
  {/* Deploy — opens an inline panel that talks to
1198
1689
  /fleet/deploy-options on the agent and lets the user
@@ -1200,7 +1691,7 @@ export const FeedbackModal: React.FC = () => {
1200
1691
  it on. Capabilities (e.g. "Linux can't TestFlight")
1201
1692
  come from the agent's doctor probes — no client-side
1202
1693
  platform smarts here. */}
1203
- {!showDeploy ? (
1694
+ {!dogfoodActive && (!showDeploy ? (
1204
1695
  <ActionRow
1205
1696
  label="Deploy"
1206
1697
  tint="#7f8cf7"
@@ -1209,7 +1700,8 @@ export const FeedbackModal: React.FC = () => {
1209
1700
  />
1210
1701
  ) : (
1211
1702
  <DeployPanel onClose={() => setShowDeploy(false)} />
1212
- )}
1703
+ ))}
1704
+ </View>
1213
1705
 
1214
1706
  {progress !== null && (
1215
1707
  <View style={styles.progressTrack}>
@@ -1293,6 +1785,33 @@ const ActionRow: React.FC<ActionRowProps> = ({
1293
1785
  );
1294
1786
 
1295
1787
  const styles = StyleSheet.create({
1788
+ dogfoodWizard: { gap: 10, borderRadius: 14, borderWidth: 1, borderColor: 'rgba(129,140,248,0.38)', backgroundColor: 'rgba(129,140,248,0.08)', padding: 13 },
1789
+ dogfoodWizardTitle: { color: '#222229', fontSize: 17, fontWeight: '800' },
1790
+ dogfoodWizardHint: { color: '#6f6f7b', fontSize: 12, lineHeight: 17 },
1791
+ dogfoodInstallationId: { color: '#6555df', fontSize: 11, fontFamily: Platform.select({ ios: 'Menlo', android: 'monospace', default: 'monospace' }) },
1792
+ dogfoodPendingBox: { gap: 9, borderRadius: 11, padding: 10, backgroundColor: 'rgba(245,158,11,0.10)' },
1793
+ dogfoodPendingText: { color: '#8a5a10', fontSize: 12, lineHeight: 17 },
1794
+ dogfoodStepLabel: { color: '#555561', fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.7 },
1795
+ dogfoodChoiceRow: { flexDirection: 'row', gap: 7 },
1796
+ dogfoodChoice: { borderRadius: 9, borderWidth: 1, borderColor: '#d8d8e3', backgroundColor: '#fff', paddingHorizontal: 10, paddingVertical: 8 },
1797
+ dogfoodChoiceSelected: { borderColor: '#818cf8', backgroundColor: 'rgba(129,140,248,0.18)' },
1798
+ dogfoodChoiceText: { color: '#666671', fontSize: 12, fontWeight: '700' },
1799
+ dogfoodChoiceTextSelected: { color: '#5645d8' },
1800
+ dogfoodConsole: { maxHeight: 260, gap: 4, borderRadius: 11, padding: 10, backgroundColor: '#15151b' },
1801
+ dogfoodConsoleStatus: { color: '#a5b4fc', fontSize: 12, fontWeight: '800' },
1802
+ dogfoodConsoleLine: { color: '#d1d5db', fontSize: 10, lineHeight: 14, fontFamily: Platform.select({ ios: 'Menlo', android: 'monospace', default: 'monospace' }) },
1803
+ dogfoodConsoleError: { color: '#fca5a5', fontSize: 11, lineHeight: 16, marginTop: 5 },
1804
+ dogfoodOpenPreview: { alignSelf: 'flex-start', borderRadius: 9, paddingHorizontal: 11, paddingVertical: 8, marginTop: 6, backgroundColor: '#6555df' },
1805
+ dogfoodOpenPreviewText: { color: '#fff', fontSize: 12, fontWeight: '800' },
1806
+ reloadRow: {
1807
+ gap: 4,
1808
+ },
1809
+ reloadHint: {
1810
+ color: '#8b8b93',
1811
+ fontSize: 11,
1812
+ lineHeight: 15,
1813
+ paddingHorizontal: 4,
1814
+ },
1296
1815
  vibeInputRow: {
1297
1816
  backgroundColor: 'rgba(129,140,248,0.08)',
1298
1817
  borderColor: 'rgba(129,140,248,0.4)',
@@ -1302,7 +1821,7 @@ const styles = StyleSheet.create({
1302
1821
  gap: 10,
1303
1822
  },
1304
1823
  vibeInput: {
1305
- color: '#fff',
1824
+ color: '#24242b',
1306
1825
  fontSize: 15,
1307
1826
  minHeight: 64,
1308
1827
  textAlignVertical: 'top',
@@ -1353,7 +1872,7 @@ const styles = StyleSheet.create({
1353
1872
  justifyContent: 'flex-end',
1354
1873
  },
1355
1874
  modal: {
1356
- backgroundColor: '#141422',
1875
+ backgroundColor: '#f8f8fb',
1357
1876
  borderTopLeftRadius: 22,
1358
1877
  borderTopRightRadius: 22,
1359
1878
  padding: 22,
@@ -1377,7 +1896,7 @@ const styles = StyleSheet.create({
1377
1896
  title: {
1378
1897
  fontSize: 20,
1379
1898
  fontWeight: '700',
1380
- color: '#fff',
1899
+ color: '#17171d',
1381
1900
  },
1382
1901
  closeBtn: {
1383
1902
  width: 36,
@@ -1385,10 +1904,10 @@ const styles = StyleSheet.create({
1385
1904
  borderRadius: 18,
1386
1905
  alignItems: 'center',
1387
1906
  justifyContent: 'center',
1388
- backgroundColor: 'rgba(255,255,255,0.08)',
1907
+ backgroundColor: '#ededf3',
1389
1908
  },
1390
1909
  closeIcon: {
1391
- color: '#fff',
1910
+ color: '#656570',
1392
1911
  fontSize: 22,
1393
1912
  lineHeight: 24,
1394
1913
  fontWeight: '400',
@@ -1407,12 +1926,19 @@ const styles = StyleSheet.create({
1407
1926
  fontSize: 15,
1408
1927
  fontWeight: '700',
1409
1928
  },
1929
+ tabs: { flexDirection: 'row', gap: 6, padding: 3, borderRadius: 12, backgroundColor: '#ededf3' },
1930
+ tab: { flex: 1, minHeight: 36, borderRadius: 9, alignItems: 'center', justifyContent: 'center' },
1931
+ tabSelected: { backgroundColor: '#fff' },
1932
+ tabText: { color: '#858590', fontSize: 12, fontWeight: '700' },
1933
+ tabTextSelected: { color: '#6252e8' },
1934
+ tabContent: { gap: 12 },
1935
+ hidden: { display: 'none' },
1410
1936
  machineCard: {
1411
1937
  borderRadius: 14,
1412
1938
  borderWidth: 1,
1413
1939
  padding: 14,
1414
- backgroundColor: 'rgba(255,255,255,0.04)',
1415
- borderColor: 'rgba(255,255,255,0.12)',
1940
+ backgroundColor: '#fff',
1941
+ borderColor: '#e1e1e8',
1416
1942
  },
1417
1943
  machineCardLive: {
1418
1944
  backgroundColor: 'rgba(34,197,94,0.10)',
@@ -1426,6 +1952,10 @@ const styles = StyleSheet.create({
1426
1952
  backgroundColor: 'rgba(239,68,68,0.10)',
1427
1953
  borderColor: 'rgba(239,68,68,0.35)',
1428
1954
  },
1955
+ machineRoutes: { flexDirection: 'row', gap: 8 },
1956
+ machineRouteCard: { flex: 1, minWidth: 0, borderRadius: 11, padding: 10, gap: 3, backgroundColor: '#fff', borderWidth: 1, borderColor: '#e1e1e8' },
1957
+ machineRouteLabel: { color: '#7b7b86', fontSize: 10, fontWeight: '700', textTransform: 'uppercase' },
1958
+ machineRouteValue: { color: '#222229', fontSize: 12, fontWeight: '700' },
1429
1959
  machineHeader: {
1430
1960
  flexDirection: 'row',
1431
1961
  alignItems: 'center',
@@ -1453,7 +1983,7 @@ const styles = StyleSheet.create({
1453
1983
  backgroundColor: '#ef4444',
1454
1984
  },
1455
1985
  machineLabel: {
1456
- color: '#cbd5e1',
1986
+ color: '#73737e',
1457
1987
  fontSize: 12,
1458
1988
  fontWeight: '700',
1459
1989
  textTransform: 'uppercase',
@@ -1465,12 +1995,12 @@ const styles = StyleSheet.create({
1465
1995
  fontWeight: '700',
1466
1996
  },
1467
1997
  machineName: {
1468
- color: '#fff',
1998
+ color: '#222229',
1469
1999
  fontSize: 16,
1470
2000
  fontWeight: '700',
1471
2001
  },
1472
2002
  machineMeta: {
1473
- color: '#cbd5e1',
2003
+ color: '#73737e',
1474
2004
  fontSize: 12,
1475
2005
  marginTop: 4,
1476
2006
  lineHeight: 17,
@@ -1485,33 +2015,33 @@ const styles = StyleSheet.create({
1485
2015
  gap: 10,
1486
2016
  },
1487
2017
  runnerSectionTitle: {
1488
- color: '#f8fafc',
2018
+ color: '#222229',
1489
2019
  fontSize: 16,
1490
2020
  fontWeight: '700',
1491
2021
  },
1492
2022
  runnerSectionSubtitle: {
1493
2023
  marginTop: 2,
1494
- color: '#94a3b8',
2024
+ color: '#83838e',
1495
2025
  fontSize: 12,
1496
2026
  },
1497
2027
  runnerRefreshBtn: {
1498
2028
  borderRadius: 10,
1499
2029
  borderWidth: 1,
1500
- borderColor: 'rgba(148,163,184,0.22)',
1501
- backgroundColor: 'rgba(15,23,42,0.65)',
2030
+ borderColor: '#dedee7',
2031
+ backgroundColor: '#fff',
1502
2032
  paddingHorizontal: 10,
1503
2033
  paddingVertical: 8,
1504
2034
  },
1505
2035
  runnerRefreshBtnText: {
1506
- color: '#cbd5e1',
2036
+ color: '#5f5f69',
1507
2037
  fontSize: 12,
1508
2038
  fontWeight: '600',
1509
2039
  },
1510
2040
  runnerCard: {
1511
2041
  borderRadius: 12,
1512
2042
  borderWidth: 1,
1513
- borderColor: 'rgba(148,163,184,0.14)',
1514
- backgroundColor: 'rgba(15,23,42,0.45)',
2043
+ borderColor: '#e2e2e9',
2044
+ backgroundColor: '#fff',
1515
2045
  paddingHorizontal: 12,
1516
2046
  paddingVertical: 11,
1517
2047
  gap: 6,
@@ -1533,15 +2063,16 @@ const styles = StyleSheet.create({
1533
2063
  alignItems: 'center',
1534
2064
  gap: 10,
1535
2065
  },
2066
+ runnerCardActions: { flexDirection: 'row', alignItems: 'center', gap: 6 },
1536
2067
  runnerCardTitle: {
1537
- color: '#f8fafc',
2068
+ color: '#222229',
1538
2069
  fontSize: 14,
1539
2070
  fontWeight: '700',
1540
2071
  },
1541
2072
  runnerCardStatus: {
1542
2073
  marginTop: 2,
1543
2074
  fontSize: 12,
1544
- color: '#cbd5e1',
2075
+ color: '#666671',
1545
2076
  },
1546
2077
  runnerCardStatusOk: {
1547
2078
  color: '#86efac',
@@ -1553,7 +2084,7 @@ const styles = StyleSheet.create({
1553
2084
  color: '#fca5a5',
1554
2085
  },
1555
2086
  runnerCardDetail: {
1556
- color: '#94a3b8',
2087
+ color: '#858590',
1557
2088
  fontSize: 11,
1558
2089
  lineHeight: 16,
1559
2090
  },
@@ -1570,6 +2101,15 @@ const styles = StyleSheet.create({
1570
2101
  fontSize: 12,
1571
2102
  fontWeight: '700',
1572
2103
  },
2104
+ runnerActionBtnSelected: { borderColor: '#818cf8', backgroundColor: 'rgba(79,70,229,0.44)' },
2105
+ routingSummary: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingHorizontal: 2 },
2106
+ routingSummaryLabel: { color: '#7c7c87', fontSize: 11, fontWeight: '600' },
2107
+ routingSummaryValue: { flex: 1, color: '#6555df', fontSize: 12, fontWeight: '700', textAlign: 'right' },
2108
+ modelChoiceRow: { gap: 6, paddingTop: 2 },
2109
+ modelChoice: { borderRadius: 9, borderWidth: 1, borderColor: 'rgba(148,163,184,0.18)', paddingHorizontal: 9, paddingVertical: 6 },
2110
+ modelChoiceSelected: { borderColor: '#818cf8', backgroundColor: 'rgba(79,70,229,0.30)' },
2111
+ modelChoiceText: { color: '#73737e', fontSize: 11 },
2112
+ modelChoiceTextSelected: { color: '#5e4ce6', fontWeight: '700' },
1573
2113
  runnerSectionError: {
1574
2114
  color: '#fca5a5',
1575
2115
  fontSize: 12,
@@ -1581,7 +2121,7 @@ const styles = StyleSheet.create({
1581
2121
  progressTrack: {
1582
2122
  height: 6,
1583
2123
  borderRadius: 3,
1584
- backgroundColor: 'rgba(255,255,255,0.08)',
2124
+ backgroundColor: '#e5e5eb',
1585
2125
  overflow: 'hidden',
1586
2126
  marginTop: 4,
1587
2127
  },
@@ -1608,10 +2148,10 @@ const styles = StyleSheet.create({
1608
2148
  paddingVertical: 6,
1609
2149
  paddingHorizontal: 10,
1610
2150
  borderRadius: 10,
1611
- backgroundColor: 'rgba(255,255,255,0.05)',
2151
+ backgroundColor: '#ededf3',
1612
2152
  },
1613
2153
  quickIconToggleText: {
1614
- color: '#cbd5e1',
2154
+ color: '#5f5f69',
1615
2155
  fontSize: 12,
1616
2156
  fontWeight: '700',
1617
2157
  },
@@ -1623,25 +2163,25 @@ const styles = StyleSheet.create({
1623
2163
  padding: 12,
1624
2164
  },
1625
2165
  quickIconNoteText: {
1626
- color: '#fde68a',
2166
+ color: '#8a5a12',
1627
2167
  fontSize: 12,
1628
2168
  lineHeight: 17,
1629
2169
  },
1630
2170
  iconSelector: {
1631
2171
  borderRadius: 12,
1632
2172
  borderWidth: 1,
1633
- borderColor: 'rgba(255,255,255,0.09)',
1634
- backgroundColor: 'rgba(255,255,255,0.03)',
2173
+ borderColor: '#e1e1e8',
2174
+ backgroundColor: '#fff',
1635
2175
  padding: 12,
1636
2176
  gap: 10,
1637
2177
  },
1638
2178
  iconSelectorTitle: {
1639
- color: '#f8fafc',
2179
+ color: '#222229',
1640
2180
  fontSize: 13,
1641
2181
  fontWeight: '700',
1642
2182
  },
1643
2183
  iconSelectorText: {
1644
- color: '#cbd5e1',
2184
+ color: '#73737e',
1645
2185
  fontSize: 12,
1646
2186
  lineHeight: 17,
1647
2187
  },
@@ -1655,8 +2195,8 @@ const styles = StyleSheet.create({
1655
2195
  minWidth: 84,
1656
2196
  borderRadius: 12,
1657
2197
  borderWidth: 1,
1658
- borderColor: 'rgba(255,255,255,0.08)',
1659
- backgroundColor: 'rgba(255,255,255,0.02)',
2198
+ borderColor: '#e4e4ea',
2199
+ backgroundColor: '#f8f8fb',
1660
2200
  paddingVertical: 10,
1661
2201
  paddingHorizontal: 8,
1662
2202
  alignItems: 'center',
@@ -1683,7 +2223,7 @@ const styles = StyleSheet.create({
1683
2223
  fontWeight: '700',
1684
2224
  },
1685
2225
  iconOptionText: {
1686
- color: '#e2e8f0',
2226
+ color: '#555561',
1687
2227
  fontSize: 11,
1688
2228
  fontWeight: '600',
1689
2229
  },
@@ -1691,14 +2231,14 @@ const styles = StyleSheet.create({
1691
2231
  marginTop: 4,
1692
2232
  borderRadius: 14,
1693
2233
  borderWidth: 1,
1694
- borderColor: 'rgba(255,255,255,0.1)',
2234
+ borderColor: '#e1e1e8',
1695
2235
  paddingVertical: 15,
1696
2236
  alignItems: 'center',
1697
2237
  justifyContent: 'center',
1698
- backgroundColor: 'rgba(255,255,255,0.04)',
2238
+ backgroundColor: '#fff',
1699
2239
  },
1700
2240
  cancelBtnText: {
1701
- color: '#e5e7eb',
2241
+ color: '#555561',
1702
2242
  fontSize: 15,
1703
2243
  fontWeight: '700',
1704
2244
  },