yaver-feedback-react-native 0.9.9 → 0.9.11

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';
@@ -56,30 +40,28 @@ import {
56
40
  import {
57
41
  DogfoodController,
58
42
  defaultDogfoodLane,
43
+ dogfoodLanePlan,
59
44
  dogfoodLaneOptions,
60
45
  type DogfoodLane,
61
46
  type DogfoodSnapshot,
62
47
  } from './DogfoodRuntime';
63
48
  import { createP2PDogfoodDriver } from './P2PDogfoodDriver';
64
49
  import { DogfoodLanePicker, DogfoodLiveConsole, DogfoodStatusRail } from './DogfoodSessionUi';
50
+ import type { DogfoodRemoteRuntimeTarget } from './P2PClient';
51
+ import {
52
+ FEEDBACK_DOGFOOD_CONSOLE_COLORS,
53
+ FEEDBACK_DOGFOOD_LIGHT_COLORS,
54
+ } from './FeedbackModalTheme';
65
55
 
66
56
  /**
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.
57
+ * Feedback modal with one conversational control surface. Authenticated users
58
+ * land directly in Chat; screenshots, fixes, and deploy intent are expressed
59
+ * in that conversation and handled by the connected agent/MCP toolchain.
77
60
  */
78
61
 
79
62
  type ActionState =
80
63
  | 'idle'
81
64
  | 'hot-reloading'
82
- | 'capturing'
83
65
  | 'vibing';
84
66
 
85
67
  type MachineCardState = {
@@ -122,8 +104,20 @@ type DogfoodProjectChoice = {
122
104
  framework?: string;
123
105
  frameworks?: string[];
124
106
  surfaces?: string[];
107
+ branch?: string;
108
+ gitRemote?: string;
125
109
  };
126
110
 
111
+ type DogfoodSetupStage = 'setup' | 'lane' | 'runtime';
112
+ type DogfoodExpandedStep = 'runner' | 'checkout' | null;
113
+
114
+ function dogfoodCheckoutDetail(project: DogfoodProjectChoice | null): string {
115
+ if (!project) return 'Choose a Git checkout on the selected machine';
116
+ return [project.path, project.branch ? `branch ${project.branch}` : project.gitRemote ? 'Git checkout' : 'checkout']
117
+ .filter(Boolean)
118
+ .join(' · ');
119
+ }
120
+
127
121
  const PRIMARY_RUNNER_IDS = ['claude', 'codex', 'opencode'] as const;
128
122
 
129
123
  function normalizeRunnerStatusRows(rows: RunnerAuthStatusRow[]): RunnerCardState[] {
@@ -261,18 +255,13 @@ export const FeedbackModal: React.FC = () => {
261
255
  // Which reload action is in flight, so only that button spins.
262
256
  const [reloadingId, setReloadingId] = useState<string | null>(null);
263
257
  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).
258
+ // Signed-in users get the composer immediately. Signed-out users still get
259
+ // an explicit entry action so auth/setup failures remain named and visible.
269
260
  const [showVibeInput, setShowVibeInput] = useState(false);
270
- const [showDeploy, setShowDeploy] = useState(false);
271
261
  const [vibePrompt, setVibePrompt] = useState('');
272
262
  const [lastVibeTaskId, setLastVibeTaskId] = useState<string | null>(null);
273
263
  const [quickIconColorPreset, setQuickIconColorPreset] =
274
264
  useState<QuickIconColorPreset | null>(null);
275
- const [keyboardInset, setKeyboardInset] = useState(0);
276
265
  const [machineCard, setMachineCard] = useState<MachineCardState>({
277
266
  device: null,
278
267
  reachable: null,
@@ -300,6 +289,11 @@ export const FeedbackModal: React.FC = () => {
300
289
  const [dogfoodProject, setDogfoodProject] = useState<DogfoodProjectChoice | null>(null);
301
290
  const [dogfoodLane, setDogfoodLane] = useState<DogfoodLane>('browser');
302
291
  const [dogfoodNativeAvailable, setDogfoodNativeAvailable] = useState(false);
292
+ const [dogfoodBrowserAvailable, setDogfoodBrowserAvailable] = useState(false);
293
+ const [dogfoodNativeTargets, setDogfoodNativeTargets] = useState<DogfoodRemoteRuntimeTarget[]>([]);
294
+ const [dogfoodNativeTargetId, setDogfoodNativeTargetId] = useState('');
295
+ const [dogfoodSetupStage, setDogfoodSetupStage] = useState<DogfoodSetupStage>('setup');
296
+ const [dogfoodExpandedStep, setDogfoodExpandedStep] = useState<DogfoodExpandedStep>(null);
303
297
  const [dogfoodRuntime, setDogfoodRuntime] = useState<DogfoodSnapshot | null>(null);
304
298
  const [dogfoodSetupLoading, setDogfoodSetupLoading] = useState(false);
305
299
  const dogfoodControllerRef = useRef<DogfoodController | null>(null);
@@ -330,12 +324,25 @@ export const FeedbackModal: React.FC = () => {
330
324
  if (preferred) {
331
325
  const framework = preferred.framework || onboarding.framework || 'expo';
332
326
  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);
327
+ const targets = capabilities?.targets || [];
328
+ const nativeTargets = targets.filter((target) => target.id !== 'browser-window');
329
+ const enabledNativeTargets = nativeTargets.filter((target) => target.enabled);
330
+ const nativeRuntimeAvailable = enabledNativeTargets.length > 0;
331
+ const browserRuntimeAvailable = targets.some((target) => target.enabled && target.id === 'browser-window');
332
+ if (mountedRef.current) {
333
+ setDogfoodNativeAvailable(nativeRuntimeAvailable);
334
+ setDogfoodBrowserAvailable(browserRuntimeAvailable);
335
+ setDogfoodNativeTargets(nativeTargets);
336
+ setDogfoodNativeTargetId((current) => enabledNativeTargets.some((target) => target.id === current)
337
+ ? current
338
+ : enabledNativeTargets[0]?.id || '');
339
+ }
335
340
  const savedLane = await getPreferredDogfoodLane(onboarding.appId);
336
- const savedSupported = dogfoodLaneOptions(framework, { nativeRuntimeAvailable })
341
+ const savedSupported = dogfoodLaneOptions(framework, { nativeRuntimeAvailable, browserRuntimeAvailable })
337
342
  .some((option) => option.lane === savedLane && option.supported);
338
- setDogfoodLane(savedLane && savedSupported ? savedLane : defaultDogfoodLane(framework));
343
+ setDogfoodLane(savedLane && savedSupported
344
+ ? savedLane
345
+ : defaultDogfoodLane(framework, { nativeRuntimeAvailable, browserRuntimeAvailable }));
339
346
  }
340
347
  } catch (cause) {
341
348
  if (mountedRef.current) setDogfoodEnrollment({ status: 'failed', error: cause instanceof Error ? cause.message : String(cause) });
@@ -347,12 +354,31 @@ export const FeedbackModal: React.FC = () => {
347
354
  useEffect(() => {
348
355
  let cancelled = false;
349
356
  setDogfoodNativeAvailable(false);
357
+ setDogfoodBrowserAvailable(false);
358
+ setDogfoodNativeTargets([]);
359
+ setDogfoodNativeTargetId('');
350
360
  const client = YaverFeedback.getP2PClient();
351
361
  if (!client || !dogfoodProject) return () => { cancelled = true; };
352
362
  const framework = dogfoodProject.framework || YaverFeedback.getDogfoodOnboarding()?.framework || 'expo';
353
363
  void client.getDogfoodRemoteRuntimeCapabilities(dogfoodProject.path, framework)
354
364
  .then((value) => {
355
- if (!cancelled) setDogfoodNativeAvailable(value.targets.some((target) => target.enabled && target.id !== 'browser-window'));
365
+ if (cancelled) return;
366
+ const nativeTargets = value.targets.filter((target) => target.id !== 'browser-window');
367
+ const enabledNativeTargets = nativeTargets.filter((target) => target.enabled);
368
+ setDogfoodNativeAvailable(enabledNativeTargets.length > 0);
369
+ setDogfoodBrowserAvailable(value.targets.some((target) => target.enabled && target.id === 'browser-window'));
370
+ setDogfoodNativeTargets(nativeTargets);
371
+ setDogfoodNativeTargetId((current) => enabledNativeTargets.some((target) => target.id === current)
372
+ ? current
373
+ : enabledNativeTargets[0]?.id || '');
374
+ const laneCapabilities = {
375
+ nativeRuntimeAvailable: enabledNativeTargets.length > 0,
376
+ browserRuntimeAvailable: value.targets.some((target) => target.enabled && target.id === 'browser-window'),
377
+ };
378
+ setDogfoodLane((current) => dogfoodLaneOptions(framework, laneCapabilities)
379
+ .some((option) => option.lane === current && option.supported)
380
+ ? current
381
+ : defaultDogfoodLane(framework, laneCapabilities));
356
382
  })
357
383
  .catch(() => {});
358
384
  return () => { cancelled = true; };
@@ -364,18 +390,25 @@ export const FeedbackModal: React.FC = () => {
364
390
  if (!client || !dogfoodProject || !onboarding) return;
365
391
  await dogfoodControllerRef.current?.stop().catch(() => {});
366
392
  const framework = dogfoodProject.framework || onboarding.framework || 'expo';
393
+ const lanePlan = dogfoodLanePlan(framework, {
394
+ nativeRuntimeAvailable: dogfoodNativeAvailable,
395
+ browserRuntimeAvailable: dogfoodBrowserAvailable,
396
+ }, dogfoodLane);
367
397
  const controller = new DogfoodController({
368
398
  name: dogfoodProject.name,
369
399
  workDir: dogfoodProject.path,
370
400
  framework,
371
- lane: dogfoodLane,
401
+ lane: lanePlan.preferred,
402
+ fallbackLane: lanePlan.fallback,
403
+ nativeTargetId: lanePlan.preferred === 'webrtc' ? dogfoodNativeTargetId : undefined,
372
404
  }, createP2PDogfoodDriver(client), {
373
405
  onChange: (snapshot) => { if (mountedRef.current) setDogfoodRuntime(snapshot); },
374
406
  });
375
407
  dogfoodControllerRef.current = controller;
376
408
  setDogfoodRuntime(controller.snapshot());
409
+ setDogfoodSetupStage('runtime');
377
410
  await controller.trigger().catch(() => {});
378
- }, [dogfoodLane, dogfoodProject]);
411
+ }, [dogfoodLane, dogfoodNativeTargetId, dogfoodProject]);
379
412
 
380
413
  /**
381
414
  * Ask the machine what its dev server is doing, so the reload actions can
@@ -571,16 +604,24 @@ export const FeedbackModal: React.FC = () => {
571
604
  // user's feedback preference merely to open Developer Mode.
572
605
  if (YaverFeedback.isEnabled() || onboarding) {
573
606
  const directDogfood = YaverFeedback.getDogfoodStatus().active;
607
+ const authenticated = YaverFeedback.isAuthed();
574
608
  setDogfoodActive(directDogfood);
575
609
  setVisible(true);
576
610
  setError(null);
577
611
  setToast(null);
578
612
  setProgress(null);
579
613
  setAction('idle');
580
- setShowVibeInput(directDogfood);
614
+ setShowVibeInput(authenticated || directDogfood);
581
615
  setVibePrompt('');
582
616
  if (onboarding) {
617
+ // A Dogfood shortcut is an explicit setup/runtime intent. Opening on
618
+ // Chat hid the machine/runner/checkout gate for signed-in SFMG users;
619
+ // keep the SDK-owned Dogfood surface visible, then show its live logs
620
+ // immediately when Start is tapped.
583
621
  setActiveTab('settings');
622
+ setDogfoodSetupStage('setup');
623
+ setDogfoodExpandedStep(null);
624
+ setDogfoodRuntime(null);
584
625
  void loadDogfoodOnboarding();
585
626
  }
586
627
  // Re-read the "user hid the quick icon" flag on every open so
@@ -652,31 +693,13 @@ export const FeedbackModal: React.FC = () => {
652
693
  return () => clearInterval(interval);
653
694
  }, [loadRunnerStatuses, loadSelectedMachine, refreshDevSnapshot, visible]);
654
695
 
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
696
  useEffect(() => {
676
697
  if (!visible || !showVibeInput) return;
677
- const timer = setTimeout(() => scrollRef.current?.scrollToEnd({ animated: true }), keyboardInset > 0 ? 120 : 40);
698
+ // UIKit owns keyboard insets on the ScrollView. This one bounded scroll only
699
+ // reveals the newly mounted composer; it does not add a second inset.
700
+ const timer = setTimeout(() => scrollRef.current?.scrollToEnd({ animated: true }), 80);
678
701
  return () => clearTimeout(timer);
679
- }, [keyboardInset, showVibeInput, visible]);
702
+ }, [showVibeInput, visible]);
680
703
 
681
704
  const closeSoon = useCallback((delayMs = 1200) => {
682
705
  setTimeout(() => {
@@ -693,6 +716,9 @@ export const FeedbackModal: React.FC = () => {
693
716
  setShowVibeInput(false);
694
717
  setVibePrompt('');
695
718
  setRunnerStatusError(null);
719
+ setDogfoodSetupStage('setup');
720
+ setDogfoodExpandedStep(null);
721
+ setDogfoodRuntime(null);
696
722
  void dogfoodControllerRef.current?.stop().catch(() => {});
697
723
  dogfoodControllerRef.current = null;
698
724
  YaverFeedback.clearDogfoodOnboarding();
@@ -886,123 +912,7 @@ export const FeedbackModal: React.FC = () => {
886
912
  }
887
913
  }, [closeSoon, loadSelectedMachine, runWithReconnect]);
888
914
 
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 ─────────────────────────────────────────────────────
915
+ // ─── Chat ──────────────────────────────────────────────────────────
1006
916
  // First tap expands the input; second submit fires the actual
1007
917
  // /vibing/execute. Mirrors the Yaver mobile app's Vibing tab —
1008
918
  // user types what they want, hits Send, sees the task id back. If
@@ -1151,45 +1061,59 @@ export const FeedbackModal: React.FC = () => {
1151
1061
  && (selectedDogfoodRunner.ready || selectedDogfoodRunner.authConfigured);
1152
1062
  const dogfoodModelReady = !selectedDogfoodRunner?.models?.length
1153
1063
  || !!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
- },
1064
+ const dogfoodFramework = dogfoodProject?.framework || YaverFeedback.getDogfoodOnboarding()?.framework || 'expo';
1065
+ const dogfoodLaneChoices = dogfoodLaneOptions(dogfoodFramework, {
1066
+ nativeRuntimeAvailable: dogfoodNativeAvailable,
1067
+ browserRuntimeAvailable: dogfoodBrowserAvailable,
1068
+ });
1069
+ const dogfoodLanePolicy = dogfoodLanePlan(dogfoodFramework, {
1070
+ nativeRuntimeAvailable: dogfoodNativeAvailable,
1071
+ browserRuntimeAvailable: dogfoodBrowserAvailable,
1072
+ }, dogfoodLane);
1073
+ const selectedDogfoodNativeTarget = dogfoodNativeTargets.find((target) => target.id === dogfoodNativeTargetId) || null;
1074
+ const dogfoodLaneReady = dogfoodLaneChoices.some((option) => option.lane === dogfoodLane && option.supported)
1075
+ && (dogfoodLane !== 'webrtc' || !!selectedDogfoodNativeTarget?.enabled);
1076
+ const dogfoodMachineReady = !!machineCard.device && machineCard.status === 'live';
1077
+ const dogfoodSetupReady = dogfoodMachineReady && !!dogfoodProject && dogfoodRunnerReady && dogfoodModelReady;
1078
+ const dogfoodSetupSteps = [
1176
1079
  {
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,
1080
+ key: 'box',
1081
+ label: 'Remote box',
1082
+ detail: dogfoodMachineReady ? machineCard.title : 'Choose a reachable development machine',
1083
+ tone: dogfoodMachineReady ? 'ready' as const : 'attention' as const,
1084
+ actionLabel: machineCard.device ? 'Change' : 'Pick',
1085
+ onAction: () => YaverFeedback.showMachinePicker(),
1180
1086
  },
1181
1087
  {
1182
- key: 'model', label: 'Model',
1183
- detail: dogfoodModelReady ? preferredModel || 'Runner default' : 'Choose a model',
1184
- tone: dogfoodModelReady ? 'ready' as const : 'attention' as const,
1088
+ key: 'runner',
1089
+ label: 'Runner',
1090
+ detail: dogfoodRunnerReady
1091
+ ? [selectedDogfoodRunner?.name || preferredRunner || 'Ready', preferredModel || 'default model'].join(' · ')
1092
+ : 'Choose or configure a coding runner',
1093
+ tone: dogfoodRunnerReady && dogfoodModelReady ? 'ready' as const : 'attention' as const,
1094
+ actionLabel: dogfoodExpandedStep === 'runner' ? 'Done' : dogfoodRunnerReady ? 'Change' : 'Choose',
1095
+ expanded: dogfoodExpandedStep === 'runner',
1096
+ onAction: () => setDogfoodExpandedStep((current) => current === 'runner' ? null : 'runner'),
1185
1097
  },
1186
1098
  {
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,
1099
+ key: 'checkout',
1100
+ label: 'Checkout',
1101
+ detail: dogfoodCheckoutDetail(dogfoodProject),
1102
+ tone: dogfoodProject ? 'ready' as const : 'attention' as const,
1103
+ actionLabel: dogfoodExpandedStep === 'checkout' ? 'Done' : dogfoodProject ? 'Change' : 'Choose',
1104
+ expanded: dogfoodExpandedStep === 'checkout',
1105
+ onAction: () => setDogfoodExpandedStep((current) => current === 'checkout' ? null : 'checkout'),
1190
1106
  },
1191
1107
  ];
1192
- const dogfoodStartBlocked = !dogfoodProject || !dogfoodRunnerReady || !dogfoodModelReady || !dogfoodLaneReady;
1108
+ const dogfoodStartBlocked = !dogfoodSetupReady || !dogfoodLaneReady;
1109
+ const activeDogfoodLane = dogfoodRuntime?.project.lane || dogfoodLane;
1110
+ const dogfoodSourceLabel = activeDogfoodLane === 'webrtc'
1111
+ ? selectedDogfoodNativeTarget
1112
+ ? [selectedDogfoodNativeTarget.label, selectedDogfoodNativeTarget.platform].filter(Boolean).join(' · ')
1113
+ : 'Native simulator, emulator, or device'
1114
+ : activeDogfoodLane === 'hermes'
1115
+ ? `Hermes build · ${machineCard.title}`
1116
+ : `${dogfoodFramework === 'flutter' ? 'Flutter web compiler' : 'Metro / browser build'} · ${machineCard.title}`;
1193
1117
 
1194
1118
  // Once the user fires off a vibe task, swap the entire modal body
1195
1119
  // for the live chat screen. The chat manages its own SSE
@@ -1252,14 +1176,9 @@ export const FeedbackModal: React.FC = () => {
1252
1176
  transparent
1253
1177
  onRequestClose={handleClose}
1254
1178
  >
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
1179
+ <View style={styles.overlay}>
1180
+ <Pressable style={styles.backdrop} onPress={handleClose} accessibilityLabel="Close feedback" />
1181
+ <View
1263
1182
  // Tablet: cap modal width and center as a card-style
1264
1183
  // sheet rather than a phone bottom sheet that stretches
1265
1184
  // across a 12.9" iPad. Phone behaviour unchanged.
@@ -1275,20 +1194,11 @@ export const FeedbackModal: React.FC = () => {
1275
1194
  }
1276
1195
  : null,
1277
1196
  ]}
1278
- onPress={(e) => {
1279
- e.stopPropagation();
1280
- Keyboard.dismiss();
1281
- }}
1282
1197
  >
1283
1198
  <ScrollView
1284
1199
  ref={scrollRef}
1285
1200
  style={styles.scroll}
1286
- contentContainerStyle={[
1287
- styles.scrollContent,
1288
- showVibeInput && keyboardInset > 0
1289
- ? { paddingBottom: 8 + keyboardInset }
1290
- : null,
1291
- ]}
1201
+ contentContainerStyle={styles.scrollContent}
1292
1202
  keyboardShouldPersistTaps="handled"
1293
1203
  keyboardDismissMode={Platform.OS === 'ios' ? 'interactive' : 'on-drag'}
1294
1204
  contentInsetAdjustmentBehavior="always"
@@ -1329,10 +1239,17 @@ export const FeedbackModal: React.FC = () => {
1329
1239
  <View style={styles.dogfoodWizard}>
1330
1240
  <Text style={styles.dogfoodWizardTitle}>Dogfood this app</Text>
1331
1241
  <Text style={styles.dogfoodWizardHint}>
1332
- OAuth ✓ · machine {machineCard.device ? '✓' : 'required'} · installation {dogfoodEnrollment?.status || 'checking'}
1242
+ {dogfoodEnrollment?.status === 'active'
1243
+ ? 'Signed in · this installation is approved'
1244
+ : `Signed in · installation ${dogfoodEnrollment?.status || 'checking'}`}
1333
1245
  </Text>
1334
- <DogfoodStatusRail steps={dogfoodReadinessSteps} />
1335
- {dogfoodEnrollment?.installationId ? (
1246
+ {dogfoodEnrollment?.status === 'active' && dogfoodSetupStage === 'setup' ? (
1247
+ <DogfoodStatusRail
1248
+ steps={dogfoodSetupSteps}
1249
+ colors={FEEDBACK_DOGFOOD_LIGHT_COLORS}
1250
+ />
1251
+ ) : null}
1252
+ {dogfoodEnrollment?.status !== 'active' && dogfoodEnrollment?.installationId ? (
1336
1253
  <Text selectable style={styles.dogfoodInstallationId}>
1337
1254
  This device · {dogfoodEnrollment.installationId}
1338
1255
  </Text>
@@ -1352,96 +1269,166 @@ export const FeedbackModal: React.FC = () => {
1352
1269
  </View>
1353
1270
  ) : (
1354
1271
  <>
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}
1272
+ {dogfoodSetupStage === 'setup' ? (
1273
+ <>
1274
+ {dogfoodExpandedStep === 'runner' ? (
1275
+ <View style={styles.dogfoodExpandedPanel}>
1276
+ <Text style={styles.dogfoodStepLabel}>Coding runner</Text>
1277
+ <View style={styles.dogfoodChoiceRow}>
1278
+ {runnerCards.filter((row) => row.ready || row.authConfigured).map((row) => (
1279
+ <Pressable
1280
+ key={row.id}
1281
+ onPress={() => {
1282
+ const nextModel = row.models?.find((model) => model.isDefault)?.id || row.models?.[0]?.id || '';
1283
+ setPreferredRunnerState(row.id);
1284
+ setPreferredModelState(nextModel);
1285
+ void setPreferredRunner(row.id);
1286
+ void setPreferredModel(nextModel || null);
1287
+ }}
1288
+ style={[styles.dogfoodChoice, preferredRunner === row.id && styles.dogfoodChoiceSelected]}
1289
+ accessibilityRole="button"
1290
+ accessibilityState={{ selected: preferredRunner === row.id }}
1291
+ >
1292
+ <Text style={[styles.dogfoodChoiceText, preferredRunner === row.id && styles.dogfoodChoiceTextSelected]}>{row.name}</Text>
1293
+ </Pressable>
1294
+ ))}
1295
+ </View>
1296
+ {readyRunnerCount === 0 ? (
1297
+ <Text style={styles.dogfoodWizardHint}>Configure a coding runner below, then retry.</Text>
1298
+ ) : null}
1299
+ {selectedDogfoodRunner?.models?.length ? (
1300
+ <>
1301
+ <Text style={styles.dogfoodStepLabel}>Model</Text>
1302
+ <View style={styles.dogfoodChoiceRow}>
1303
+ {selectedDogfoodRunner.models.map((model) => (
1304
+ <Pressable
1305
+ key={model.id}
1306
+ onPress={() => {
1307
+ setPreferredModelState(model.id);
1308
+ void setPreferredModel(model.id);
1309
+ }}
1310
+ style={[styles.dogfoodChoice, preferredModel === model.id && styles.dogfoodChoiceSelected]}
1311
+ >
1312
+ <Text style={[styles.dogfoodChoiceText, preferredModel === model.id && styles.dogfoodChoiceTextSelected]}>{model.name || model.id}</Text>
1313
+ </Pressable>
1314
+ ))}
1315
+ </View>
1316
+ </>
1317
+ ) : null}
1318
+ </View>
1319
+ ) : null}
1320
+ {dogfoodExpandedStep === 'checkout' ? (
1321
+ <View style={styles.dogfoodExpandedPanel}>
1322
+ <Text style={styles.dogfoodStepLabel}>Git checkout on remote box</Text>
1323
+ {dogfoodProjects.map((project) => (
1324
+ <Pressable
1325
+ key={project.path}
1326
+ onPress={() => {
1327
+ setDogfoodProject(project);
1328
+ setDogfoodLane(defaultDogfoodLane(project.framework || YaverFeedback.getDogfoodOnboarding()?.framework || 'expo'));
1329
+ setDogfoodRuntime(null);
1330
+ }}
1331
+ style={[styles.dogfoodProjectChoice, dogfoodProject?.path === project.path && styles.dogfoodChoiceSelected]}
1332
+ >
1333
+ <Text style={[styles.dogfoodChoiceText, dogfoodProject?.path === project.path && styles.dogfoodChoiceTextSelected]}>{project.name}</Text>
1334
+ <Text style={styles.dogfoodProjectDetail}>{dogfoodCheckoutDetail(project)}</Text>
1335
+ </Pressable>
1336
+ ))}
1337
+ </View>
1338
+ ) : null}
1339
+ <ActionRow
1340
+ label="Choose runtime"
1341
+ tint="#5645d8"
1376
1342
  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);
1343
+ setDogfoodExpandedStep(null);
1344
+ setDogfoodSetupStage('lane');
1382
1345
  }}
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>
1346
+ disabled={!dogfoodSetupReady}
1347
+ />
1348
+ </>
1394
1349
  ) : null}
1395
- {selectedDogfoodRunner?.models?.length ? (
1350
+
1351
+ {dogfoodSetupStage === 'lane' ? (
1396
1352
  <>
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>
1353
+ <View style={styles.dogfoodStageHeader}>
1354
+ <View style={styles.dogfoodStageCopy}>
1355
+ <Text style={styles.dogfoodStepLabel}>Runtime</Text>
1356
+ <Text style={styles.dogfoodStageTitle}>{dogfoodProject?.name} · {dogfoodFramework}</Text>
1357
+ </View>
1358
+ <Pressable onPress={() => setDogfoodSetupStage('setup')} style={styles.dogfoodSmallAction}>
1359
+ <Text style={styles.dogfoodSmallActionText}>Back</Text>
1360
+ </Pressable>
1361
+ </View>
1362
+ <DogfoodLanePicker
1363
+ options={dogfoodLaneChoices}
1364
+ selected={dogfoodLane}
1365
+ fallbackLane={dogfoodLanePolicy.fallback}
1366
+ colors={FEEDBACK_DOGFOOD_LIGHT_COLORS}
1367
+ onSelect={(lane) => {
1368
+ setDogfoodLane(lane);
1369
+ const appId = YaverFeedback.getDogfoodOnboarding()?.appId;
1370
+ if (appId) void setPreferredDogfoodLane(appId, lane);
1371
+ }}
1372
+ />
1373
+ {dogfoodLane === 'webrtc' ? (
1374
+ <View style={styles.dogfoodExpandedPanel}>
1375
+ <Text style={styles.dogfoodStepLabel}>Simulator, emulator, or device</Text>
1376
+ {dogfoodNativeTargets.map((target) => (
1377
+ <Pressable
1378
+ key={target.id}
1379
+ disabled={!target.enabled}
1380
+ onPress={() => setDogfoodNativeTargetId(target.id)}
1381
+ style={[
1382
+ styles.dogfoodProjectChoice,
1383
+ dogfoodNativeTargetId === target.id && styles.dogfoodChoiceSelected,
1384
+ !target.enabled && styles.dogfoodChoiceDisabled,
1385
+ ]}
1386
+ >
1387
+ <Text style={[styles.dogfoodChoiceText, dogfoodNativeTargetId === target.id && styles.dogfoodChoiceTextSelected]}>{target.label}</Text>
1388
+ <Text style={styles.dogfoodProjectDetail}>{target.enabled
1389
+ ? [target.platform, target.displaySurface || target.surface, 'WebRTC'].filter(Boolean).join(' · ')
1390
+ : target.reason || 'Unavailable on this box'}</Text>
1391
+ </Pressable>
1392
+ ))}
1393
+ </View>
1394
+ ) : null}
1395
+ <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>
1396
+ <ActionRow
1397
+ label="Start Dogfood"
1398
+ tint="#5645d8"
1399
+ onPress={() => void startDogfoodRuntime()}
1400
+ disabled={dogfoodStartBlocked}
1401
+ />
1415
1402
  </>
1416
1403
  ) : 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 ? (
1404
+
1405
+ {dogfoodSetupStage === 'runtime' && dogfoodRuntime ? (
1438
1406
  <>
1407
+ <View style={styles.dogfoodStageHeader}>
1408
+ <View style={styles.dogfoodStageCopy}>
1409
+ <Text style={styles.dogfoodStepLabel}>Dogfooding</Text>
1410
+ <Text style={styles.dogfoodStageTitle}>{dogfoodSourceLabel}</Text>
1411
+ </View>
1412
+ <Pressable
1413
+ onPress={() => {
1414
+ void dogfoodControllerRef.current?.stop().catch(() => {});
1415
+ dogfoodControllerRef.current = null;
1416
+ setDogfoodRuntime(null);
1417
+ setDogfoodSetupStage('lane');
1418
+ }}
1419
+ style={styles.dogfoodSmallAction}
1420
+ >
1421
+ <Text style={styles.dogfoodSmallActionText}>Change</Text>
1422
+ </Pressable>
1423
+ </View>
1439
1424
  <DogfoodLiveConsole
1440
1425
  lane={dogfoodRuntime.project.lane}
1426
+ sourceLabel={dogfoodSourceLabel}
1441
1427
  phase={dogfoodRuntime.phase}
1442
1428
  message={dogfoodRuntime.message}
1443
1429
  logs={dogfoodRuntime.logs}
1444
1430
  failure={dogfoodRuntime.failure}
1431
+ colors={FEEDBACK_DOGFOOD_CONSOLE_COLORS}
1445
1432
  />
1446
1433
  {dogfoodRuntime.result?.url ? (
1447
1434
  <Pressable onPress={() => void Linking.openURL(dogfoodRuntime.result!.url!)} style={styles.dogfoodOpenPreview}>
@@ -1714,7 +1701,7 @@ export const FeedbackModal: React.FC = () => {
1714
1701
  ? `${reloadAction.label}…`
1715
1702
  : reloadAction.label
1716
1703
  }
1717
- tint={reloadAction.id === 'rebuild' ? '#38bdf8' : '#fbbf24'}
1704
+ tint={reloadAction.id === 'rebuild' ? '#0369a1' : '#9a5700'}
1718
1705
  onPress={() => {
1719
1706
  void handleReloadAction(reloadAction);
1720
1707
  }}
@@ -1735,16 +1722,12 @@ export const FeedbackModal: React.FC = () => {
1735
1722
  </View>
1736
1723
 
1737
1724
  <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. */}
1725
+ {/* Chat creates the first task, then VibeChatScreen owns the
1726
+ transcript and every MCP-backed follow-up. */}
1744
1727
  {!showVibeInput ? (
1745
1728
  <ActionRow
1746
1729
  label={action === 'vibing' ? 'Starting…' : 'Vibing'}
1747
- tint="#818cf8"
1730
+ tint="#5645d8"
1748
1731
  onPress={handleVibingButton}
1749
1732
  disabled={busy}
1750
1733
  busy={action === 'vibing'}
@@ -1794,31 +1777,6 @@ export const FeedbackModal: React.FC = () => {
1794
1777
  </Text>
1795
1778
  )}
1796
1779
 
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
1780
  </View>
1823
1781
 
1824
1782
  {progress !== null && (
@@ -1846,9 +1804,8 @@ export const FeedbackModal: React.FC = () => {
1846
1804
  <Text style={styles.cancelBtnText}>Cancel</Text>
1847
1805
  </Pressable>
1848
1806
  </ScrollView>
1849
- </Pressable>
1850
- </KeyboardAvoidingView>
1851
- </Pressable>
1807
+ </View>
1808
+ </View>
1852
1809
  </Modal>
1853
1810
  )}
1854
1811
  {runnerAuthModal ? (
@@ -1913,8 +1870,17 @@ const styles = StyleSheet.create({
1913
1870
  dogfoodChoiceRow: { flexDirection: 'row', gap: 7 },
1914
1871
  dogfoodChoice: { borderRadius: 9, borderWidth: 1, borderColor: '#d8d8e3', backgroundColor: '#fff', paddingHorizontal: 10, paddingVertical: 8 },
1915
1872
  dogfoodChoiceSelected: { borderColor: '#818cf8', backgroundColor: 'rgba(129,140,248,0.18)' },
1873
+ dogfoodChoiceDisabled: { opacity: 0.48 },
1916
1874
  dogfoodChoiceText: { color: '#666671', fontSize: 12, fontWeight: '700' },
1917
1875
  dogfoodChoiceTextSelected: { color: '#5645d8' },
1876
+ dogfoodExpandedPanel: { gap: 8, borderRadius: 11, borderWidth: 1, borderColor: '#d8d8e3', backgroundColor: 'rgba(255,255,255,0.72)', padding: 10 },
1877
+ dogfoodProjectChoice: { gap: 3, borderRadius: 9, borderWidth: 1, borderColor: '#d8d8e3', backgroundColor: '#fff', paddingHorizontal: 10, paddingVertical: 9 },
1878
+ dogfoodProjectDetail: { color: '#777783', fontSize: 10, lineHeight: 14 },
1879
+ dogfoodStageHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 10 },
1880
+ dogfoodStageCopy: { flex: 1, gap: 2 },
1881
+ dogfoodStageTitle: { color: '#222229', fontSize: 13, fontWeight: '700' },
1882
+ dogfoodSmallAction: { borderRadius: 8, borderWidth: 1, borderColor: '#d8d8e3', backgroundColor: '#fff', paddingHorizontal: 10, paddingVertical: 7 },
1883
+ dogfoodSmallActionText: { color: '#5645d8', fontSize: 11, fontWeight: '800' },
1918
1884
  dogfoodConsole: { maxHeight: 260, gap: 4, borderRadius: 11, padding: 10, backgroundColor: '#15151b' },
1919
1885
  dogfoodConsoleStatus: { color: '#a5b4fc', fontSize: 12, fontWeight: '800' },
1920
1886
  dogfoodConsoleLine: { color: '#d1d5db', fontSize: 10, lineHeight: 14, fontFamily: Platform.select({ ios: 'Menlo', android: 'monospace', default: 'monospace' }) },
@@ -1927,7 +1893,7 @@ const styles = StyleSheet.create({
1927
1893
  gap: 4,
1928
1894
  },
1929
1895
  reloadHint: {
1930
- color: '#8b8b93',
1896
+ color: '#666671',
1931
1897
  fontSize: 11,
1932
1898
  lineHeight: 15,
1933
1899
  paddingHorizontal: 4,
@@ -1959,7 +1925,7 @@ const styles = StyleSheet.create({
1959
1925
  backgroundColor: 'transparent',
1960
1926
  },
1961
1927
  vibeCancelBtnText: {
1962
- color: '#999',
1928
+ color: '#5f5f69',
1963
1929
  fontSize: 14,
1964
1930
  fontWeight: '600',
1965
1931
  },
@@ -1967,7 +1933,7 @@ const styles = StyleSheet.create({
1967
1933
  paddingHorizontal: 16,
1968
1934
  paddingVertical: 8,
1969
1935
  borderRadius: 8,
1970
- backgroundColor: '#818cf8',
1936
+ backgroundColor: '#5645d8',
1971
1937
  minWidth: 72,
1972
1938
  alignItems: 'center',
1973
1939
  },
@@ -1977,7 +1943,7 @@ const styles = StyleSheet.create({
1977
1943
  fontWeight: '700',
1978
1944
  },
1979
1945
  vibeTaskLine: {
1980
- color: '#818cf8',
1946
+ color: '#5645d8',
1981
1947
  fontSize: 12,
1982
1948
  marginTop: -4,
1983
1949
  fontFamily: Platform.select({ ios: 'Menlo', android: 'monospace', default: 'monospace' }),
@@ -1987,10 +1953,7 @@ const styles = StyleSheet.create({
1987
1953
  backgroundColor: 'rgba(0,0,0,0.55)',
1988
1954
  justifyContent: 'flex-end',
1989
1955
  },
1990
- kbAvoider: {
1991
- width: '100%',
1992
- justifyContent: 'flex-end',
1993
- },
1956
+ backdrop: StyleSheet.absoluteFillObject,
1994
1957
  modal: {
1995
1958
  backgroundColor: '#f8f8fb',
1996
1959
  borderTopLeftRadius: 22,
@@ -2049,7 +2012,7 @@ const styles = StyleSheet.create({
2049
2012
  tabs: { flexDirection: 'row', gap: 6, padding: 3, borderRadius: 12, backgroundColor: '#ededf3' },
2050
2013
  tab: { flex: 1, minHeight: 36, borderRadius: 9, alignItems: 'center', justifyContent: 'center' },
2051
2014
  tabSelected: { backgroundColor: '#fff' },
2052
- tabText: { color: '#858590', fontSize: 12, fontWeight: '700' },
2015
+ tabText: { color: '#666671', fontSize: 12, fontWeight: '700' },
2053
2016
  tabTextSelected: { color: '#6252e8' },
2054
2017
  tabContent: { gap: 12 },
2055
2018
  hidden: { display: 'none' },
@@ -2074,7 +2037,7 @@ const styles = StyleSheet.create({
2074
2037
  },
2075
2038
  machineRoutes: { flexDirection: 'row', gap: 8 },
2076
2039
  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' },
2040
+ machineRouteLabel: { color: '#666671', fontSize: 10, fontWeight: '700', textTransform: 'uppercase' },
2078
2041
  machineRouteValue: { color: '#222229', fontSize: 12, fontWeight: '700' },
2079
2042
  machineHeader: {
2080
2043
  flexDirection: 'row',
@@ -2110,7 +2073,7 @@ const styles = StyleSheet.create({
2110
2073
  letterSpacing: 0.8,
2111
2074
  },
2112
2075
  machineAction: {
2113
- color: '#a5b4fc',
2076
+ color: '#5645d8',
2114
2077
  fontSize: 12,
2115
2078
  fontWeight: '700',
2116
2079
  },
@@ -2141,7 +2104,7 @@ const styles = StyleSheet.create({
2141
2104
  },
2142
2105
  runnerSectionSubtitle: {
2143
2106
  marginTop: 2,
2144
- color: '#83838e',
2107
+ color: '#666671',
2145
2108
  fontSize: 12,
2146
2109
  },
2147
2110
  runnerRefreshBtn: {
@@ -2168,15 +2131,15 @@ const styles = StyleSheet.create({
2168
2131
  },
2169
2132
  runnerCardOk: {
2170
2133
  borderColor: 'rgba(34,197,94,0.28)',
2171
- backgroundColor: 'rgba(20,83,45,0.20)',
2134
+ backgroundColor: 'rgba(34,197,94,0.08)',
2172
2135
  },
2173
2136
  runnerCardWarning: {
2174
2137
  borderColor: 'rgba(251,191,36,0.28)',
2175
- backgroundColor: 'rgba(120,53,15,0.18)',
2138
+ backgroundColor: 'rgba(245,158,11,0.08)',
2176
2139
  },
2177
2140
  runnerCardError: {
2178
2141
  borderColor: 'rgba(248,113,113,0.28)',
2179
- backgroundColor: 'rgba(127,29,29,0.18)',
2142
+ backgroundColor: 'rgba(239,68,68,0.07)',
2180
2143
  },
2181
2144
  runnerCardTop: {
2182
2145
  flexDirection: 'row',
@@ -2195,13 +2158,13 @@ const styles = StyleSheet.create({
2195
2158
  color: '#666671',
2196
2159
  },
2197
2160
  runnerCardStatusOk: {
2198
- color: '#86efac',
2161
+ color: '#137a3f',
2199
2162
  },
2200
2163
  runnerCardStatusWarning: {
2201
- color: '#fcd34d',
2164
+ color: '#9a5700',
2202
2165
  },
2203
2166
  runnerCardStatusError: {
2204
- color: '#fca5a5',
2167
+ color: '#b42318',
2205
2168
  },
2206
2169
  runnerCardDetail: {
2207
2170
  color: '#858590',
@@ -2212,26 +2175,26 @@ const styles = StyleSheet.create({
2212
2175
  borderRadius: 10,
2213
2176
  borderWidth: 1,
2214
2177
  borderColor: 'rgba(129,140,248,0.35)',
2215
- backgroundColor: 'rgba(67,56,202,0.22)',
2178
+ backgroundColor: 'rgba(86,69,216,0.10)',
2216
2179
  paddingHorizontal: 12,
2217
2180
  paddingVertical: 8,
2218
2181
  },
2219
2182
  runnerActionBtnText: {
2220
- color: '#c7d2fe',
2183
+ color: '#5645d8',
2221
2184
  fontSize: 12,
2222
2185
  fontWeight: '700',
2223
2186
  },
2224
- runnerActionBtnSelected: { borderColor: '#818cf8', backgroundColor: 'rgba(79,70,229,0.44)' },
2187
+ runnerActionBtnSelected: { borderColor: '#6555df', backgroundColor: 'rgba(86,69,216,0.18)' },
2225
2188
  routingSummary: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingHorizontal: 2 },
2226
2189
  routingSummaryLabel: { color: '#7c7c87', fontSize: 11, fontWeight: '600' },
2227
2190
  routingSummaryValue: { flex: 1, color: '#6555df', fontSize: 12, fontWeight: '700', textAlign: 'right' },
2228
2191
  modelChoiceRow: { gap: 6, paddingTop: 2 },
2229
2192
  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)' },
2193
+ modelChoiceSelected: { borderColor: '#6555df', backgroundColor: 'rgba(86,69,216,0.12)' },
2231
2194
  modelChoiceText: { color: '#73737e', fontSize: 11 },
2232
2195
  modelChoiceTextSelected: { color: '#5e4ce6', fontWeight: '700' },
2233
2196
  runnerSectionError: {
2234
- color: '#fca5a5',
2197
+ color: '#b42318',
2235
2198
  fontSize: 12,
2236
2199
  lineHeight: 18,
2237
2200
  },
@@ -2251,13 +2214,13 @@ const styles = StyleSheet.create({
2251
2214
  borderRadius: 3,
2252
2215
  },
2253
2216
  toast: {
2254
- color: '#22c55e',
2217
+ color: '#137a3f',
2255
2218
  fontSize: 13,
2256
2219
  textAlign: 'center',
2257
2220
  marginTop: 4,
2258
2221
  },
2259
2222
  error: {
2260
- color: '#ef4444',
2223
+ color: '#b42318',
2261
2224
  fontSize: 12,
2262
2225
  textAlign: 'center',
2263
2226
  marginTop: 4,