yaver-feedback-react-native 0.9.2 → 0.9.4

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 (108) hide show
  1. package/README.md +102 -1
  2. package/dist/AuthOverlay.d.ts +1 -14
  3. package/dist/AuthOverlay.js +9 -62
  4. package/dist/Discovery.js +0 -6
  5. package/dist/DogfoodRuntime.d.ts +124 -0
  6. package/dist/DogfoodRuntime.js +273 -0
  7. package/dist/FeedbackModal.js +347 -61
  8. package/dist/LoginScreen.d.ts +1 -5
  9. package/dist/LoginScreen.js +2 -6
  10. package/dist/MachinePickerScreen.d.ts +1 -3
  11. package/dist/MachinePickerScreen.js +6 -18
  12. package/dist/P2PClient.d.ts +116 -3
  13. package/dist/P2PClient.js +273 -3
  14. package/dist/P2PDogfoodDriver.d.ts +12 -0
  15. package/dist/P2PDogfoodDriver.js +118 -0
  16. package/dist/PairDeviceModal.d.ts +2 -3
  17. package/dist/ShakeDetector.js +31 -17
  18. package/dist/VibeChatScreen.d.ts +11 -1
  19. package/dist/VibeChatScreen.js +200 -41
  20. package/dist/YaverFeedback.d.ts +34 -0
  21. package/dist/YaverFeedback.js +124 -19
  22. package/dist/YaverModeBadge.d.ts +22 -0
  23. package/dist/YaverModeBadge.js +219 -0
  24. package/dist/__tests__/AuthDevices.test.js +1 -48
  25. package/dist/__tests__/DogfoodRuntime.test.d.ts +1 -0
  26. package/dist/__tests__/DogfoodRuntime.test.js +116 -0
  27. package/dist/__tests__/P2PDogfoodDriver.test.d.ts +1 -0
  28. package/dist/__tests__/P2PDogfoodDriver.test.js +64 -0
  29. package/dist/__tests__/ReportIdentity.test.d.ts +25 -1
  30. package/dist/__tests__/ReportIdentity.test.js +34 -22
  31. package/dist/__tests__/ShakeDetectorWeb.test.d.ts +1 -0
  32. package/dist/__tests__/ShakeDetectorWeb.test.js +27 -0
  33. package/dist/__tests__/YaverFeedback.test.js +13 -3
  34. package/dist/__tests__/deviceDogfood.test.d.ts +1 -0
  35. package/dist/__tests__/deviceDogfood.test.js +86 -0
  36. package/dist/__tests__/dogfoodPolicy.test.d.ts +1 -0
  37. package/dist/__tests__/dogfoodPolicy.test.js +33 -0
  38. package/dist/_core/ansi.d.ts +117 -0
  39. package/dist/_core/ansi.js +468 -0
  40. package/dist/_core/ansi.test.d.ts +1 -0
  41. package/dist/_core/ansi.test.js +225 -0
  42. package/dist/_core/buildFeedbackPrompt.d.ts +4 -9
  43. package/dist/_core/buildFeedbackPrompt.js +18 -72
  44. package/dist/_core/constants.d.ts +19 -6
  45. package/dist/_core/constants.js +20 -7
  46. package/dist/_core/device.d.ts +9 -23
  47. package/dist/_core/device.js +13 -28
  48. package/dist/_core/endpoints.d.ts +0 -7
  49. package/dist/_core/endpoints.js +0 -7
  50. package/dist/_core/index.d.ts +4 -0
  51. package/dist/_core/index.js +4 -0
  52. package/dist/_core/remoteless.d.ts +44 -0
  53. package/dist/_core/remoteless.js +75 -0
  54. package/dist/_core/trace.d.ts +47 -0
  55. package/dist/_core/trace.js +38 -0
  56. package/dist/_core/trace.test.d.ts +1 -0
  57. package/dist/_core/trace.test.js +60 -0
  58. package/dist/auth.d.ts +3 -60
  59. package/dist/auth.js +6 -88
  60. package/dist/deviceDogfood.d.ts +53 -0
  61. package/dist/deviceDogfood.js +137 -0
  62. package/dist/dogfoodPolicy.d.ts +25 -0
  63. package/dist/dogfoodPolicy.js +24 -0
  64. package/dist/index.d.ts +13 -4
  65. package/dist/index.js +24 -8
  66. package/dist/reloadActions.js +2 -2
  67. package/dist/types.d.ts +50 -7
  68. package/package.json +12 -3
  69. package/src/AuthOverlay.tsx +20 -106
  70. package/src/Discovery.ts +0 -6
  71. package/src/DogfoodRuntime.ts +373 -0
  72. package/src/FeedbackModal.tsx +445 -67
  73. package/src/LoginScreen.tsx +2 -22
  74. package/src/MachinePickerScreen.tsx +6 -21
  75. package/src/P2PClient.ts +347 -4
  76. package/src/P2PDogfoodDriver.ts +132 -0
  77. package/src/PairDeviceModal.tsx +2 -3
  78. package/src/ShakeDetector.ts +31 -18
  79. package/src/VibeChatScreen.tsx +232 -42
  80. package/src/YaverFeedback.ts +133 -22
  81. package/src/YaverModeBadge.tsx +234 -0
  82. package/src/__tests__/AuthDevices.test.ts +1 -52
  83. package/src/__tests__/DogfoodRuntime.test.ts +135 -0
  84. package/src/__tests__/P2PDogfoodDriver.test.ts +80 -0
  85. package/src/__tests__/ReportIdentity.test.ts +36 -27
  86. package/src/__tests__/ShakeDetectorWeb.test.ts +30 -0
  87. package/src/__tests__/YaverFeedback.test.ts +15 -3
  88. package/src/__tests__/deviceDogfood.test.ts +77 -0
  89. package/src/__tests__/dogfoodPolicy.test.ts +34 -0
  90. package/src/_core/ansi.test.ts +250 -0
  91. package/src/_core/ansi.ts +475 -0
  92. package/src/_core/buildFeedbackPrompt.ts +18 -95
  93. package/src/_core/constants.ts +20 -6
  94. package/src/_core/device.ts +13 -30
  95. package/src/_core/endpoints.ts +0 -7
  96. package/src/_core/index.ts +4 -0
  97. package/src/_core/remoteless.ts +110 -0
  98. package/src/_core/trace.test.ts +58 -0
  99. package/src/_core/trace.ts +75 -0
  100. package/src/auth.ts +7 -156
  101. package/src/deviceDogfood.ts +171 -0
  102. package/src/dogfoodPolicy.ts +40 -0
  103. package/src/index.ts +40 -11
  104. package/src/reloadActions.ts +2 -2
  105. package/src/types.ts +45 -7
  106. package/dist/GuestOnboardingScreen.d.ts +0 -8
  107. package/dist/GuestOnboardingScreen.js +0 -282
  108. 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,
