yaver-feedback-react-native 0.9.9 → 0.9.10

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.
@@ -2,8 +2,6 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import {
3
3
  ActivityIndicator,
4
4
  DeviceEventEmitter,
5
- Keyboard,
6
- KeyboardAvoidingView,
7
5
  Linking,
8
6
  Modal,
9
7
  Platform,
@@ -17,19 +15,6 @@ import {
17
15
  } from 'react-native';
18
16
  import { YaverFeedback } from './YaverFeedback';
19
17
  import {
20
- captureScreenshot,
21
- // Launch scope for the feedback test SDK is intentionally smaller for now.
22
- // Keep the dormant file-upload and screen-recording helpers nearby, but
23
- // comment them out until we bring them back with stronger test coverage.
24
- // pickFeedbackFile,
25
- // startVideoRecording,
26
- // stopVideoRecording,
27
- } from './capture';
28
- import { uploadFeedback } from './upload';
29
- import { resolveReportIdentity } from './P2PClient';
30
- import {
31
- DeviceInfo,
32
- FeedbackBundle,
33
18
  OpenCodeConfigSummary,
34
19
  OpenCodeProviderSummary,
35
20
  RunnerAuthStatusRow,
@@ -39,7 +24,6 @@ import { QuickActionIcon } from './QuickActionIcon';
39
24
  import { YaverModeBadge } from './YaverModeBadge';
40
25
  import { VibeChatScreen } from './VibeChatScreen';
41
26
  import { DogfoodQuickControls } from './DogfoodQuickControls';
42
- import { DeployPanel } from './DeployPanel';
43
27
  import { listReachableDevices, RemoteDevice } from './auth';
44
28
  import { reloadActions } from './reloadActions';
45
29
  import type { DevServerSnapshot, ReloadAction } from './reloadActions';
@@ -62,24 +46,21 @@ import {
62
46
  } from './DogfoodRuntime';
63
47
  import { createP2PDogfoodDriver } from './P2PDogfoodDriver';
64
48
  import { DogfoodLanePicker, DogfoodLiveConsole, DogfoodStatusRail } from './DogfoodSessionUi';
49
+ import type { DogfoodRemoteRuntimeTarget } from './P2PClient';
50
+ import {
51
+ FEEDBACK_DOGFOOD_CONSOLE_COLORS,
52
+ FEEDBACK_DOGFOOD_LIGHT_COLORS,
53
+ } from './FeedbackModalTheme';
65
54
 
66
55
  /**
67
- * Simplified feedback modal launch scope is 3 actions:
68
- *
69
- * 1. Hot Reload — instant JS reload (most common use case)
70
- * 2. Vibing — open a vibing session on the agent
71
- * 3. Screenshot & Fix — capture the underlying app (modal hidden
72
- * during capture), upload it, and trigger
73
- * the fix loop
74
- *
75
- * The footer also has an explicit Cancel button so the icon tap path
76
- * feels like a standard action sheet rather than a hidden modal.
56
+ * Feedback modal with one conversational control surface. Authenticated users
57
+ * land directly in Chat; screenshots, fixes, and deploy intent are expressed
58
+ * in that conversation and handled by the connected agent/MCP toolchain.
77
59
  */
78
60
 
79
61
  type ActionState =
80
62
  | 'idle'
81
63
  | 'hot-reloading'
82
- | 'capturing'
83
64
  | 'vibing';
84
65
 
85
66
  type MachineCardState = {
@@ -122,8 +103,20 @@ type DogfoodProjectChoice = {
122
103
  framework?: string;
123
104
  frameworks?: string[];
124
105
  surfaces?: string[];
106
+ branch?: string;
107
+ gitRemote?: string;
125
108
  };
126
109
 
110
+ type DogfoodSetupStage = 'setup' | 'lane' | 'runtime';
111
+ type DogfoodExpandedStep = 'runner' | 'checkout' | null;
112
+
113
+ function dogfoodCheckoutDetail(project: DogfoodProjectChoice | null): string {
114
+ if (!project) return 'Choose a Git checkout on the selected machine';
115
+ return [project.path, project.branch ? `branch ${project.branch}` : project.gitRemote ? 'Git checkout' : 'checkout']
116
+ .filter(Boolean)
117
+ .join(' · ');
118
+ }
119
+
127
120
  const PRIMARY_RUNNER_IDS = ['claude', 'codex', 'opencode'] as const;
128
121
 
129
122
  function normalizeRunnerStatusRows(rows: RunnerAuthStatusRow[]): RunnerCardState[] {
@@ -261,18 +254,13 @@ export const FeedbackModal: React.FC = () => {
261
254
  // Which reload action is in flight, so only that button spins.
262
255
  const [reloadingId, setReloadingId] = useState<string | null>(null);
263
256
  const [runnerAuthModal, setRunnerAuthModal] = useState<string | null>(null);
264
- // Vibing-input mode: same expand-on-tap pattern as email login.
265
- // Tap "Vibing" once the button reveals an input + Send; that lets
266
- // the user say WHAT they want to vibe on instead of firing a canned
267
- // "pick something for me" prompt (which in 0.7.13 pointed Claude at
268
- // the wrong project because the matcher grepped the prompt itself).
257
+ // Signed-in users get the composer immediately. Signed-out users still get
258
+ // an explicit entry action so auth/setup failures remain named and visible.
269
259
  const [showVibeInput, setShowVibeInput] = useState(false);
270
- const [showDeploy, setShowDeploy] = useState(false);
271
260
  const [vibePrompt, setVibePrompt] = useState('');
272
261
  const [lastVibeTaskId, setLastVibeTaskId] = useState<string | null>(null);
273
262
  const [quickIconColorPreset, setQuickIconColorPreset] =
274
263
  useState<QuickIconColorPreset | null>(null);
275
- const [keyboardInset, setKeyboardInset] = useState(0);
276
264
  const [machineCard, setMachineCard] = useState<MachineCardState>({
277
265
  device: null,
278
266
  reachable: null,
@@ -300,6 +288,11 @@ export const FeedbackModal: React.FC = () => {
300
288
  const [dogfoodProject, setDogfoodProject] = useState<DogfoodProjectChoice | null>(null);
301
289
  const [dogfoodLane, setDogfoodLane] = useState<DogfoodLane>('browser');
302
290
  const [dogfoodNativeAvailable, setDogfoodNativeAvailable] = useState(false);
291
+ const [dogfoodBrowserAvailable, setDogfoodBrowserAvailable] = useState(false);
292
+ const [dogfoodNativeTargets, setDogfoodNativeTargets] = useState<DogfoodRemoteRuntimeTarget[]>([]);
293
+ const [dogfoodNativeTargetId, setDogfoodNativeTargetId] = useState('');
294
+ const [dogfoodSetupStage, setDogfoodSetupStage] = useState<DogfoodSetupStage>('setup');
295
+ const [dogfoodExpandedStep, setDogfoodExpandedStep] = useState<DogfoodExpandedStep>(null);
303
296
  const [dogfoodRuntime, setDogfoodRuntime] = useState<DogfoodSnapshot | null>(null);
304
297
  const [dogfoodSetupLoading, setDogfoodSetupLoading] = useState(false);
305
298
  const dogfoodControllerRef = useRef<DogfoodController | null>(null);
@@ -330,12 +323,25 @@ export const FeedbackModal: React.FC = () => {
330
323
  if (preferred) {
331
324
  const framework = preferred.framework || onboarding.framework || 'expo';
332
325
  const capabilities = await client.getDogfoodRemoteRuntimeCapabilities(preferred.path, framework).catch(() => null);
333
- const nativeRuntimeAvailable = !!capabilities?.targets.some((target) => target.enabled && target.id !== 'browser-window');
334
- if (mountedRef.current) setDogfoodNativeAvailable(nativeRuntimeAvailable);
326
+ const targets = capabilities?.targets || [];
327
+ const nativeTargets = targets.filter((target) => target.id !== 'browser-window');
328
+ const enabledNativeTargets = nativeTargets.filter((target) => target.enabled);
329
+ const nativeRuntimeAvailable = enabledNativeTargets.length > 0;
330
+ const browserRuntimeAvailable = targets.some((target) => target.enabled && target.id === 'browser-window');
331
+ if (mountedRef.current) {
332
+ setDogfoodNativeAvailable(nativeRuntimeAvailable);
333
+ setDogfoodBrowserAvailable(browserRuntimeAvailable);
334
+ setDogfoodNativeTargets(nativeTargets);
335
+ setDogfoodNativeTargetId((current) => enabledNativeTargets.some((target) => target.id === current)
336
+ ? current
337
+ : enabledNativeTargets[0]?.id || '');
338
+ }
335
339
  const savedLane = await getPreferredDogfoodLane(onboarding.appId);
336
- const savedSupported = dogfoodLaneOptions(framework, { nativeRuntimeAvailable })
340
+ const savedSupported = dogfoodLaneOptions(framework, { nativeRuntimeAvailable, browserRuntimeAvailable })
337
341
  .some((option) => option.lane === savedLane && option.supported);
338
- setDogfoodLane(savedLane && savedSupported ? savedLane : defaultDogfoodLane(framework));
342
+ setDogfoodLane(savedLane && savedSupported
343
+ ? savedLane
344
+ : defaultDogfoodLane(framework, { nativeRuntimeAvailable, browserRuntimeAvailable }));
339
345
  }
340
346
  } catch (cause) {
341
347
  if (mountedRef.current) setDogfoodEnrollment({ status: 'failed', error: cause instanceof Error ? cause.message : String(cause) });
@@ -347,12 +353,31 @@ export const FeedbackModal: React.FC = () => {
347
353
  useEffect(() => {
348
354
  let cancelled = false;
349
355
  setDogfoodNativeAvailable(false);
356
+ setDogfoodBrowserAvailable(false);
357
+ setDogfoodNativeTargets([]);
358
+ setDogfoodNativeTargetId('');
350
359
  const client = YaverFeedback.getP2PClient();
351
360
  if (!client || !dogfoodProject) return () => { cancelled = true; };
352
361
  const framework = dogfoodProject.framework || YaverFeedback.getDogfoodOnboarding()?.framework || 'expo';
353
362
  void client.getDogfoodRemoteRuntimeCapabilities(dogfoodProject.path, framework)
354
363
  .then((value) => {
355
- if (!cancelled) setDogfoodNativeAvailable(value.targets.some((target) => target.enabled && target.id !== 'browser-window'));
364
+ if (cancelled) return;
365
+ const nativeTargets = value.targets.filter((target) => target.id !== 'browser-window');
366
+ const enabledNativeTargets = nativeTargets.filter((target) => target.enabled);
367
+ setDogfoodNativeAvailable(enabledNativeTargets.length > 0);
368
+ setDogfoodBrowserAvailable(value.targets.some((target) => target.enabled && target.id === 'browser-window'));
369
+ setDogfoodNativeTargets(nativeTargets);
370
+ setDogfoodNativeTargetId((current) => enabledNativeTargets.some((target) => target.id === current)
371
+ ? current
372
+ : enabledNativeTargets[0]?.id || '');
373
+ const laneCapabilities = {
374
+ nativeRuntimeAvailable: enabledNativeTargets.length > 0,
375
+ browserRuntimeAvailable: value.targets.some((target) => target.enabled && target.id === 'browser-window'),
376
+ };
377
+ setDogfoodLane((current) => dogfoodLaneOptions(framework, laneCapabilities)
378
+ .some((option) => option.lane === current && option.supported)
379
+ ? current
380
+ : defaultDogfoodLane(framework, laneCapabilities));
356
381
  })
357
382
  .catch(() => {});
358
383
  return () => { cancelled = true; };
@@ -369,13 +394,15 @@ export const FeedbackModal: React.FC = () => {
369
394
  workDir: dogfoodProject.path,
370
395
  framework,
371
396
  lane: dogfoodLane,
397
+ nativeTargetId: dogfoodLane === 'webrtc' ? dogfoodNativeTargetId : undefined,
372
398
  }, createP2PDogfoodDriver(client), {
373
399
  onChange: (snapshot) => { if (mountedRef.current) setDogfoodRuntime(snapshot); },
374
400
  });
375
401
  dogfoodControllerRef.current = controller;
376
402
  setDogfoodRuntime(controller.snapshot());
403
+ setDogfoodSetupStage('runtime');
377
404
  await controller.trigger().catch(() => {});
378
- }, [dogfoodLane, dogfoodProject]);
405
+ }, [dogfoodLane, dogfoodNativeTargetId, dogfoodProject]);
379
406
 
380
407
  /**
381
408
  * Ask the machine what its dev server is doing, so the reload actions can
@@ -571,16 +598,20 @@ export const FeedbackModal: React.FC = () => {
571
598
  // user's feedback preference merely to open Developer Mode.
572
599
  if (YaverFeedback.isEnabled() || onboarding) {
573
600
  const directDogfood = YaverFeedback.getDogfoodStatus().active;
601
+ const authenticated = YaverFeedback.isAuthed();
574
602
  setDogfoodActive(directDogfood);
575
603
  setVisible(true);
576
604
  setError(null);
577
605
  setToast(null);
578
606
  setProgress(null);
579
607
  setAction('idle');
580
- setShowVibeInput(directDogfood);
608
+ setShowVibeInput(authenticated || directDogfood);
581
609
  setVibePrompt('');
582
610
  if (onboarding) {
583
- setActiveTab('settings');
611
+ setActiveTab(authenticated ? 'chat' : 'settings');
612
+ setDogfoodSetupStage('setup');
613
+ setDogfoodExpandedStep(null);
614
+ setDogfoodRuntime(null);
584
615
  void loadDogfoodOnboarding();
585
616
  }
586
617
  // Re-read the "user hid the quick icon" flag on every open so
@@ -652,31 +683,13 @@ export const FeedbackModal: React.FC = () => {
652
683
  return () => clearInterval(interval);
653
684
  }, [loadRunnerStatuses, loadSelectedMachine, refreshDevSnapshot, visible]);
654
685
 
655
- useEffect(() => {
656
- if (!visible) {
657
- setKeyboardInset(0);
658
- return;
659
- }
660
-
661
- const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
662
- const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide';
663
- const showSub = Keyboard.addListener(showEvent, (event) => {
664
- setKeyboardInset(event.endCoordinates?.height ?? 0);
665
- });
666
- const hideSub = Keyboard.addListener(hideEvent, () => {
667
- setKeyboardInset(0);
668
- });
669
- return () => {
670
- showSub.remove();
671
- hideSub.remove();
672
- };
673
- }, [visible]);
674
-
675
686
  useEffect(() => {
676
687
  if (!visible || !showVibeInput) return;
677
- const timer = setTimeout(() => scrollRef.current?.scrollToEnd({ animated: true }), keyboardInset > 0 ? 120 : 40);
688
+ // UIKit owns keyboard insets on the ScrollView. This one bounded scroll only
689
+ // reveals the newly mounted composer; it does not add a second inset.
690
+ const timer = setTimeout(() => scrollRef.current?.scrollToEnd({ animated: true }), 80);
678
691
  return () => clearTimeout(timer);
679
- }, [keyboardInset, showVibeInput, visible]);
692
+ }, [showVibeInput, visible]);
680
693
 
681
694
  const closeSoon = useCallback((delayMs = 1200) => {
682
695
  setTimeout(() => {
@@ -693,6 +706,9 @@ export const FeedbackModal: React.FC = () => {
693
706
  setShowVibeInput(false);
694
707
  setVibePrompt('');
695
708
  setRunnerStatusError(null);
709
+ setDogfoodSetupStage('setup');
710
+ setDogfoodExpandedStep(null);
711
+ setDogfoodRuntime(null);
696
712
  void dogfoodControllerRef.current?.stop().catch(() => {});
697
713
  dogfoodControllerRef.current = null;
698
714
  YaverFeedback.clearDogfoodOnboarding();
@@ -886,123 +902,7 @@ export const FeedbackModal: React.FC = () => {
886
902
  }
887
903
  }, [closeSoon, loadSelectedMachine, runWithReconnect]);
888
904
 
889
- const uploadBundleWithOptionalFix = useCallback(async (
890
- bundle: FeedbackBundle,
891
- fixOnUpload: boolean,
892
- successToast: string,
893
- failureToast?: string,
894
- ) => {
895
- const client = YaverFeedback.getP2PClient();
896
- const config = YaverFeedback.getConfig();
897
- if (!client || !config?.agentUrl) {
898
- setError('Not connected to the agent yet.');
899
- return;
900
- }
901
- try {
902
- const uploaded = await uploadFeedback(
903
- config.agentUrl,
904
- config.authToken ?? '',
905
- bundle,
906
- YaverFeedback.getRelayPassword(),
907
- );
908
- // The agent returns the new report id as `id` (see
909
- // feedback_http.go::ReceiveFeedback). Trigger the fix loop if we got
910
- // one back; otherwise just ack the upload.
911
- const reportId =
912
- (uploaded as { id?: string; reportId?: string } | null | undefined)?.id ??
913
- (uploaded as { reportId?: string } | null | undefined)?.reportId;
914
- if (reportId && fixOnUpload) {
915
- try {
916
- await client.triggerFix(reportId);
917
- setToast(successToast);
918
- } catch (err: unknown) {
919
- setToast(failureToast ?? 'Report uploaded — fix trigger failed');
920
- setError(err instanceof Error ? err.message : String(err));
921
- }
922
- } else {
923
- setToast(successToast);
924
- }
925
- closeSoon(1400);
926
- } catch (err: unknown) {
927
- setError(err instanceof Error ? err.message : String(err));
928
- }
929
- }, [closeSoon]);
930
-
931
- const handleScreenshotAndFix = useCallback(async () => {
932
- setAction('capturing');
933
- setError(null);
934
-
935
- setVisible(false);
936
- await new Promise((resolve) => setTimeout(resolve, 350));
937
-
938
- let path: string;
939
- try {
940
- path = await captureScreenshot();
941
- } catch (err: unknown) {
942
- setVisible(true);
943
- setError(err instanceof Error ? err.message : String(err));
944
- setAction('idle');
945
- return;
946
- }
947
-
948
- setVisible(true);
949
- await new Promise((resolve) => setTimeout(resolve, 150));
950
-
951
- try {
952
- const { Dimensions } = require('react-native');
953
- const { width, height } = Dimensions.get('window');
954
- const cfg = YaverFeedback.getConfig();
955
- const identity = resolveReportIdentity({
956
- projectName: cfg?.projectName,
957
- bundleId: cfg?.bundleId,
958
- surface: cfg?.surface,
959
- surfaces: cfg?.surfaces,
960
- stack: cfg?.stack,
961
- stacks: cfg?.stacks,
962
- testSurfaces: cfg?.testSurfaces,
963
- feedbackSdk: cfg?.feedbackSdk,
964
- feedbackTransport: cfg?.feedbackTransport,
965
- voiceCapabilities: cfg?.voiceCapabilities,
966
- sttProvider: cfg?.sttProvider,
967
- ttsProvider: cfg?.ttsProvider,
968
- });
969
- const deviceInfo: DeviceInfo = {
970
- platform: Platform.OS,
971
- osVersion: String(Platform.Version),
972
- model: Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
973
- screenWidth: width,
974
- screenHeight: height,
975
- appName: identity.appName,
976
- };
977
- const capturedErrors = YaverFeedback.getCapturedErrors();
978
- const bundle: FeedbackBundle = {
979
- metadata: {
980
- timestamp: new Date().toISOString(),
981
- deviceInfo,
982
- app: identity.app,
983
- project: identity.project,
984
- userNote: '[Screenshot + Fix]',
985
- },
986
- screenshots: [path],
987
- errors: capturedErrors.length > 0 ? capturedErrors : undefined,
988
- };
989
- await uploadBundleWithOptionalFix(
990
- bundle,
991
- true,
992
- 'Fix task started',
993
- );
994
- } finally {
995
- if (mountedRef.current) setAction('idle');
996
- }
997
- }, [uploadBundleWithOptionalFix]);
998
-
999
- /*
1000
- const handleFileUpload = useCallback(async () => {
1001
- ...
1002
- }, [uploadBundleWithOptionalFix]);
1003
- */
1004
-
1005
- // ─── 3. Vibing ─────────────────────────────────────────────────────
905
+ // ─── Chat ──────────────────────────────────────────────────────────
1006
906
  // First tap expands the input; second submit fires the actual
1007
907
  // /vibing/execute. Mirrors the Yaver mobile app's Vibing tab —
1008
908
  // user types what they want, hits Send, sees the task id back. If
@@ -1151,45 +1051,54 @@ export const FeedbackModal: React.FC = () => {
1151
1051
  && (selectedDogfoodRunner.ready || selectedDogfoodRunner.authConfigured);
1152
1052
  const dogfoodModelReady = !selectedDogfoodRunner?.models?.length
1153
1053
  || !!preferredModel && selectedDogfoodRunner.models.some((model) => model.id === preferredModel);
1154
- const dogfoodLaneChoices = dogfoodLaneOptions(
1155
- dogfoodProject?.framework || YaverFeedback.getDogfoodOnboarding()?.framework || 'expo',
1156
- { nativeRuntimeAvailable: dogfoodNativeAvailable },
1157
- );
1158
- const dogfoodLaneReady = dogfoodLaneChoices.some((option) => option.lane === dogfoodLane && option.supported);
1159
- const dogfoodReadinessSteps = [
1160
- {
1161
- key: 'oauth', label: 'Yaver OAuth',
1162
- detail: YaverFeedback.isAuthed() ? 'Signed in · session saved' : 'Sign in required',
1163
- tone: YaverFeedback.isAuthed() ? 'ready' as const : 'attention' as const,
1164
- },
1165
- {
1166
- key: 'machine', label: 'Remote PC',
1167
- detail: machineCard.device ? machineCard.title : 'Choose a reachable development machine',
1168
- tone: machineCard.device && machineCard.status === 'live' ? 'ready' as const : 'attention' as const,
1169
- },
1170
- {
1171
- key: 'installation', label: 'This installation',
1172
- detail: dogfoodEnrollment?.status === 'active' ? 'Device key approved' : dogfoodEnrollment?.status || 'Checking device key',
1173
- tone: dogfoodEnrollment?.status === 'active' ? 'ready' as const
1174
- : dogfoodEnrollment?.status === 'failed' ? 'blocked' as const : 'pending' as const,
1175
- },
1054
+ const dogfoodFramework = dogfoodProject?.framework || YaverFeedback.getDogfoodOnboarding()?.framework || 'expo';
1055
+ const dogfoodLaneChoices = dogfoodLaneOptions(dogfoodFramework, {
1056
+ nativeRuntimeAvailable: dogfoodNativeAvailable,
1057
+ browserRuntimeAvailable: dogfoodBrowserAvailable,
1058
+ });
1059
+ const selectedDogfoodNativeTarget = dogfoodNativeTargets.find((target) => target.id === dogfoodNativeTargetId) || null;
1060
+ const dogfoodLaneReady = dogfoodLaneChoices.some((option) => option.lane === dogfoodLane && option.supported)
1061
+ && (dogfoodLane !== 'webrtc' || !!selectedDogfoodNativeTarget?.enabled);
1062
+ const dogfoodMachineReady = !!machineCard.device && machineCard.status === 'live';
1063
+ const dogfoodSetupReady = dogfoodMachineReady && !!dogfoodProject && dogfoodRunnerReady && dogfoodModelReady;
1064
+ const dogfoodSetupSteps = [
1176
1065
  {
1177
- key: 'runner', label: 'Runner',
1178
- detail: dogfoodRunnerReady ? selectedDogfoodRunner?.name || preferredRunner || 'Ready' : 'Choose or configure a coding runner',
1179
- tone: dogfoodRunnerReady ? 'ready' as const : 'attention' as const,
1066
+ key: 'box',
1067
+ label: 'Remote box',
1068
+ detail: dogfoodMachineReady ? machineCard.title : 'Choose a reachable development machine',
1069
+ tone: dogfoodMachineReady ? 'ready' as const : 'attention' as const,
1070
+ actionLabel: machineCard.device ? 'Change' : 'Pick',
1071
+ onAction: () => YaverFeedback.showMachinePicker(),
1180
1072
  },
1181
1073
  {
1182
- key: 'model', label: 'Model',
1183
- detail: dogfoodModelReady ? preferredModel || 'Runner default' : 'Choose a model',
1184
- tone: dogfoodModelReady ? 'ready' as const : 'attention' as const,
1074
+ key: 'runner',
1075
+ label: 'Runner',
1076
+ detail: dogfoodRunnerReady
1077
+ ? [selectedDogfoodRunner?.name || preferredRunner || 'Ready', preferredModel || 'default model'].join(' · ')
1078
+ : 'Choose or configure a coding runner',
1079
+ tone: dogfoodRunnerReady && dogfoodModelReady ? 'ready' as const : 'attention' as const,
1080
+ actionLabel: dogfoodExpandedStep === 'runner' ? 'Done' : dogfoodRunnerReady ? 'Change' : 'Choose',
1081
+ expanded: dogfoodExpandedStep === 'runner',
1082
+ onAction: () => setDogfoodExpandedStep((current) => current === 'runner' ? null : 'runner'),
1185
1083
  },
1186
1084
  {
1187
- key: 'lane', label: 'Runtime lane',
1188
- detail: dogfoodLaneChoices.find((option) => option.lane === dogfoodLane)?.label || dogfoodLane,
1189
- tone: dogfoodLaneReady ? 'ready' as const : 'blocked' as const,
1085
+ key: 'checkout',
1086
+ label: 'Checkout',
1087
+ detail: dogfoodCheckoutDetail(dogfoodProject),
1088
+ tone: dogfoodProject ? 'ready' as const : 'attention' as const,
1089
+ actionLabel: dogfoodExpandedStep === 'checkout' ? 'Done' : dogfoodProject ? 'Change' : 'Choose',
1090
+ expanded: dogfoodExpandedStep === 'checkout',
1091
+ onAction: () => setDogfoodExpandedStep((current) => current === 'checkout' ? null : 'checkout'),
1190
1092
  },
1191
1093
  ];
1192
- const dogfoodStartBlocked = !dogfoodProject || !dogfoodRunnerReady || !dogfoodModelReady || !dogfoodLaneReady;
1094
+ const dogfoodStartBlocked = !dogfoodSetupReady || !dogfoodLaneReady;
1095
+ const dogfoodSourceLabel = dogfoodLane === 'webrtc'
1096
+ ? selectedDogfoodNativeTarget
1097
+ ? [selectedDogfoodNativeTarget.label, selectedDogfoodNativeTarget.platform].filter(Boolean).join(' · ')
1098
+ : 'Native simulator, emulator, or device'
1099
+ : dogfoodLane === 'hermes'
1100
+ ? `Hermes build · ${machineCard.title}`
1101
+ : `${dogfoodFramework === 'flutter' ? 'Flutter web compiler' : 'Metro / browser build'} · ${machineCard.title}`;
1193
1102
 
1194
1103
  // Once the user fires off a vibe task, swap the entire modal body
1195
1104
  // for the live chat screen. The chat manages its own SSE
@@ -1252,14 +1161,9 @@ export const FeedbackModal: React.FC = () => {
1252
1161
  transparent
1253
1162
  onRequestClose={handleClose}
1254
1163
  >
1255
- <Pressable style={styles.overlay} onPress={handleClose}>
1256
- <KeyboardAvoidingView
1257
- behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
1258
- keyboardVerticalOffset={Platform.OS === 'ios' ? 12 : 0}
1259
- style={styles.kbAvoider}
1260
- pointerEvents="box-none"
1261
- >
1262
- <Pressable
1164
+ <View style={styles.overlay}>
1165
+ <Pressable style={styles.backdrop} onPress={handleClose} accessibilityLabel="Close feedback" />
1166
+ <View
1263
1167
  // Tablet: cap modal width and center as a card-style
1264
1168
  // sheet rather than a phone bottom sheet that stretches
1265
1169
  // across a 12.9" iPad. Phone behaviour unchanged.
@@ -1275,20 +1179,11 @@ export const FeedbackModal: React.FC = () => {
1275
1179
  }
1276
1180
  : null,
1277
1181
  ]}
1278
- onPress={(e) => {
1279
- e.stopPropagation();
1280
- Keyboard.dismiss();
1281
- }}
1282
1182
  >
1283
1183
  <ScrollView
1284
1184
  ref={scrollRef}
1285
1185
  style={styles.scroll}
1286
- contentContainerStyle={[
1287
- styles.scrollContent,
1288
- showVibeInput && keyboardInset > 0
1289
- ? { paddingBottom: 8 + keyboardInset }
1290
- : null,
1291
- ]}
1186
+ contentContainerStyle={styles.scrollContent}
1292
1187
  keyboardShouldPersistTaps="handled"
1293
1188
  keyboardDismissMode={Platform.OS === 'ios' ? 'interactive' : 'on-drag'}
1294
1189
  contentInsetAdjustmentBehavior="always"
@@ -1329,10 +1224,17 @@ export const FeedbackModal: React.FC = () => {
1329
1224
  <View style={styles.dogfoodWizard}>
1330
1225
  <Text style={styles.dogfoodWizardTitle}>Dogfood this app</Text>
1331
1226
  <Text style={styles.dogfoodWizardHint}>
1332
- OAuth ✓ · machine {machineCard.device ? '✓' : 'required'} · installation {dogfoodEnrollment?.status || 'checking'}
1227
+ {dogfoodEnrollment?.status === 'active'
1228
+ ? 'Signed in · this installation is approved'
1229
+ : `Signed in · installation ${dogfoodEnrollment?.status || 'checking'}`}
1333
1230
  </Text>
1334
- <DogfoodStatusRail steps={dogfoodReadinessSteps} />
1335
- {dogfoodEnrollment?.installationId ? (
1231
+ {dogfoodEnrollment?.status === 'active' && dogfoodSetupStage === 'setup' ? (
1232
+ <DogfoodStatusRail
1233
+ steps={dogfoodSetupSteps}
1234
+ colors={FEEDBACK_DOGFOOD_LIGHT_COLORS}
1235
+ />
1236
+ ) : null}
1237
+ {dogfoodEnrollment?.status !== 'active' && dogfoodEnrollment?.installationId ? (
1336
1238
  <Text selectable style={styles.dogfoodInstallationId}>
1337
1239
  This device · {dogfoodEnrollment.installationId}
1338
1240
  </Text>
@@ -1352,96 +1254,165 @@ export const FeedbackModal: React.FC = () => {
1352
1254
  </View>
1353
1255
  ) : (
1354
1256
  <>
1355
- <Text style={styles.dogfoodStepLabel}>Project on selected machine</Text>
1356
- <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.dogfoodChoiceRow}>
1357
- {dogfoodProjects.map((project) => (
1358
- <Pressable
1359
- key={project.path}
1360
- onPress={() => {
1361
- setDogfoodProject(project);
1362
- setDogfoodLane(defaultDogfoodLane(project.framework || YaverFeedback.getDogfoodOnboarding()?.framework || 'expo'));
1363
- setDogfoodRuntime(null);
1364
- }}
1365
- style={[styles.dogfoodChoice, dogfoodProject?.path === project.path && styles.dogfoodChoiceSelected]}
1366
- >
1367
- <Text style={[styles.dogfoodChoiceText, dogfoodProject?.path === project.path && styles.dogfoodChoiceTextSelected]}>{project.name}</Text>
1368
- </Pressable>
1369
- ))}
1370
- </ScrollView>
1371
- <Text style={styles.dogfoodStepLabel}>Coding agent</Text>
1372
- <View style={styles.dogfoodChoiceRow}>
1373
- {runnerCards.filter((row) => row.ready || row.authConfigured).map((row) => (
1374
- <Pressable
1375
- key={row.id}
1257
+ {dogfoodSetupStage === 'setup' ? (
1258
+ <>
1259
+ {dogfoodExpandedStep === 'runner' ? (
1260
+ <View style={styles.dogfoodExpandedPanel}>
1261
+ <Text style={styles.dogfoodStepLabel}>Coding runner</Text>
1262
+ <View style={styles.dogfoodChoiceRow}>
1263
+ {runnerCards.filter((row) => row.ready || row.authConfigured).map((row) => (
1264
+ <Pressable
1265
+ key={row.id}
1266
+ onPress={() => {
1267
+ const nextModel = row.models?.find((model) => model.isDefault)?.id || row.models?.[0]?.id || '';
1268
+ setPreferredRunnerState(row.id);
1269
+ setPreferredModelState(nextModel);
1270
+ void setPreferredRunner(row.id);
1271
+ void setPreferredModel(nextModel || null);
1272
+ }}
1273
+ style={[styles.dogfoodChoice, preferredRunner === row.id && styles.dogfoodChoiceSelected]}
1274
+ accessibilityRole="button"
1275
+ accessibilityState={{ selected: preferredRunner === row.id }}
1276
+ >
1277
+ <Text style={[styles.dogfoodChoiceText, preferredRunner === row.id && styles.dogfoodChoiceTextSelected]}>{row.name}</Text>
1278
+ </Pressable>
1279
+ ))}
1280
+ </View>
1281
+ {readyRunnerCount === 0 ? (
1282
+ <Text style={styles.dogfoodWizardHint}>Configure a coding runner below, then retry.</Text>
1283
+ ) : null}
1284
+ {selectedDogfoodRunner?.models?.length ? (
1285
+ <>
1286
+ <Text style={styles.dogfoodStepLabel}>Model</Text>
1287
+ <View style={styles.dogfoodChoiceRow}>
1288
+ {selectedDogfoodRunner.models.map((model) => (
1289
+ <Pressable
1290
+ key={model.id}
1291
+ onPress={() => {
1292
+ setPreferredModelState(model.id);
1293
+ void setPreferredModel(model.id);
1294
+ }}
1295
+ style={[styles.dogfoodChoice, preferredModel === model.id && styles.dogfoodChoiceSelected]}
1296
+ >
1297
+ <Text style={[styles.dogfoodChoiceText, preferredModel === model.id && styles.dogfoodChoiceTextSelected]}>{model.name || model.id}</Text>
1298
+ </Pressable>
1299
+ ))}
1300
+ </View>
1301
+ </>
1302
+ ) : null}
1303
+ </View>
1304
+ ) : null}
1305
+ {dogfoodExpandedStep === 'checkout' ? (
1306
+ <View style={styles.dogfoodExpandedPanel}>
1307
+ <Text style={styles.dogfoodStepLabel}>Git checkout on remote box</Text>
1308
+ {dogfoodProjects.map((project) => (
1309
+ <Pressable
1310
+ key={project.path}
1311
+ onPress={() => {
1312
+ setDogfoodProject(project);
1313
+ setDogfoodLane(defaultDogfoodLane(project.framework || YaverFeedback.getDogfoodOnboarding()?.framework || 'expo'));
1314
+ setDogfoodRuntime(null);
1315
+ }}
1316
+ style={[styles.dogfoodProjectChoice, dogfoodProject?.path === project.path && styles.dogfoodChoiceSelected]}
1317
+ >
1318
+ <Text style={[styles.dogfoodChoiceText, dogfoodProject?.path === project.path && styles.dogfoodChoiceTextSelected]}>{project.name}</Text>
1319
+ <Text style={styles.dogfoodProjectDetail}>{dogfoodCheckoutDetail(project)}</Text>
1320
+ </Pressable>
1321
+ ))}
1322
+ </View>
1323
+ ) : null}
1324
+ <ActionRow
1325
+ label="Choose runtime"
1326
+ tint="#5645d8"
1376
1327
  onPress={() => {
1377
- const nextModel = row.models?.find((model) => model.isDefault)?.id || row.models?.[0]?.id || '';
1378
- setPreferredRunnerState(row.id);
1379
- setPreferredModelState(nextModel);
1380
- void setPreferredRunner(row.id);
1381
- void setPreferredModel(nextModel || null);
1328
+ setDogfoodExpandedStep(null);
1329
+ setDogfoodSetupStage('lane');
1382
1330
  }}
1383
- style={[styles.dogfoodChoice, preferredRunner === row.id && styles.dogfoodChoiceSelected]}
1384
- accessibilityRole="button"
1385
- accessibilityState={{ selected: preferredRunner === row.id }}
1386
- accessibilityLabel={`Use ${row.name} for Dogfood`}
1387
- >
1388
- <Text style={[styles.dogfoodChoiceText, preferredRunner === row.id && styles.dogfoodChoiceTextSelected]}>{row.name}</Text>
1389
- </Pressable>
1390
- ))}
1391
- </View>
1392
- {readyRunnerCount === 0 ? (
1393
- <Text style={styles.dogfoodWizardHint}>Sign in or configure a coding agent under Coding Agents below.</Text>
1331
+ disabled={!dogfoodSetupReady}
1332
+ />
1333
+ </>
1394
1334
  ) : null}
1395
- {selectedDogfoodRunner?.models?.length ? (
1335
+
1336
+ {dogfoodSetupStage === 'lane' ? (
1396
1337
  <>
1397
- <Text style={styles.dogfoodStepLabel}>Model</Text>
1398
- <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.dogfoodChoiceRow}>
1399
- {selectedDogfoodRunner.models.map((model) => (
1400
- <Pressable
1401
- key={model.id}
1402
- onPress={() => {
1403
- setPreferredModelState(model.id);
1404
- void setPreferredModel(model.id);
1405
- }}
1406
- style={[styles.dogfoodChoice, preferredModel === model.id && styles.dogfoodChoiceSelected]}
1407
- accessibilityRole="button"
1408
- accessibilityState={{ selected: preferredModel === model.id }}
1409
- accessibilityLabel={`Use ${model.name || model.id} model for Dogfood`}
1410
- >
1411
- <Text style={[styles.dogfoodChoiceText, preferredModel === model.id && styles.dogfoodChoiceTextSelected]}>{model.name || model.id}</Text>
1412
- </Pressable>
1413
- ))}
1414
- </ScrollView>
1338
+ <View style={styles.dogfoodStageHeader}>
1339
+ <View style={styles.dogfoodStageCopy}>
1340
+ <Text style={styles.dogfoodStepLabel}>Runtime</Text>
1341
+ <Text style={styles.dogfoodStageTitle}>{dogfoodProject?.name} · {dogfoodFramework}</Text>
1342
+ </View>
1343
+ <Pressable onPress={() => setDogfoodSetupStage('setup')} style={styles.dogfoodSmallAction}>
1344
+ <Text style={styles.dogfoodSmallActionText}>Back</Text>
1345
+ </Pressable>
1346
+ </View>
1347
+ <DogfoodLanePicker
1348
+ options={dogfoodLaneChoices}
1349
+ selected={dogfoodLane}
1350
+ colors={FEEDBACK_DOGFOOD_LIGHT_COLORS}
1351
+ onSelect={(lane) => {
1352
+ setDogfoodLane(lane);
1353
+ const appId = YaverFeedback.getDogfoodOnboarding()?.appId;
1354
+ if (appId) void setPreferredDogfoodLane(appId, lane);
1355
+ }}
1356
+ />
1357
+ {dogfoodLane === 'webrtc' ? (
1358
+ <View style={styles.dogfoodExpandedPanel}>
1359
+ <Text style={styles.dogfoodStepLabel}>Simulator, emulator, or device</Text>
1360
+ {dogfoodNativeTargets.map((target) => (
1361
+ <Pressable
1362
+ key={target.id}
1363
+ disabled={!target.enabled}
1364
+ onPress={() => setDogfoodNativeTargetId(target.id)}
1365
+ style={[
1366
+ styles.dogfoodProjectChoice,
1367
+ dogfoodNativeTargetId === target.id && styles.dogfoodChoiceSelected,
1368
+ !target.enabled && styles.dogfoodChoiceDisabled,
1369
+ ]}
1370
+ >
1371
+ <Text style={[styles.dogfoodChoiceText, dogfoodNativeTargetId === target.id && styles.dogfoodChoiceTextSelected]}>{target.label}</Text>
1372
+ <Text style={styles.dogfoodProjectDetail}>{target.enabled
1373
+ ? [target.platform, target.displaySurface || target.surface, 'WebRTC'].filter(Boolean).join(' · ')
1374
+ : target.reason || 'Unavailable on this box'}</Text>
1375
+ </Pressable>
1376
+ ))}
1377
+ </View>
1378
+ ) : null}
1379
+ <Text style={styles.dogfoodWizardHint}>Logs will be labelled with their real source: remote browser build, Hermes host, iOS Simulator, Android emulator, or connected device.</Text>
1380
+ <ActionRow
1381
+ label="Start Dogfood"
1382
+ tint="#5645d8"
1383
+ onPress={() => void startDogfoodRuntime()}
1384
+ disabled={dogfoodStartBlocked}
1385
+ />
1415
1386
  </>
1416
1387
  ) : null}
1417
- <Text style={styles.dogfoodStepLabel}>Runtime lane</Text>
1418
- <DogfoodLanePicker
1419
- options={dogfoodLaneChoices}
1420
- selected={dogfoodLane}
1421
- onSelect={(lane) => {
1422
- setDogfoodLane(lane);
1423
- const appId = YaverFeedback.getDogfoodOnboarding()?.appId;
1424
- if (appId) void setPreferredDogfoodLane(appId, lane);
1425
- }}
1426
- />
1427
- <Text style={styles.dogfoodWizardHint}>
1428
- {[preferredRunner || 'Choose a coding agent', preferredModel].filter(Boolean).join(' · ')}
1429
- </Text>
1430
- <ActionRow
1431
- label={dogfoodRuntime && !['idle', 'ready', 'failed', 'stopped'].includes(dogfoodRuntime.phase) ? dogfoodRuntime.message : 'Start Dogfood'}
1432
- tint="#818cf8"
1433
- onPress={() => void startDogfoodRuntime()}
1434
- disabled={dogfoodStartBlocked || !!(dogfoodRuntime && !['idle', 'ready', 'failed', 'stopped'].includes(dogfoodRuntime.phase))}
1435
- busy={!!dogfoodRuntime && !['idle', 'ready', 'failed', 'stopped'].includes(dogfoodRuntime.phase)}
1436
- />
1437
- {dogfoodRuntime ? (
1388
+
1389
+ {dogfoodSetupStage === 'runtime' && dogfoodRuntime ? (
1438
1390
  <>
1391
+ <View style={styles.dogfoodStageHeader}>
1392
+ <View style={styles.dogfoodStageCopy}>
1393
+ <Text style={styles.dogfoodStepLabel}>Dogfooding</Text>
1394
+ <Text style={styles.dogfoodStageTitle}>{dogfoodSourceLabel}</Text>
1395
+ </View>
1396
+ <Pressable
1397
+ onPress={() => {
1398
+ void dogfoodControllerRef.current?.stop().catch(() => {});
1399
+ dogfoodControllerRef.current = null;
1400
+ setDogfoodRuntime(null);
1401
+ setDogfoodSetupStage('lane');
1402
+ }}
1403
+ style={styles.dogfoodSmallAction}
1404
+ >
1405
+ <Text style={styles.dogfoodSmallActionText}>Change</Text>
1406
+ </Pressable>
1407
+ </View>
1439
1408
  <DogfoodLiveConsole
1440
1409
  lane={dogfoodRuntime.project.lane}
1410
+ sourceLabel={dogfoodSourceLabel}
1441
1411
  phase={dogfoodRuntime.phase}
1442
1412
  message={dogfoodRuntime.message}
1443
1413
  logs={dogfoodRuntime.logs}
1444
1414
  failure={dogfoodRuntime.failure}
1415
+ colors={FEEDBACK_DOGFOOD_CONSOLE_COLORS}
1445
1416
  />
1446
1417
  {dogfoodRuntime.result?.url ? (
1447
1418
  <Pressable onPress={() => void Linking.openURL(dogfoodRuntime.result!.url!)} style={styles.dogfoodOpenPreview}>
@@ -1714,7 +1685,7 @@ export const FeedbackModal: React.FC = () => {
1714
1685
  ? `${reloadAction.label}…`
1715
1686
  : reloadAction.label
1716
1687
  }
1717
- tint={reloadAction.id === 'rebuild' ? '#38bdf8' : '#fbbf24'}
1688
+ tint={reloadAction.id === 'rebuild' ? '#0369a1' : '#9a5700'}
1718
1689
  onPress={() => {
1719
1690
  void handleReloadAction(reloadAction);
1720
1691
  }}
@@ -1735,16 +1706,12 @@ export const FeedbackModal: React.FC = () => {
1735
1706
  </View>
1736
1707
 
1737
1708
  <View style={[styles.tabContent, activeTab !== 'chat' && styles.hidden]}>
1738
- {/* 3. Vibing expands to an input box on first tap
1739
- so the user says WHAT they want to vibe on, just
1740
- like the Yaver mobile app's Vibing tab. Second
1741
- tap (Send) fires /vibing/execute with the typed
1742
- prompt + resolved bundle id so the agent routes
1743
- to the right repo. */}
1709
+ {/* Chat creates the first task, then VibeChatScreen owns the
1710
+ transcript and every MCP-backed follow-up. */}
1744
1711
  {!showVibeInput ? (
1745
1712
  <ActionRow
1746
1713
  label={action === 'vibing' ? 'Starting…' : 'Vibing'}
1747
- tint="#818cf8"
1714
+ tint="#5645d8"
1748
1715
  onPress={handleVibingButton}
1749
1716
  disabled={busy}
1750
1717
  busy={action === 'vibing'}
@@ -1794,31 +1761,6 @@ export const FeedbackModal: React.FC = () => {
1794
1761
  </Text>
1795
1762
  )}
1796
1763
 
1797
- {/* Screenshot & Fix */}
1798
- {!dogfoodActive ? <ActionRow
1799
- label={action === 'capturing' ? 'Working…' : 'Screenshot & Fix'}
1800
- tint="#22c55e"
1801
- onPress={handleScreenshotAndFix}
1802
- disabled={busy}
1803
- busy={action === 'capturing'}
1804
- /> : null}
1805
-
1806
- {/* Deploy — opens an inline panel that talks to
1807
- /fleet/deploy-options on the agent and lets the user
1808
- pick TestFlight / Play / Both, then a machine to run
1809
- it on. Capabilities (e.g. "Linux can't TestFlight")
1810
- come from the agent's doctor probes — no client-side
1811
- platform smarts here. */}
1812
- {!dogfoodActive && (!showDeploy ? (
1813
- <ActionRow
1814
- label="Deploy"
1815
- tint="#7f8cf7"
1816
- onPress={() => setShowDeploy(true)}
1817
- disabled={busy}
1818
- />
1819
- ) : (
1820
- <DeployPanel onClose={() => setShowDeploy(false)} />
1821
- ))}
1822
1764
  </View>
1823
1765
 
1824
1766
  {progress !== null && (
@@ -1846,9 +1788,8 @@ export const FeedbackModal: React.FC = () => {
1846
1788
  <Text style={styles.cancelBtnText}>Cancel</Text>
1847
1789
  </Pressable>
1848
1790
  </ScrollView>
1849
- </Pressable>
1850
- </KeyboardAvoidingView>
1851
- </Pressable>
1791
+ </View>
1792
+ </View>
1852
1793
  </Modal>
1853
1794
  )}
1854
1795
  {runnerAuthModal ? (
@@ -1913,8 +1854,17 @@ const styles = StyleSheet.create({
1913
1854
  dogfoodChoiceRow: { flexDirection: 'row', gap: 7 },
1914
1855
  dogfoodChoice: { borderRadius: 9, borderWidth: 1, borderColor: '#d8d8e3', backgroundColor: '#fff', paddingHorizontal: 10, paddingVertical: 8 },
1915
1856
  dogfoodChoiceSelected: { borderColor: '#818cf8', backgroundColor: 'rgba(129,140,248,0.18)' },
1857
+ dogfoodChoiceDisabled: { opacity: 0.48 },
1916
1858
  dogfoodChoiceText: { color: '#666671', fontSize: 12, fontWeight: '700' },
1917
1859
  dogfoodChoiceTextSelected: { color: '#5645d8' },
1860
+ dogfoodExpandedPanel: { gap: 8, borderRadius: 11, borderWidth: 1, borderColor: '#d8d8e3', backgroundColor: 'rgba(255,255,255,0.72)', padding: 10 },
1861
+ dogfoodProjectChoice: { gap: 3, borderRadius: 9, borderWidth: 1, borderColor: '#d8d8e3', backgroundColor: '#fff', paddingHorizontal: 10, paddingVertical: 9 },
1862
+ dogfoodProjectDetail: { color: '#777783', fontSize: 10, lineHeight: 14 },
1863
+ dogfoodStageHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 10 },
1864
+ dogfoodStageCopy: { flex: 1, gap: 2 },
1865
+ dogfoodStageTitle: { color: '#222229', fontSize: 13, fontWeight: '700' },
1866
+ dogfoodSmallAction: { borderRadius: 8, borderWidth: 1, borderColor: '#d8d8e3', backgroundColor: '#fff', paddingHorizontal: 10, paddingVertical: 7 },
1867
+ dogfoodSmallActionText: { color: '#5645d8', fontSize: 11, fontWeight: '800' },
1918
1868
  dogfoodConsole: { maxHeight: 260, gap: 4, borderRadius: 11, padding: 10, backgroundColor: '#15151b' },
1919
1869
  dogfoodConsoleStatus: { color: '#a5b4fc', fontSize: 12, fontWeight: '800' },
1920
1870
  dogfoodConsoleLine: { color: '#d1d5db', fontSize: 10, lineHeight: 14, fontFamily: Platform.select({ ios: 'Menlo', android: 'monospace', default: 'monospace' }) },
@@ -1927,7 +1877,7 @@ const styles = StyleSheet.create({
1927
1877
  gap: 4,
1928
1878
  },
1929
1879
  reloadHint: {
1930
- color: '#8b8b93',
1880
+ color: '#666671',
1931
1881
  fontSize: 11,
1932
1882
  lineHeight: 15,
1933
1883
  paddingHorizontal: 4,
@@ -1959,7 +1909,7 @@ const styles = StyleSheet.create({
1959
1909
  backgroundColor: 'transparent',
1960
1910
  },
1961
1911
  vibeCancelBtnText: {
1962
- color: '#999',
1912
+ color: '#5f5f69',
1963
1913
  fontSize: 14,
1964
1914
  fontWeight: '600',
1965
1915
  },
@@ -1967,7 +1917,7 @@ const styles = StyleSheet.create({
1967
1917
  paddingHorizontal: 16,
1968
1918
  paddingVertical: 8,
1969
1919
  borderRadius: 8,
1970
- backgroundColor: '#818cf8',
1920
+ backgroundColor: '#5645d8',
1971
1921
  minWidth: 72,
1972
1922
  alignItems: 'center',
1973
1923
  },
@@ -1977,7 +1927,7 @@ const styles = StyleSheet.create({
1977
1927
  fontWeight: '700',
1978
1928
  },
1979
1929
  vibeTaskLine: {
1980
- color: '#818cf8',
1930
+ color: '#5645d8',
1981
1931
  fontSize: 12,
1982
1932
  marginTop: -4,
1983
1933
  fontFamily: Platform.select({ ios: 'Menlo', android: 'monospace', default: 'monospace' }),
@@ -1987,10 +1937,7 @@ const styles = StyleSheet.create({
1987
1937
  backgroundColor: 'rgba(0,0,0,0.55)',
1988
1938
  justifyContent: 'flex-end',
1989
1939
  },
1990
- kbAvoider: {
1991
- width: '100%',
1992
- justifyContent: 'flex-end',
1993
- },
1940
+ backdrop: StyleSheet.absoluteFillObject,
1994
1941
  modal: {
1995
1942
  backgroundColor: '#f8f8fb',
1996
1943
  borderTopLeftRadius: 22,
@@ -2049,7 +1996,7 @@ const styles = StyleSheet.create({
2049
1996
  tabs: { flexDirection: 'row', gap: 6, padding: 3, borderRadius: 12, backgroundColor: '#ededf3' },
2050
1997
  tab: { flex: 1, minHeight: 36, borderRadius: 9, alignItems: 'center', justifyContent: 'center' },
2051
1998
  tabSelected: { backgroundColor: '#fff' },
2052
- tabText: { color: '#858590', fontSize: 12, fontWeight: '700' },
1999
+ tabText: { color: '#666671', fontSize: 12, fontWeight: '700' },
2053
2000
  tabTextSelected: { color: '#6252e8' },
2054
2001
  tabContent: { gap: 12 },
2055
2002
  hidden: { display: 'none' },
@@ -2074,7 +2021,7 @@ const styles = StyleSheet.create({
2074
2021
  },
2075
2022
  machineRoutes: { flexDirection: 'row', gap: 8 },
2076
2023
  machineRouteCard: { flex: 1, minWidth: 0, borderRadius: 11, padding: 10, gap: 3, backgroundColor: '#fff', borderWidth: 1, borderColor: '#e1e1e8' },
2077
- machineRouteLabel: { color: '#7b7b86', fontSize: 10, fontWeight: '700', textTransform: 'uppercase' },
2024
+ machineRouteLabel: { color: '#666671', fontSize: 10, fontWeight: '700', textTransform: 'uppercase' },
2078
2025
  machineRouteValue: { color: '#222229', fontSize: 12, fontWeight: '700' },
2079
2026
  machineHeader: {
2080
2027
  flexDirection: 'row',
@@ -2110,7 +2057,7 @@ const styles = StyleSheet.create({
2110
2057
  letterSpacing: 0.8,
2111
2058
  },
2112
2059
  machineAction: {
2113
- color: '#a5b4fc',
2060
+ color: '#5645d8',
2114
2061
  fontSize: 12,
2115
2062
  fontWeight: '700',
2116
2063
  },
@@ -2141,7 +2088,7 @@ const styles = StyleSheet.create({
2141
2088
  },
2142
2089
  runnerSectionSubtitle: {
2143
2090
  marginTop: 2,
2144
- color: '#83838e',
2091
+ color: '#666671',
2145
2092
  fontSize: 12,
2146
2093
  },
2147
2094
  runnerRefreshBtn: {
@@ -2168,15 +2115,15 @@ const styles = StyleSheet.create({
2168
2115
  },
2169
2116
  runnerCardOk: {
2170
2117
  borderColor: 'rgba(34,197,94,0.28)',
2171
- backgroundColor: 'rgba(20,83,45,0.20)',
2118
+ backgroundColor: 'rgba(34,197,94,0.08)',
2172
2119
  },
2173
2120
  runnerCardWarning: {
2174
2121
  borderColor: 'rgba(251,191,36,0.28)',
2175
- backgroundColor: 'rgba(120,53,15,0.18)',
2122
+ backgroundColor: 'rgba(245,158,11,0.08)',
2176
2123
  },
2177
2124
  runnerCardError: {
2178
2125
  borderColor: 'rgba(248,113,113,0.28)',
2179
- backgroundColor: 'rgba(127,29,29,0.18)',
2126
+ backgroundColor: 'rgba(239,68,68,0.07)',
2180
2127
  },
2181
2128
  runnerCardTop: {
2182
2129
  flexDirection: 'row',
@@ -2195,13 +2142,13 @@ const styles = StyleSheet.create({
2195
2142
  color: '#666671',
2196
2143
  },
2197
2144
  runnerCardStatusOk: {
2198
- color: '#86efac',
2145
+ color: '#137a3f',
2199
2146
  },
2200
2147
  runnerCardStatusWarning: {
2201
- color: '#fcd34d',
2148
+ color: '#9a5700',
2202
2149
  },
2203
2150
  runnerCardStatusError: {
2204
- color: '#fca5a5',
2151
+ color: '#b42318',
2205
2152
  },
2206
2153
  runnerCardDetail: {
2207
2154
  color: '#858590',
@@ -2212,26 +2159,26 @@ const styles = StyleSheet.create({
2212
2159
  borderRadius: 10,
2213
2160
  borderWidth: 1,
2214
2161
  borderColor: 'rgba(129,140,248,0.35)',
2215
- backgroundColor: 'rgba(67,56,202,0.22)',
2162
+ backgroundColor: 'rgba(86,69,216,0.10)',
2216
2163
  paddingHorizontal: 12,
2217
2164
  paddingVertical: 8,
2218
2165
  },
2219
2166
  runnerActionBtnText: {
2220
- color: '#c7d2fe',
2167
+ color: '#5645d8',
2221
2168
  fontSize: 12,
2222
2169
  fontWeight: '700',
2223
2170
  },
2224
- runnerActionBtnSelected: { borderColor: '#818cf8', backgroundColor: 'rgba(79,70,229,0.44)' },
2171
+ runnerActionBtnSelected: { borderColor: '#6555df', backgroundColor: 'rgba(86,69,216,0.18)' },
2225
2172
  routingSummary: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingHorizontal: 2 },
2226
2173
  routingSummaryLabel: { color: '#7c7c87', fontSize: 11, fontWeight: '600' },
2227
2174
  routingSummaryValue: { flex: 1, color: '#6555df', fontSize: 12, fontWeight: '700', textAlign: 'right' },
2228
2175
  modelChoiceRow: { gap: 6, paddingTop: 2 },
2229
2176
  modelChoice: { borderRadius: 9, borderWidth: 1, borderColor: 'rgba(148,163,184,0.18)', paddingHorizontal: 9, paddingVertical: 6 },
2230
- modelChoiceSelected: { borderColor: '#818cf8', backgroundColor: 'rgba(79,70,229,0.30)' },
2177
+ modelChoiceSelected: { borderColor: '#6555df', backgroundColor: 'rgba(86,69,216,0.12)' },
2231
2178
  modelChoiceText: { color: '#73737e', fontSize: 11 },
2232
2179
  modelChoiceTextSelected: { color: '#5e4ce6', fontWeight: '700' },
2233
2180
  runnerSectionError: {
2234
- color: '#fca5a5',
2181
+ color: '#b42318',
2235
2182
  fontSize: 12,
2236
2183
  lineHeight: 18,
2237
2184
  },
@@ -2251,13 +2198,13 @@ const styles = StyleSheet.create({
2251
2198
  borderRadius: 3,
2252
2199
  },
2253
2200
  toast: {
2254
- color: '#22c55e',
2201
+ color: '#137a3f',
2255
2202
  fontSize: 13,
2256
2203
  textAlign: 'center',
2257
2204
  marginTop: 4,
2258
2205
  },
2259
2206
  error: {
2260
- color: '#ef4444',
2207
+ color: '#b42318',
2261
2208
  fontSize: 12,
2262
2209
  textAlign: 'center',
2263
2210
  marginTop: 4,