@@ -35,6 +36,7 @@ import {
35
36
  } from './types';
36
37
  import { AuthOverlay } from './AuthOverlay';
37
38
  import { QuickActionIcon } from './QuickActionIcon';
39
+ import { YaverModeBadge } from './YaverModeBadge';
38
40
  import { VibeChatScreen } from './VibeChatScreen';
39
41
  import { DeployPanel } from './DeployPanel';
40
42
  import { listReachableDevices, RemoteDevice } from './auth';
@@ -48,6 +50,14 @@ import {
48
50
  setPreferredModel,
49
51
  setPreferredRunner,
50
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';
51
61
 
52
62
  /**
53
63
  * Simplified feedback modal — launch scope is 3 actions:
@@ -91,6 +101,7 @@ type RunnerCardState = {
91
101
  detail?: string;
92
102
  actionLabel?: string;
93
103
  actionRunner?: string;
104
+ models?: Array<{ id: string; name?: string; provider?: string; isDefault?: boolean }>;
94
105
  };
95
106
 
96
107
  type ProviderEditorState = {
@@ -101,6 +112,14 @@ type ProviderEditorState = {
101
112
  apiKey: string;
102
113
  };
103
114
 
115
+ type DogfoodProjectChoice = {
116
+ name: string;
117
+ path: string;
118
+ framework?: string;
119
+ frameworks?: string[];
120
+ surfaces?: string[];
121
+ };
122
+
104
123
  const PRIMARY_RUNNER_IDS = ['claude', 'codex', 'opencode'] as const;
105
124
 
106
125
  function normalizeRunnerStatusRows(rows: RunnerAuthStatusRow[]): RunnerCardState[] {
@@ -142,6 +161,7 @@ function normalizeRunnerStatusRows(rows: RunnerAuthStatusRow[]): RunnerCardState
142
161
  version: row.version,
143
162
  tone: 'warning',
144
163
  statusLine: 'Not installed on the selected machine',
164
+ models: row.models,
145
165
  detail,
146
166
  };
147
167
  }
@@ -159,6 +179,7 @@ function normalizeRunnerStatusRows(rows: RunnerAuthStatusRow[]): RunnerCardState
159
179
  statusLine: configured
160
180
  ? `${versionPrefix}Configured on the selected machine`
161
181
  : `${versionPrefix}Needs provider config on the selected machine`,
182
+ models: row.models,
162
183
  detail,
163
184
  };
164
185
  }
@@ -175,6 +196,7 @@ function normalizeRunnerStatusRows(rows: RunnerAuthStatusRow[]): RunnerCardState
175
196
  statusLine: authed
176
197
  ? `${versionPrefix}Signed in on the selected machine`
177
198
  : `${versionPrefix}Not signed in on the selected machine`,
199
+ models: row.models,
178
200
  detail,
179
201
  actionLabel: authed ? 'Re-auth' : 'Sign in',
180
202
  actionRunner: id,
@@ -182,6 +204,36 @@ function normalizeRunnerStatusRows(rows: RunnerAuthStatusRow[]): RunnerCardState
182
204
  });
183
205
  }
184
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
+
185
237
  export const FeedbackModal: React.FC = () => {
186
238
  const { width: winW, height: winH } = useWindowDimensions();
187
239
  const isTablet = Math.min(winW, winH) >= 600;
@@ -189,6 +241,7 @@ export const FeedbackModal: React.FC = () => {
189
241
  // looks empty on a 1024pt iPad. Mobile keeps 3-col.
190
242
  const iconOptionWidthOverride = isTablet ? '18%' : undefined;
191
243
  const [visible, setVisible] = useState(false);
244
+ const [dogfoodActive, setDogfoodActive] = useState(() => YaverFeedback.getDogfoodStatus().active);
192
245
  const [action, setAction] = useState<ActionState>('idle');
193
246
  const [error, setError] = useState<string | null>(null);
194
247
  const [toast, setToast] = useState<string | null>(null);
@@ -231,9 +284,86 @@ export const FeedbackModal: React.FC = () => {
231
284
  const [runnerStatusError, setRunnerStatusError] = useState<string | null>(null);
232
285
  const [preferredRunner, setPreferredRunnerState] = useState<string | null>(null);
233
286
  const [preferredModel, setPreferredModelState] = useState('');
287
+ const [activeTab, setActiveTab] = useState<'chat' | 'settings'>('chat');
234
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);
235
302
  const mountedRef = useRef(true);
236
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
+
237
367
  /**
238
368
  * Ask the machine what its dev server is doing, so the reload actions can
239
369
  * be enabled/disabled against reality rather than against a guess.
@@ -291,9 +421,8 @@ export const FeedbackModal: React.FC = () => {
291
421
 
292
422
  try {
293
423
  const devices = await listReachableDevices(cfg.authToken);
294
- const all = [...devices.owned, ...devices.shared];
295
424
  const device =
296
- all.find((candidate) => candidate.deviceId === cfg.preferredDeviceId) ?? null;
425
+ devices.owned.find((candidate) => candidate.deviceId === cfg.preferredDeviceId) ?? null;
297
426
 
298
427
  if (!device) {
299
428
  if (mountedRef.current) {
@@ -315,9 +444,8 @@ export const FeedbackModal: React.FC = () => {
315
444
  reachable = await client.health();
316
445
  }
317
446
 
318
- const hostHint = device.hostEmail ? ` via ${device.hostEmail}` : '';
319
447
  let status: MachineCardState['status'] = 'live';
320
- let detail = `${device.platform}${hostHint}`;
448
+ let detail = device.platform;
321
449
 
322
450
  if (!device.isOnline) {
323
451
  status = 'offline';
@@ -390,7 +518,7 @@ export const FeedbackModal: React.FC = () => {
390
518
  if (!client) {
391
519
  throw new Error('Not connected to the selected machine yet.');
392
520
  }
393
- const rows = await client.getRunnerAuthStatus();
521
+ const rows = await client.getAvailableRunners();
394
522
  if (mountedRef.current) {
395
523
  setRunnerCards(normalizeRunnerStatusRows(rows));
396
524
  }
@@ -424,13 +552,20 @@ export const FeedbackModal: React.FC = () => {
424
552
  mountedRef.current = true;
425
553
  const sub = DeviceEventEmitter.addListener('yaverFeedback:startReport', () => {
426
554
  if (YaverFeedback.isEnabled()) {
555
+ const onboarding = YaverFeedback.getDogfoodOnboarding();
556
+ const directDogfood = YaverFeedback.getDogfoodStatus().active;
557
+ setDogfoodActive(directDogfood);
427
558
  setVisible(true);
428
559
  setError(null);
429
560
  setToast(null);
430
561
  setProgress(null);
431
562
  setAction('idle');
432
- setShowVibeInput(false);
563
+ setShowVibeInput(directDogfood);
433
564
  setVibePrompt('');
565
+ if (onboarding) {
566
+ setActiveTab('settings');
567
+ void loadDogfoodOnboarding();
568
+ }
434
569
  // Re-read the "user hid the quick icon" flag on every open so
435
570
  // the re-enable row reflects the latest preference (the user
436
571
  // might have hidden or shown it between opens).
@@ -448,6 +583,14 @@ export const FeedbackModal: React.FC = () => {
448
583
  void loadRunnerStatuses();
449
584
  }
450
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
+ );
451
594
  // Agent streams build / compile progress through the BlackBox
452
595
  // SSE command channel as `command: "status"`; YaverFeedback re-emits
453
596
  // it as `yaverFeedback:status`. Show the most recent message in the
@@ -472,9 +615,10 @@ export const FeedbackModal: React.FC = () => {
472
615
  return () => {
473
616
  mountedRef.current = false;
474
617
  sub.remove();
618
+ dogfoodSub.remove();
475
619
  statusSub.remove();
476
620
  };
477
- }, [loadRunnerStatuses, loadSelectedMachine]);
621
+ }, [loadDogfoodOnboarding, loadRunnerStatuses, loadSelectedMachine]);
478
622
 
479
623
  useEffect(() => {
480
624
  if (!visible) return;
@@ -511,6 +655,12 @@ export const FeedbackModal: React.FC = () => {
511
655
  };
512
656
  }, [visible]);
513
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
+
514
664
  const closeSoon = useCallback((delayMs = 1200) => {
515
665
  setTimeout(() => {
516
666
  if (mountedRef.current) setVisible(false);
@@ -526,6 +676,9 @@ export const FeedbackModal: React.FC = () => {
526
676
  setShowVibeInput(false);
527
677
  setVibePrompt('');
528
678
  setRunnerStatusError(null);
679
+ void dogfoodControllerRef.current?.stop().catch(() => {});
680
+ dogfoodControllerRef.current = null;
681
+ YaverFeedback.clearDogfoodOnboarding();
529
682
  }, []);
530
683
 
531
684
  // Helper: run a P2P call; on network failure, ask YaverFeedback to
@@ -629,8 +782,10 @@ export const FeedbackModal: React.FC = () => {
629
782
  }
630
783
 
631
784
  let ackMessage = `${reloadAction.label} requested.`;
632
- await runWithReconnect(async (client) => {
633
- const ack = await client.reloadWithMode(reloadAction.mode, devSnapshot);
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);
634
789
  ackMessage = ack.message;
635
790
  setToast(ack.message);
636
791
  setProgress(0.2);
@@ -685,8 +840,10 @@ export const FeedbackModal: React.FC = () => {
685
840
  // map this app to its MobileProject scan entry without needing
686
841
  // `yaver dev start` to have been run.
687
842
  let ackMessage = 'Reload request acknowledged.';
688
- await runWithReconnect(async (client) => {
689
- 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');
690
847
  ackMessage = ack.message;
691
848
  setToast(ack.message);
692
849
  setProgress(0.2);
@@ -875,6 +1032,7 @@ export const FeedbackModal: React.FC = () => {
875
1032
  taskId: string;
876
1033
  initialPrompt: string;
877
1034
  project?: string;
1035
+ projectPath?: string;
878
1036
  runner?: string;
879
1037
  model?: string;
880
1038
  } | null>(null);
@@ -943,6 +1101,7 @@ export const FeedbackModal: React.FC = () => {
943
1101
  taskId: result.taskId,
944
1102
  initialPrompt: promptText,
945
1103
  project: identity.projectName,
1104
+ projectPath: identity.projectPath,
946
1105
  runner: preferredRunner ?? undefined,
947
1106
  model: preferredModel ?? undefined,
948
1107
  });
@@ -962,6 +1121,7 @@ export const FeedbackModal: React.FC = () => {
962
1121
  */
963
1122
 
964
1123
  const busy = action !== 'idle';
1124
+ const machineRouting = YaverFeedback.getMachineRouting();
965
1125
  const readyRunnerCount = runnerCards.filter((row) => row.ready || row.authConfigured).length;
966
1126
  const missingRunnerCount = runnerCards.filter((row) => !row.installed).length;
967
1127
  const needsAuthRunnerCount = runnerCards.filter(
@@ -972,17 +1132,18 @@ export const FeedbackModal: React.FC = () => {
972
1132
  // for the live chat screen. The chat manages its own SSE
973
1133
  // subscription, multi-turn follow-ups, and Reload button. Closing
974
1134
  // the chat returns to idle and clears the active vibe.
975
- if (visible && activeVibe) {
1135
+ if (activeVibe) {
976
1136
  const client = YaverFeedback.getP2PClient();
977
1137
  return (
978
1138
  <>
979
1139
  <AuthOverlay />
980
1140
  <QuickActionIcon />
1141
+ <YaverModeBadgeGate />
981
1142
  <Modal
982
1143
  visible={visible}
983
1144
  animationType="slide"
984
1145
  transparent
985
- onRequestClose={() => setActiveVibe(null)}
1146
+ onRequestClose={handleClose}
986
1147
  >
987
1148
  {client ? (
988
1149
  <VibeChatScreen
@@ -990,11 +1151,20 @@ export const FeedbackModal: React.FC = () => {
990
1151
  initialTaskId={activeVibe.taskId}
991
1152
  initialUserPrompt={activeVibe.initialPrompt}
992
1153
  project={activeVibe.project}
1154
+ projectPath={activeVibe.projectPath}
993
1155
  runner={activeVibe.runner}
994
1156
  model={activeVibe.model}
1157
+ voiceInputEnabled={YaverFeedback.getConfig()?.voiceInputEnabled === true}
995
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}
996
1166
  onReload={async () => {
997
- const c = YaverFeedback.getP2PClient();
1167
+ const c = YaverFeedback.getRenderP2PClient();
998
1168
  if (!c) throw new Error('Not connected');
999
1169
  await c.reloadApp();
1000
1170
  }}
@@ -1009,6 +1179,7 @@ export const FeedbackModal: React.FC = () => {
1009
1179
  <>
1010
1180
  <AuthOverlay />
1011
1181
  <QuickActionIcon />
1182
+ <YaverModeBadgeGate />
1012
1183
  {visible && (
1013
1184
  <Modal
1014
1185
  visible={visible}
@@ -1045,6 +1216,7 @@ export const FeedbackModal: React.FC = () => {
1045
1216
  }}
1046
1217
  >
1047
1218
  <ScrollView
1219
+ ref={scrollRef}
1048
1220
  style={styles.scroll}
1049
1221
  contentContainerStyle={[
1050
1222
  styles.scrollContent,
@@ -1054,9 +1226,13 @@ export const FeedbackModal: React.FC = () => {
1054
1226
  ]}
1055
1227
  keyboardShouldPersistTaps="handled"
1056
1228
  keyboardDismissMode={Platform.OS === 'ios' ? 'interactive' : 'on-drag'}
1229
+ contentInsetAdjustmentBehavior="always"
1230
+ automaticallyAdjustKeyboardInsets={Platform.OS === 'ios'}
1057
1231
  >
1058
1232
  <View style={styles.header}>
1059
- <Text style={styles.title}>Send Feedback</Text>
1233
+ <Text style={styles.title}>
1234
+ {dogfoodActive ? `${YaverFeedback.getDogfoodStatus().label || 'App'} Developer Mode` : 'Send Feedback'}
1235
+ </Text>
1060
1236
  <Pressable
1061
1237
  onPress={handleClose}
1062
1238
  hitSlop={12}
@@ -1068,6 +1244,114 @@ export const FeedbackModal: React.FC = () => {
1068
1244
  </Pressable>
1069
1245
  </View>
1070
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}
1071
1355
  <Pressable
1072
1356
  onPress={() => {
1073
1357
  if (!YaverFeedback.isAuthed()) {
@@ -1093,7 +1377,7 @@ export const FeedbackModal: React.FC = () => {
1093
1377
  machineCard.status === 'offline' && styles.machineDotOffline,
1094
1378
  ]}
1095
1379
  />
1096
- <Text style={styles.machineLabel}>Selected Machine</Text>
1380
+ <Text style={styles.machineLabel}>Coding Machine</Text>
1097
1381
  </View>
1098
1382
  <Text style={styles.machineAction}>
1099
1383
  {machineCard.loading ? 'Refreshing…' : 'Change'}
@@ -1105,6 +1389,17 @@ export const FeedbackModal: React.FC = () => {
1105
1389
  <Text style={styles.machineMeta}>{machineCard.detail}</Text>
1106
1390
  </Pressable>
1107
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
+
1108
1403
  <View style={styles.runnerSection}>
1109
1404
  <View style={styles.runnerSectionHeader}>
1110
1405
  <View style={{ flex: 1 }}>
@@ -1130,6 +1425,13 @@ export const FeedbackModal: React.FC = () => {
1130
1425
  </Pressable>
1131
1426
  </View>
1132
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
+
1133
1435
  {runnerCards.map((row) => (
1134
1436
  <View
1135
1437
  key={row.id}
@@ -1154,20 +1456,53 @@ export const FeedbackModal: React.FC = () => {
1154
1456
  {row.statusLine}
1155
1457
  </Text>
1156
1458
  </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}
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>
1170
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}
1171
1506
  {row.detail ? (
1172
1507
  <Text style={styles.runnerCardDetail}>{row.detail}</Text>
1173
1508
  ) : null}
@@ -1278,7 +1613,10 @@ export const FeedbackModal: React.FC = () => {
1278
1613
  </Text>
1279
1614
  </View>
1280
1615
  ))}
1616
+ </>
1617
+ </View>
1281
1618
 
1619
+ <View style={[styles.tabContent, activeTab !== 'chat' && styles.hidden]}>
1282
1620
  {/* 3. Vibing — expands to an input box on first tap
1283
1621
  so the user says WHAT they want to vibe on, just
1284
1622
  like the Yaver mobile app's Vibing tab. Second
@@ -1339,13 +1677,13 @@ export const FeedbackModal: React.FC = () => {
1339
1677
  )}
1340
1678
 
1341
1679
  {/* Screenshot & Fix */}
1342
- <ActionRow
1680
+ {!dogfoodActive ? <ActionRow
1343
1681
  label={action === 'capturing' ? 'Working…' : 'Screenshot & Fix'}
1344
1682
  tint="#22c55e"
1345
1683
  onPress={handleScreenshotAndFix}
1346
1684
  disabled={busy}
1347
1685
  busy={action === 'capturing'}
1348
- />
1686
+ /> : null}
1349
1687
 
1350
1688
  {/* Deploy — opens an inline panel that talks to
1351
1689
  /fleet/deploy-options on the agent and lets the user
@@ -1353,7 +1691,7 @@ export const FeedbackModal: React.FC = () => {
1353
1691
  it on. Capabilities (e.g. "Linux can't TestFlight")
1354
1692
  come from the agent's doctor probes — no client-side
1355
1693
  platform smarts here. */}
1356
- {!showDeploy ? (
1694
+ {!dogfoodActive && (!showDeploy ? (
1357
1695
  <ActionRow
1358
1696
  label="Deploy"
1359
1697
  tint="#7f8cf7"
@@ -1362,7 +1700,8 @@ export const FeedbackModal: React.FC = () => {
1362
1700
  />
1363
1701
  ) : (
1364
1702
  <DeployPanel onClose={() => setShowDeploy(false)} />
1365
- )}
1703
+ ))}
1704
+ </View>
1366
1705
 
1367
1706
  {progress !== null && (
1368
1707
  <View style={styles.progressTrack}>
@@ -1446,6 +1785,24 @@ const ActionRow: React.FC<ActionRowProps> = ({
1446
1785
  );
1447
1786
 
1448
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' },
1449
1806
  reloadRow: {
1450
1807
  gap: 4,
1451
1808
  },
@@ -1464,7 +1821,7 @@ const styles = StyleSheet.create({
1464
1821
  gap: 10,
1465
1822
  },
1466
1823
  vibeInput: {
1467
- color: '#fff',
1824
+ color: '#24242b',
1468
1825
  fontSize: 15,
1469
1826
  minHeight: 64,
1470
1827
  textAlignVertical: 'top',
@@ -1515,7 +1872,7 @@ const styles = StyleSheet.create({
1515
1872
  justifyContent: 'flex-end',
1516
1873
  },
1517
1874
  modal: {
1518
- backgroundColor: '#141422',
1875
+ backgroundColor: '#f8f8fb',
1519
1876
  borderTopLeftRadius: 22,
1520
1877
  borderTopRightRadius: 22,
1521
1878
  padding: 22,
@@ -1539,7 +1896,7 @@ const styles = StyleSheet.create({
1539
1896
  title: {
1540
1897
  fontSize: 20,
1541
1898
  fontWeight: '700',
1542
- color: '#fff',
1899
+ color: '#17171d',
1543
1900
  },
1544
1901
  closeBtn: {
1545
1902
  width: 36,
@@ -1547,10 +1904,10 @@ const styles = StyleSheet.create({
1547
1904
  borderRadius: 18,
1548
1905
  alignItems: 'center',
1549
1906
  justifyContent: 'center',
1550
- backgroundColor: 'rgba(255,255,255,0.08)',
1907
+ backgroundColor: '#ededf3',
1551
1908
  },
1552
1909
  closeIcon: {
1553
- color: '#fff',
1910
+ color: '#656570',
1554
1911
  fontSize: 22,
1555
1912
  lineHeight: 24,
1556
1913
  fontWeight: '400',
@@ -1569,12 +1926,19 @@ const styles = StyleSheet.create({
1569
1926
  fontSize: 15,
1570
1927
  fontWeight: '700',
1571
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' },
1572
1936
  machineCard: {
1573
1937
  borderRadius: 14,
1574
1938
  borderWidth: 1,
1575
1939
  padding: 14,
1576
- backgroundColor: 'rgba(255,255,255,0.04)',
1577
- borderColor: 'rgba(255,255,255,0.12)',
1940
+ backgroundColor: '#fff',
1941
+ borderColor: '#e1e1e8',
1578
1942
  },
1579
1943
  machineCardLive: {
1580
1944
  backgroundColor: 'rgba(34,197,94,0.10)',
@@ -1588,6 +1952,10 @@ const styles = StyleSheet.create({
1588
1952
  backgroundColor: 'rgba(239,68,68,0.10)',
1589
1953
  borderColor: 'rgba(239,68,68,0.35)',
1590
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' },
1591
1959
  machineHeader: {
1592
1960
  flexDirection: 'row',
1593
1961
  alignItems: 'center',
@@ -1615,7 +1983,7 @@ const styles = StyleSheet.create({
1615
1983
  backgroundColor: '#ef4444',
1616
1984
  },
1617
1985
  machineLabel: {
1618
- color: '#cbd5e1',
1986
+ color: '#73737e',
1619
1987
  fontSize: 12,
1620
1988
  fontWeight: '700',
1621
1989
  textTransform: 'uppercase',
@@ -1627,12 +1995,12 @@ const styles = StyleSheet.create({
1627
1995
  fontWeight: '700',
1628
1996
  },
1629
1997
  machineName: {
1630
- color: '#fff',
1998
+ color: '#222229',
1631
1999
  fontSize: 16,
1632
2000
  fontWeight: '700',
1633
2001
  },
1634
2002
  machineMeta: {
1635
- color: '#cbd5e1',
2003
+ color: '#73737e',
1636
2004
  fontSize: 12,
1637
2005
  marginTop: 4,
1638
2006
  lineHeight: 17,
@@ -1647,33 +2015,33 @@ const styles = StyleSheet.create({
1647
2015
  gap: 10,
1648
2016
  },
1649
2017
  runnerSectionTitle: {
1650
- color: '#f8fafc',
2018
+ color: '#222229',
1651
2019
  fontSize: 16,
1652
2020
  fontWeight: '700',
1653
2021
  },
1654
2022
  runnerSectionSubtitle: {
1655
2023
  marginTop: 2,
1656
- color: '#94a3b8',
2024
+ color: '#83838e',
1657
2025
  fontSize: 12,
1658
2026
  },
1659
2027
  runnerRefreshBtn: {
1660
2028
  borderRadius: 10,
1661
2029
  borderWidth: 1,
1662
- borderColor: 'rgba(148,163,184,0.22)',
1663
- backgroundColor: 'rgba(15,23,42,0.65)',
2030
+ borderColor: '#dedee7',
2031
+ backgroundColor: '#fff',
1664
2032
  paddingHorizontal: 10,
1665
2033
  paddingVertical: 8,
1666
2034
  },
1667
2035
  runnerRefreshBtnText: {
1668
- color: '#cbd5e1',
2036
+ color: '#5f5f69',
1669
2037
  fontSize: 12,
1670
2038
  fontWeight: '600',
1671
2039
  },
1672
2040
  runnerCard: {
1673
2041
  borderRadius: 12,
1674
2042
  borderWidth: 1,
1675
- borderColor: 'rgba(148,163,184,0.14)',
1676
- backgroundColor: 'rgba(15,23,42,0.45)',
2043
+ borderColor: '#e2e2e9',
2044
+ backgroundColor: '#fff',
1677
2045
  paddingHorizontal: 12,
1678
2046
  paddingVertical: 11,
1679
2047
  gap: 6,
@@ -1695,15 +2063,16 @@ const styles = StyleSheet.create({
1695
2063
  alignItems: 'center',
1696
2064
  gap: 10,
1697
2065
  },
2066
+ runnerCardActions: { flexDirection: 'row', alignItems: 'center', gap: 6 },
1698
2067
  runnerCardTitle: {
1699
- color: '#f8fafc',
2068
+ color: '#222229',
1700
2069
  fontSize: 14,
1701
2070
  fontWeight: '700',
1702
2071
  },
1703
2072
  runnerCardStatus: {
1704
2073
  marginTop: 2,
1705
2074
  fontSize: 12,
1706
- color: '#cbd5e1',
2075
+ color: '#666671',
1707
2076
  },
1708
2077
  runnerCardStatusOk: {
1709
2078
  color: '#86efac',
@@ -1715,7 +2084,7 @@ const styles = StyleSheet.create({
1715
2084
  color: '#fca5a5',
1716
2085
  },
1717
2086
  runnerCardDetail: {
1718
- color: '#94a3b8',
2087
+ color: '#858590',
1719
2088
  fontSize: 11,
1720
2089
  lineHeight: 16,
1721
2090
  },
@@ -1732,6 +2101,15 @@ const styles = StyleSheet.create({
1732
2101
  fontSize: 12,
1733
2102
  fontWeight: '700',
1734
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' },
1735
2113
  runnerSectionError: {
1736
2114
  color: '#fca5a5',
1737
2115
  fontSize: 12,
@@ -1743,7 +2121,7 @@ const styles = StyleSheet.create({
1743
2121
  progressTrack: {
1744
2122
  height: 6,
1745
2123
  borderRadius: 3,
1746
- backgroundColor: 'rgba(255,255,255,0.08)',
2124
+ backgroundColor: '#e5e5eb',
1747
2125
  overflow: 'hidden',
1748
2126
  marginTop: 4,
1749
2127
  },
@@ -1770,10 +2148,10 @@ const styles = StyleSheet.create({
1770
2148
  paddingVertical: 6,
1771
2149
  paddingHorizontal: 10,
1772
2150
  borderRadius: 10,
1773
- backgroundColor: 'rgba(255,255,255,0.05)',
2151
+ backgroundColor: '#ededf3',
1774
2152
  },
1775
2153
  quickIconToggleText: {
1776
- color: '#cbd5e1',
2154
+ color: '#5f5f69',
1777
2155
  fontSize: 12,
1778
2156
  fontWeight: '700',
1779
2157
  },
@@ -1785,25 +2163,25 @@ const styles = StyleSheet.create({
1785
2163
  padding: 12,
1786
2164
  },
1787
2165
  quickIconNoteText: {
1788
- color: '#fde68a',
2166
+ color: '#8a5a12',
1789
2167
  fontSize: 12,
1790
2168
  lineHeight: 17,
1791
2169
  },
1792
2170
  iconSelector: {
1793
2171
  borderRadius: 12,
1794
2172
  borderWidth: 1,
1795
- borderColor: 'rgba(255,255,255,0.09)',
1796
- backgroundColor: 'rgba(255,255,255,0.03)',
2173
+ borderColor: '#e1e1e8',
2174
+ backgroundColor: '#fff',
1797
2175
  padding: 12,
1798
2176
  gap: 10,
1799
2177
  },
1800
2178
  iconSelectorTitle: {
1801
- color: '#f8fafc',
2179
+ color: '#222229',
1802
2180
  fontSize: 13,
1803
2181
  fontWeight: '700',
1804
2182
  },
1805
2183
  iconSelectorText: {
1806
- color: '#cbd5e1',
2184
+ color: '#73737e',
1807
2185
  fontSize: 12,
1808
2186
  lineHeight: 17,
1809
2187
  },
@@ -1817,8 +2195,8 @@ const styles = StyleSheet.create({
1817
2195
  minWidth: 84,
1818
2196
  borderRadius: 12,
1819
2197
  borderWidth: 1,
1820
- borderColor: 'rgba(255,255,255,0.08)',
1821
- backgroundColor: 'rgba(255,255,255,0.02)',
2198
+ borderColor: '#e4e4ea',
2199
+ backgroundColor: '#f8f8fb',
1822
2200
  paddingVertical: 10,
1823
2201
  paddingHorizontal: 8,
1824
2202
  alignItems: 'center',
@@ -1845,7 +2223,7 @@ const styles = StyleSheet.create({
1845
2223
  fontWeight: '700',
1846
2224
  },
1847
2225
  iconOptionText: {
1848
- color: '#e2e8f0',
2226
+ color: '#555561',
1849
2227
  fontSize: 11,
1850
2228
  fontWeight: '600',
1851
2229
  },
@@ -1853,14 +2231,14 @@ const styles = StyleSheet.create({
1853
2231
  marginTop: 4,
1854
2232
  borderRadius: 14,
1855
2233
  borderWidth: 1,
1856
- borderColor: 'rgba(255,255,255,0.1)',
2234
+ borderColor: '#e1e1e8',
1857
2235
  paddingVertical: 15,
1858
2236
  alignItems: 'center',
1859
2237
  justifyContent: 'center',
1860
- backgroundColor: 'rgba(255,255,255,0.04)',
2238
+ backgroundColor: '#fff',
1861
2239
  },
1862
2240
  cancelBtnText: {
1863
- color: '#e5e7eb',
2241
+ color: '#555561',
1864
2242
  fontSize: 15,
1865
2243
  fontWeight: '700',
1866
2244
  },