yaver-feedback-react-native 0.9.0 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/README.md +102 -1
  2. package/app.plugin.js +126 -9
  3. package/dist/AuthOverlay.d.ts +1 -14
  4. package/dist/AuthOverlay.js +9 -62
  5. package/dist/BlackBox.js +9 -1
  6. package/dist/DeployPanel.js +65 -0
  7. package/dist/Discovery.js +13 -8
  8. package/dist/DogfoodRuntime.d.ts +124 -0
  9. package/dist/DogfoodRuntime.js +273 -0
  10. package/dist/FeedbackModal.js +505 -65
  11. package/dist/LoginScreen.d.ts +1 -5
  12. package/dist/LoginScreen.js +2 -6
  13. package/dist/MachinePickerScreen.d.ts +1 -3
  14. package/dist/MachinePickerScreen.js +6 -18
  15. package/dist/P2PClient.d.ts +178 -2
  16. package/dist/P2PClient.js +493 -3
  17. package/dist/P2PDogfoodDriver.d.ts +12 -0
  18. package/dist/P2PDogfoodDriver.js +118 -0
  19. package/dist/PairDeviceModal.d.ts +2 -3
  20. package/dist/ShakeDetector.js +2 -0
  21. package/dist/VibeChatScreen.d.ts +11 -1
  22. package/dist/VibeChatScreen.js +200 -41
  23. package/dist/YaverFeedback.d.ts +132 -0
  24. package/dist/YaverFeedback.js +633 -65
  25. package/dist/YaverModeBadge.d.ts +22 -0
  26. package/dist/YaverModeBadge.js +219 -0
  27. package/dist/__tests__/AuthDevices.test.js +1 -48
  28. package/dist/__tests__/BlackBox.relayPassword.test.d.ts +1 -0
  29. package/dist/__tests__/BlackBox.relayPassword.test.js +105 -0
  30. package/dist/__tests__/BlackBoxAutoStart.test.d.ts +1 -0
  31. package/dist/__tests__/BlackBoxAutoStart.test.js +91 -0
  32. package/dist/__tests__/BlackBoxAutoStartColdStart.test.d.ts +1 -0
  33. package/dist/__tests__/BlackBoxAutoStartColdStart.test.js +156 -0
  34. package/dist/__tests__/BrowserLaneIcon.test.d.ts +3 -0
  35. package/dist/__tests__/BrowserLaneIcon.test.js +80 -0
  36. package/dist/__tests__/DogfoodRuntime.test.d.ts +1 -0
  37. package/dist/__tests__/DogfoodRuntime.test.js +116 -0
  38. package/dist/__tests__/P2PClient.test.js +2 -2
  39. package/dist/__tests__/P2PDogfoodDriver.test.d.ts +1 -0
  40. package/dist/__tests__/P2PDogfoodDriver.test.js +64 -0
  41. package/dist/__tests__/ReportIdentity.test.d.ts +25 -0
  42. package/dist/__tests__/ReportIdentity.test.js +180 -0
  43. package/dist/__tests__/SDKToken.test.js +1 -1
  44. package/dist/__tests__/ShakeToggle.test.d.ts +1 -0
  45. package/dist/__tests__/ShakeToggle.test.js +121 -0
  46. package/dist/__tests__/YaverFeedback.test.js +13 -3
  47. package/dist/__tests__/deviceDogfood.test.d.ts +1 -0
  48. package/dist/__tests__/deviceDogfood.test.js +86 -0
  49. package/dist/__tests__/dogfoodPolicy.test.d.ts +1 -0
  50. package/dist/__tests__/dogfoodPolicy.test.js +33 -0
  51. package/dist/__tests__/pickTargetDevice.test.d.ts +1 -0
  52. package/dist/__tests__/pickTargetDevice.test.js +89 -0
  53. package/dist/__tests__/reloadActions.test.d.ts +1 -0
  54. package/dist/__tests__/reloadActions.test.js +129 -0
  55. package/dist/__tests__/reloadActionsParity.test.d.ts +1 -0
  56. package/dist/__tests__/reloadActionsParity.test.js +42 -0
  57. package/dist/__tests__/types.test.js +5 -5
  58. package/dist/_core/ansi.d.ts +117 -0
  59. package/dist/_core/ansi.js +468 -0
  60. package/dist/_core/ansi.test.d.ts +1 -0
  61. package/dist/_core/ansi.test.js +225 -0
  62. package/dist/_core/buildFeedbackPrompt.d.ts +4 -9
  63. package/dist/_core/buildFeedbackPrompt.js +18 -72
  64. package/dist/_core/constants.d.ts +19 -6
  65. package/dist/_core/constants.js +20 -7
  66. package/dist/_core/device.d.ts +1 -5
  67. package/dist/_core/device.js +5 -14
  68. package/dist/_core/endpoints.d.ts +0 -7
  69. package/dist/_core/endpoints.js +0 -7
  70. package/dist/_core/index.d.ts +4 -0
  71. package/dist/_core/index.js +4 -0
  72. package/dist/_core/remoteless.d.ts +44 -0
  73. package/dist/_core/remoteless.js +75 -0
  74. package/dist/_core/trace.d.ts +47 -0
  75. package/dist/_core/trace.js +38 -0
  76. package/dist/_core/trace.test.d.ts +1 -0
  77. package/dist/_core/trace.test.js +60 -0
  78. package/dist/auth.d.ts +3 -60
  79. package/dist/auth.js +6 -88
  80. package/dist/deviceDogfood.d.ts +53 -0
  81. package/dist/deviceDogfood.js +137 -0
  82. package/dist/dogfoodPolicy.d.ts +25 -0
  83. package/dist/dogfoodPolicy.js +24 -0
  84. package/dist/index.d.ts +17 -4
  85. package/dist/index.js +33 -7
  86. package/dist/reloadActions.d.ts +88 -0
  87. package/dist/reloadActions.js +200 -0
  88. package/dist/storeShots.d.ts +67 -0
  89. package/dist/storeShots.js +137 -0
  90. package/dist/types.d.ts +208 -8
  91. package/package.json +13 -4
  92. package/src/AuthOverlay.tsx +20 -106
  93. package/src/BlackBox.ts +10 -1
  94. package/src/DeployPanel.tsx +74 -0
  95. package/src/Discovery.ts +13 -8
  96. package/src/DogfoodRuntime.ts +373 -0
  97. package/src/FeedbackModal.tsx +619 -79
  98. package/src/LoginScreen.tsx +2 -22
  99. package/src/MachinePickerScreen.tsx +6 -21
  100. package/src/P2PClient.ts +580 -4
  101. package/src/P2PDogfoodDriver.ts +132 -0
  102. package/src/PairDeviceModal.tsx +2 -3
  103. package/src/ShakeDetector.ts +1 -0
  104. package/src/VibeChatScreen.tsx +232 -42
  105. package/src/YaverFeedback.ts +631 -68
  106. package/src/YaverModeBadge.tsx +234 -0
  107. package/src/__tests__/AuthDevices.test.ts +1 -52
  108. package/src/__tests__/BlackBox.relayPassword.test.ts +129 -0
  109. package/src/__tests__/BlackBoxAutoStart.test.ts +111 -0
  110. package/src/__tests__/BlackBoxAutoStartColdStart.test.ts +191 -0
  111. package/src/__tests__/BrowserLaneIcon.test.ts +85 -0
  112. package/src/__tests__/DogfoodRuntime.test.ts +135 -0
  113. package/src/__tests__/P2PClient.test.ts +2 -2
  114. package/src/__tests__/P2PDogfoodDriver.test.ts +80 -0
  115. package/src/__tests__/ReportIdentity.test.ts +212 -0
  116. package/src/__tests__/SDKToken.test.ts +1 -1
  117. package/src/__tests__/ShakeToggle.test.ts +153 -0
  118. package/src/__tests__/YaverFeedback.test.ts +15 -3
  119. package/src/__tests__/deviceDogfood.test.ts +77 -0
  120. package/src/__tests__/dogfoodPolicy.test.ts +34 -0
  121. package/src/__tests__/pickTargetDevice.test.ts +101 -0
  122. package/src/__tests__/reloadActions.test.ts +171 -0
  123. package/src/__tests__/reloadActionsParity.test.ts +49 -0
  124. package/src/__tests__/types.test.ts +5 -5
  125. package/src/_core/ansi.test.ts +250 -0
  126. package/src/_core/ansi.ts +475 -0
  127. package/src/_core/buildFeedbackPrompt.ts +18 -95
  128. package/src/_core/constants.ts +20 -6
  129. package/src/_core/device.ts +5 -16
  130. package/src/_core/endpoints.ts +0 -7
  131. package/src/_core/index.ts +4 -0
  132. package/src/_core/remoteless.ts +110 -0
  133. package/src/_core/trace.test.ts +58 -0
  134. package/src/_core/trace.ts +75 -0
  135. package/src/auth.ts +7 -156
  136. package/src/deviceDogfood.ts +171 -0
  137. package/src/dogfoodPolicy.ts +40 -0
  138. package/src/index.ts +61 -11
  139. package/src/reloadActions.ts +273 -0
  140. package/src/storeShots.ts +189 -0
  141. package/src/types.ts +209 -8
  142. package/dist/GuestOnboardingScreen.d.ts +0 -8
  143. package/dist/GuestOnboardingScreen.js +0 -282
  144. package/src/GuestOnboardingScreen.tsx +0 -307
@@ -39,12 +39,17 @@ const react_native_1 = require("react-native");
39
39
  const YaverFeedback_1 = require("./YaverFeedback");
40
40
  const capture_1 = require("./capture");
41
41
  const upload_1 = require("./upload");
42
+ const P2PClient_1 = require("./P2PClient");
42
43
  const AuthOverlay_1 = require("./AuthOverlay");
43
44
  const QuickActionIcon_1 = require("./QuickActionIcon");
45
+ const YaverModeBadge_1 = require("./YaverModeBadge");
44
46
  const VibeChatScreen_1 = require("./VibeChatScreen");
45
47
  const DeployPanel_1 = require("./DeployPanel");
46
48
  const auth_1 = require("./auth");
49
+ const reloadActions_1 = require("./reloadActions");
47
50
  const preferences_1 = require("./preferences");
51
+ const DogfoodRuntime_1 = require("./DogfoodRuntime");
52
+ const P2PDogfoodDriver_1 = require("./P2PDogfoodDriver");
48
53
  const PRIMARY_RUNNER_IDS = ['claude', 'codex', 'opencode'];
49
54
  function normalizeRunnerStatusRows(rows) {
50
55
  const byId = new Map();
@@ -83,6 +88,7 @@ function normalizeRunnerStatusRows(rows) {
83
88
  version: row.version,
84
89
  tone: 'warning',
85
90
  statusLine: 'Not installed on the selected machine',
91
+ models: row.models,
86
92
  detail,
87
93
  };
88
94
  }
@@ -99,6 +105,7 @@ function normalizeRunnerStatusRows(rows) {
99
105
  statusLine: configured
100
106
  ? `${versionPrefix}Configured on the selected machine`
101
107
  : `${versionPrefix}Needs provider config on the selected machine`,
108
+ models: row.models,
102
109
  detail,
103
110
  };
104
111
  }
@@ -114,12 +121,34 @@ function normalizeRunnerStatusRows(rows) {
114
121
  statusLine: authed
115
122
  ? `${versionPrefix}Signed in on the selected machine`
116
123
  : `${versionPrefix}Not signed in on the selected machine`,
124
+ models: row.models,
117
125
  detail,
118
126
  actionLabel: authed ? 'Re-auth' : 'Sign in',
119
127
  actionRunner: id,
120
128
  };
121
129
  });
122
130
  }
131
+ /**
132
+ * Renders the "inside Yaver" mark unless the app opted out.
133
+ *
134
+ * Lives here rather than in YaverModeBadge so the component stays a dumb view
135
+ * and the DEFAULT (`modeBadge !== false` → on) is expressed in exactly one
136
+ * place. Mounted from FeedbackModal because that is the component integrating
137
+ * apps already render — a default that required a new mount point would not be
138
+ * a default, it would be a feature request.
139
+ */
140
+ const YaverModeBadgeGate = () => {
141
+ const [, refresh] = (0, react_1.useState)(0);
142
+ (0, react_1.useEffect)(() => {
143
+ const sub = react_native_1.DeviceEventEmitter.addListener('yaverFeedback:dogfoodChanged', () => refresh((n) => n + 1));
144
+ return () => sub.remove();
145
+ }, []);
146
+ const cfg = YaverFeedback_1.YaverFeedback.getConfig();
147
+ if (cfg?.modeBadge === false)
148
+ return null;
149
+ const dogfood = YaverFeedback_1.YaverFeedback.getDogfoodStatus();
150
+ return (<YaverModeBadge_1.YaverModeBadge position={cfg?.modeBadgePosition ?? 'bottom-left'} force={dogfood.active} dogfood={dogfood.active} dogfoodLabel={dogfood.label} onExitDogfood={() => YaverFeedback_1.YaverFeedback.exitDogfoodMode()}/>);
151
+ };
123
152
  const FeedbackModal = () => {
124
153
  const { width: winW, height: winH } = (0, react_native_1.useWindowDimensions)();
125
154
  const isTablet = Math.min(winW, winH) >= 600;
@@ -127,6 +156,7 @@ const FeedbackModal = () => {
127
156
  // looks empty on a 1024pt iPad. Mobile keeps 3-col.
128
157
  const iconOptionWidthOverride = isTablet ? '18%' : undefined;
129
158
  const [visible, setVisible] = (0, react_1.useState)(false);
159
+ const [dogfoodActive, setDogfoodActive] = (0, react_1.useState)(() => YaverFeedback_1.YaverFeedback.getDogfoodStatus().active);
130
160
  const [action, setAction] = (0, react_1.useState)('idle');
131
161
  const [error, setError] = (0, react_1.useState)(null);
132
162
  const [toast, setToast] = (0, react_1.useState)(null);
@@ -136,6 +166,11 @@ const FeedbackModal = () => {
136
166
  // is our guaranteed UI for bringing the icon back — we surface a
137
167
  // small "Show quick icon" row when this is true.
138
168
  const [quickIconHidden, setQuickIconHidden] = (0, react_1.useState)(false);
169
+ // Dev-server snapshot behind the reload buttons. `null` means "we have not
170
+ // been able to ask" — rendered as "not connected", never as "no dev server".
171
+ const [devSnapshot, setDevSnapshot] = (0, react_1.useState)(null);
172
+ // Which reload action is in flight, so only that button spins.
173
+ const [reloadingId, setReloadingId] = (0, react_1.useState)(null);
139
174
  const [runnerAuthModal, setRunnerAuthModal] = (0, react_1.useState)(null);
140
175
  // Vibing-input mode: same expand-on-tap pattern as email login.
141
176
  // Tap "Vibing" once → the button reveals an input + Send; that lets
@@ -161,8 +196,115 @@ const FeedbackModal = () => {
161
196
  const [runnerStatusError, setRunnerStatusError] = (0, react_1.useState)(null);
162
197
  const [preferredRunner, setPreferredRunnerState] = (0, react_1.useState)(null);
163
198
  const [preferredModel, setPreferredModelState] = (0, react_1.useState)('');
199
+ const [activeTab, setActiveTab] = (0, react_1.useState)('chat');
164
200
  const [showOpenCodeConfig, setShowOpenCodeConfig] = (0, react_1.useState)(false);
201
+ const scrollRef = (0, react_1.useRef)(null);
202
+ const [dogfoodEnrollment, setDogfoodEnrollment] = (0, react_1.useState)(null);
203
+ const [dogfoodProjects, setDogfoodProjects] = (0, react_1.useState)([]);
204
+ const [dogfoodProject, setDogfoodProject] = (0, react_1.useState)(null);
205
+ const [dogfoodLane, setDogfoodLane] = (0, react_1.useState)('browser');
206
+ const [dogfoodNativeAvailable, setDogfoodNativeAvailable] = (0, react_1.useState)(false);
207
+ const [dogfoodRuntime, setDogfoodRuntime] = (0, react_1.useState)(null);
208
+ const [dogfoodSetupLoading, setDogfoodSetupLoading] = (0, react_1.useState)(false);
209
+ const dogfoodControllerRef = (0, react_1.useRef)(null);
165
210
  const mountedRef = (0, react_1.useRef)(true);
211
+ const loadDogfoodOnboarding = (0, react_1.useCallback)(async () => {
212
+ const onboarding = YaverFeedback_1.YaverFeedback.getDogfoodOnboarding();
213
+ if (!onboarding)
214
+ return;
215
+ setDogfoodSetupLoading(true);
216
+ setDogfoodEnrollment({ status: 'checking' });
217
+ try {
218
+ const enrolled = await YaverFeedback_1.YaverFeedback.enableDeviceDogfood(onboarding);
219
+ if (!mountedRef.current)
220
+ return;
221
+ setDogfoodEnrollment({ status: enrolled.status, installationId: enrolled.installationId });
222
+ if (enrolled.status !== 'active')
223
+ return;
224
+ let client = YaverFeedback_1.YaverFeedback.getP2PClient();
225
+ if (!client && await YaverFeedback_1.YaverFeedback.reconnect())
226
+ client = YaverFeedback_1.YaverFeedback.getP2PClient();
227
+ if (!client)
228
+ throw new Error('The selected Yaver machine is not reachable yet.');
229
+ const projects = await client.listDogfoodProjects();
230
+ if (!mountedRef.current)
231
+ return;
232
+ setDogfoodProjects(projects);
233
+ const hint = String(onboarding.projectName || '').trim().toLowerCase();
234
+ const preferred = projects.find((item) => item.name.toLowerCase() === hint)
235
+ || projects.find((item) => hint && item.name.toLowerCase().includes(hint))
236
+ || projects[0]
237
+ || null;
238
+ setDogfoodProject((current) => current && projects.some((item) => item.path === current.path) ? current : preferred);
239
+ if (preferred)
240
+ setDogfoodLane((0, DogfoodRuntime_1.defaultDogfoodLane)(preferred.framework || onboarding.framework || 'expo'));
241
+ }
242
+ catch (cause) {
243
+ if (mountedRef.current)
244
+ setDogfoodEnrollment({ status: 'failed', error: cause instanceof Error ? cause.message : String(cause) });
245
+ }
246
+ finally {
247
+ if (mountedRef.current)
248
+ setDogfoodSetupLoading(false);
249
+ }
250
+ }, []);
251
+ (0, react_1.useEffect)(() => {
252
+ let cancelled = false;
253
+ setDogfoodNativeAvailable(false);
254
+ const client = YaverFeedback_1.YaverFeedback.getP2PClient();
255
+ if (!client || !dogfoodProject)
256
+ return () => { cancelled = true; };
257
+ const framework = dogfoodProject.framework || YaverFeedback_1.YaverFeedback.getDogfoodOnboarding()?.framework || 'expo';
258
+ void client.getDogfoodRemoteRuntimeCapabilities(dogfoodProject.path, framework)
259
+ .then((value) => {
260
+ if (!cancelled)
261
+ setDogfoodNativeAvailable(value.targets.some((target) => target.enabled && target.id !== 'browser-window'));
262
+ })
263
+ .catch(() => { });
264
+ return () => { cancelled = true; };
265
+ }, [dogfoodProject]);
266
+ const startDogfoodRuntime = (0, react_1.useCallback)(async () => {
267
+ const client = YaverFeedback_1.YaverFeedback.getP2PClient();
268
+ const onboarding = YaverFeedback_1.YaverFeedback.getDogfoodOnboarding();
269
+ if (!client || !dogfoodProject || !onboarding)
270
+ return;
271
+ await dogfoodControllerRef.current?.stop().catch(() => { });
272
+ const framework = dogfoodProject.framework || onboarding.framework || 'expo';
273
+ const controller = new DogfoodRuntime_1.DogfoodController({
274
+ name: dogfoodProject.name,
275
+ workDir: dogfoodProject.path,
276
+ framework,
277
+ lane: dogfoodLane,
278
+ }, (0, P2PDogfoodDriver_1.createP2PDogfoodDriver)(client), {
279
+ onChange: (snapshot) => { if (mountedRef.current)
280
+ setDogfoodRuntime(snapshot); },
281
+ });
282
+ dogfoodControllerRef.current = controller;
283
+ setDogfoodRuntime(controller.snapshot());
284
+ await controller.trigger().catch(() => { });
285
+ }, [dogfoodLane, dogfoodProject]);
286
+ /**
287
+ * Ask the machine what its dev server is doing, so the reload actions can
288
+ * be enabled/disabled against reality rather than against a guess.
289
+ *
290
+ * Best-effort and deliberately null-on-failure: null means "we could not
291
+ * ask", which the seam renders as "not connected to a machine yet" — a
292
+ * different sentence from "no dev server is running", because they have
293
+ * different fixes.
294
+ */
295
+ const refreshDevSnapshot = (0, react_1.useCallback)(async () => {
296
+ try {
297
+ const client = YaverFeedback_1.YaverFeedback.getP2PClient();
298
+ if (!client) {
299
+ setDevSnapshot(null);
300
+ return;
301
+ }
302
+ setDevSnapshot(await client.getDevServerStatus());
303
+ }
304
+ catch {
305
+ setDevSnapshot(null);
306
+ }
307
+ }, []);
166
308
  const loadSelectedMachine = (0, react_1.useCallback)(async () => {
167
309
  const cfg = YaverFeedback_1.YaverFeedback.getConfig();
168
310
  if (!cfg?.authToken) {
@@ -196,8 +338,7 @@ const FeedbackModal = () => {
196
338
  }
197
339
  try {
198
340
  const devices = await (0, auth_1.listReachableDevices)(cfg.authToken);
199
- const all = [...devices.owned, ...devices.shared];
200
- const device = all.find((candidate) => candidate.deviceId === cfg.preferredDeviceId) ?? null;
341
+ const device = devices.owned.find((candidate) => candidate.deviceId === cfg.preferredDeviceId) ?? null;
201
342
  if (!device) {
202
343
  if (mountedRef.current) {
203
344
  setMachineCard({
@@ -216,9 +357,8 @@ const FeedbackModal = () => {
216
357
  if (device.isOnline && !device.needsAuth && client) {
217
358
  reachable = await client.health();
218
359
  }
219
- const hostHint = device.hostEmail ? ` via ${device.hostEmail}` : '';
220
360
  let status = 'live';
221
- let detail = `${device.platform}${hostHint}`;
361
+ let detail = device.platform;
222
362
  if (!device.isOnline) {
223
363
  status = 'offline';
224
364
  detail = 'Machine offline. Start `yaver serve` on the selected machine.';
@@ -291,7 +431,7 @@ const FeedbackModal = () => {
291
431
  if (!client) {
292
432
  throw new Error('Not connected to the selected machine yet.');
293
433
  }
294
- const rows = await client.getRunnerAuthStatus();
434
+ const rows = await client.getAvailableRunners();
295
435
  if (mountedRef.current) {
296
436
  setRunnerCards(normalizeRunnerStatusRows(rows));
297
437
  }
@@ -329,13 +469,20 @@ const FeedbackModal = () => {
329
469
  mountedRef.current = true;
330
470
  const sub = react_native_1.DeviceEventEmitter.addListener('yaverFeedback:startReport', () => {
331
471
  if (YaverFeedback_1.YaverFeedback.isEnabled()) {
472
+ const onboarding = YaverFeedback_1.YaverFeedback.getDogfoodOnboarding();
473
+ const directDogfood = YaverFeedback_1.YaverFeedback.getDogfoodStatus().active;
474
+ setDogfoodActive(directDogfood);
332
475
  setVisible(true);
333
476
  setError(null);
334
477
  setToast(null);
335
478
  setProgress(null);
336
479
  setAction('idle');
337
- setShowVibeInput(false);
480
+ setShowVibeInput(directDogfood);
338
481
  setVibePrompt('');
482
+ if (onboarding) {
483
+ setActiveTab('settings');
484
+ void loadDogfoodOnboarding();
485
+ }
339
486
  // Re-read the "user hid the quick icon" flag on every open so
340
487
  // the re-enable row reflects the latest preference (the user
341
488
  // might have hidden or shown it between opens).
@@ -355,6 +502,13 @@ const FeedbackModal = () => {
355
502
  void loadRunnerStatuses();
356
503
  }
357
504
  });
505
+ const dogfoodSub = react_native_1.DeviceEventEmitter.addListener('yaverFeedback:dogfoodChanged', (payload) => {
506
+ if (!mountedRef.current)
507
+ return;
508
+ setDogfoodActive(payload?.active === true);
509
+ if (payload?.exited === true)
510
+ setVisible(false);
511
+ });
358
512
  // Agent streams build / compile progress through the BlackBox
359
513
  // SSE command channel as `command: "status"`; YaverFeedback re-emits
360
514
  // it as `yaverFeedback:status`. Show the most recent message in the
@@ -378,18 +532,25 @@ const FeedbackModal = () => {
378
532
  return () => {
379
533
  mountedRef.current = false;
380
534
  sub.remove();
535
+ dogfoodSub.remove();
381
536
  statusSub.remove();
382
537
  };
383
- }, [loadRunnerStatuses, loadSelectedMachine]);
538
+ }, [loadDogfoodOnboarding, loadRunnerStatuses, loadSelectedMachine]);
384
539
  (0, react_1.useEffect)(() => {
385
540
  if (!visible)
386
541
  return;
542
+ // Poll the dev server alongside the machine + runners. A reload button
543
+ // whose enabled state was decided once, when the sheet opened, goes
544
+ // stale the moment the user starts Metro from another surface — and a
545
+ // stale "no dev server is running" reads as the product being broken.
546
+ void refreshDevSnapshot();
387
547
  const interval = setInterval(() => {
388
548
  void loadSelectedMachine();
389
549
  void loadRunnerStatuses();
550
+ void refreshDevSnapshot();
390
551
  }, 5000);
391
552
  return () => clearInterval(interval);
392
- }, [loadRunnerStatuses, loadSelectedMachine, visible]);
553
+ }, [loadRunnerStatuses, loadSelectedMachine, refreshDevSnapshot, visible]);
393
554
  (0, react_1.useEffect)(() => {
394
555
  if (!visible) {
395
556
  setKeyboardInset(0);
@@ -408,6 +569,12 @@ const FeedbackModal = () => {
408
569
  hideSub.remove();
409
570
  };
410
571
  }, [visible]);
572
+ (0, react_1.useEffect)(() => {
573
+ if (!visible || !showVibeInput)
574
+ return;
575
+ const timer = setTimeout(() => scrollRef.current?.scrollToEnd({ animated: true }), keyboardInset > 0 ? 120 : 40);
576
+ return () => clearTimeout(timer);
577
+ }, [keyboardInset, showVibeInput, visible]);
411
578
  const closeSoon = (0, react_1.useCallback)((delayMs = 1200) => {
412
579
  setTimeout(() => {
413
580
  if (mountedRef.current)
@@ -423,6 +590,9 @@ const FeedbackModal = () => {
423
590
  setShowVibeInput(false);
424
591
  setVibePrompt('');
425
592
  setRunnerStatusError(null);
593
+ void dogfoodControllerRef.current?.stop().catch(() => { });
594
+ dogfoodControllerRef.current = null;
595
+ YaverFeedback_1.YaverFeedback.clearDogfoodOnboarding();
426
596
  }, []);
427
597
  // Helper: run a P2P call; on network failure, ask YaverFeedback to
428
598
  // re-query Convex for the fresh IP and retry once. Solves the common
@@ -473,7 +643,84 @@ const FeedbackModal = () => {
473
643
  await fn(fresh);
474
644
  }
475
645
  }, []);
476
- // ─── 1. Hot reload ─────────────────────────────────────────────────
646
+ // ─── 1. Reload ─────────────────────────────────────────────────────
647
+ //
648
+ // Three actions now, not one: Hot Reload (mode=fast), Full Reload
649
+ // (mode=full — Flutter's hot RESTART), and Rebuild Bundle
650
+ // (/dev/reload-app, the only one that works with Metro down).
651
+ //
652
+ // WHICH of them render, and which are disabled with what reason, is
653
+ // decided by the pure `reloadActions()` seam — never inline here, so the
654
+ // same policy holds on web, Flutter, Unity, Swift and Kotlin. In
655
+ // particular: a production build (`__DEV__ === false`) gets NONE of them.
656
+ const availableReloadActions = (0, reloadActions_1.reloadActions)(devSnapshot, {
657
+ // __DEV__ is React Native's own build flag. A release bundle sets it
658
+ // false, so a shipped app renders no reload UI at all — which is the
659
+ // point, and is what reloadActions.test.ts pins.
660
+ isDevBuild: typeof __DEV__ !== 'undefined' && __DEV__ === true,
661
+ connected: devSnapshot !== null,
662
+ machineLabel: machineCard.device?.name || undefined,
663
+ includeRebuild: true,
664
+ });
665
+ const handleReloadAction = (0, react_1.useCallback)(async (reloadAction) => {
666
+ if (!reloadAction.enabled) {
667
+ // Pressing a disabled action must SAY why. A row that does nothing
668
+ // is the same defect as a spinner that never resolves.
669
+ setToast(reloadAction.disabledReason || 'Reload is unavailable right now.');
670
+ setError(reloadAction.disabledReason || null);
671
+ return;
672
+ }
673
+ setReloadingId(reloadAction.id);
674
+ setAction('hot-reloading');
675
+ setError(null);
676
+ setProgress(0);
677
+ setToast(`${reloadAction.label}…`);
678
+ try {
679
+ await loadSelectedMachine();
680
+ const selected = await YaverFeedback_1.YaverFeedback.getSelectedRemoteDevice();
681
+ if (!selected) {
682
+ YaverFeedback_1.YaverFeedback.showMachinePicker();
683
+ throw new Error('No machine selected. Pick a machine and try again.');
684
+ }
685
+ if (selected.needsAuth) {
686
+ YaverFeedback_1.YaverFeedback.showMachinePicker();
687
+ throw new Error('Selected machine needs pairing again.');
688
+ }
689
+ if (!selected.isOnline) {
690
+ throw new Error('Selected machine is offline. Start `yaver serve` on it first.');
691
+ }
692
+ let ackMessage = `${reloadAction.label} requested.`;
693
+ await runWithReconnect(async () => {
694
+ const renderClient = YaverFeedback_1.YaverFeedback.getRenderP2PClient();
695
+ if (!renderClient)
696
+ throw new Error('Render machine is not connected yet.');
697
+ const ack = await renderClient.reloadWithMode(reloadAction.mode, devSnapshot);
698
+ ackMessage = ack.message;
699
+ setToast(ack.message);
700
+ setProgress(0.2);
701
+ });
702
+ setToast(ackMessage);
703
+ if (reloadAction.mode === 'bundle')
704
+ closeSoon(2500);
705
+ }
706
+ catch (err) {
707
+ const message = err instanceof Error ? err.message : String(err);
708
+ setError(message);
709
+ setToast(message.toLowerCase().indexOf('session expired') >= 0
710
+ ? 'Session expired. Sign in again.'
711
+ : message);
712
+ await loadSelectedMachine();
713
+ setProgress(null);
714
+ }
715
+ finally {
716
+ if (mountedRef.current) {
717
+ setAction('idle');
718
+ setReloadingId(null);
719
+ void refreshDevSnapshot();
720
+ }
721
+ }
722
+ }, [closeSoon, devSnapshot, loadSelectedMachine, refreshDevSnapshot, runWithReconnect]);
723
+ /** Kept for the legacy one-tap path (BlackBox command, chat Reload button). */
477
724
  const handleHotReload = (0, react_1.useCallback)(async () => {
478
725
  setAction('hot-reloading');
479
726
  setError(null);
@@ -499,8 +746,11 @@ const FeedbackModal = () => {
499
746
  // map this app to its MobileProject scan entry without needing
500
747
  // `yaver dev start` to have been run.
501
748
  let ackMessage = 'Reload request acknowledged.';
502
- await runWithReconnect(async (client) => {
503
- const ack = await client.reloadApp('bundle');
749
+ await runWithReconnect(async () => {
750
+ const renderClient = YaverFeedback_1.YaverFeedback.getRenderP2PClient();
751
+ if (!renderClient)
752
+ throw new Error('Render machine is not connected yet.');
753
+ const ack = await renderClient.reloadApp('bundle');
504
754
  ackMessage = ack.message;
505
755
  setToast(ack.message);
506
756
  setProgress(0.2);
@@ -579,19 +829,36 @@ const FeedbackModal = () => {
579
829
  try {
580
830
  const { Dimensions } = require('react-native');
581
831
  const { width, height } = Dimensions.get('window');
832
+ const cfg = YaverFeedback_1.YaverFeedback.getConfig();
833
+ const identity = (0, P2PClient_1.resolveReportIdentity)({
834
+ projectName: cfg?.projectName,
835
+ bundleId: cfg?.bundleId,
836
+ surface: cfg?.surface,
837
+ surfaces: cfg?.surfaces,
838
+ stack: cfg?.stack,
839
+ stacks: cfg?.stacks,
840
+ testSurfaces: cfg?.testSurfaces,
841
+ feedbackSdk: cfg?.feedbackSdk,
842
+ feedbackTransport: cfg?.feedbackTransport,
843
+ voiceCapabilities: cfg?.voiceCapabilities,
844
+ sttProvider: cfg?.sttProvider,
845
+ ttsProvider: cfg?.ttsProvider,
846
+ });
582
847
  const deviceInfo = {
583
848
  platform: react_native_1.Platform.OS,
584
849
  osVersion: String(react_native_1.Platform.Version),
585
850
  model: react_native_1.Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
586
851
  screenWidth: width,
587
852
  screenHeight: height,
853
+ appName: identity.appName,
588
854
  };
589
855
  const capturedErrors = YaverFeedback_1.YaverFeedback.getCapturedErrors();
590
856
  const bundle = {
591
857
  metadata: {
592
858
  timestamp: new Date().toISOString(),
593
- device: deviceInfo,
594
- app: {},
859
+ deviceInfo,
860
+ app: identity.app,
861
+ project: identity.project,
595
862
  userNote: '[Screenshot + Fix]',
596
863
  },
597
864
  screenshots: [path],
@@ -711,6 +978,7 @@ const FeedbackModal = () => {
711
978
  taskId: result.taskId,
712
979
  initialPrompt: promptText,
713
980
  project: identity.projectName,
981
+ projectPath: identity.projectPath,
714
982
  runner: preferredRunner ?? undefined,
715
983
  model: preferredModel ?? undefined,
716
984
  });
@@ -731,6 +999,7 @@ const FeedbackModal = () => {
731
999
  }, [closeSoon, isRecordingVideo, lastVideo]);
732
1000
  */
733
1001
  const busy = action !== 'idle';
1002
+ const machineRouting = YaverFeedback_1.YaverFeedback.getMachineRouting();
734
1003
  const readyRunnerCount = runnerCards.filter((row) => row.ready || row.authConfigured).length;
735
1004
  const missingRunnerCount = runnerCards.filter((row) => !row.installed).length;
736
1005
  const needsAuthRunnerCount = runnerCards.filter((row) => row.installed && !row.authConfigured && !row.ready).length;
@@ -738,14 +1007,18 @@ const FeedbackModal = () => {
738
1007
  // for the live chat screen. The chat manages its own SSE
739
1008
  // subscription, multi-turn follow-ups, and Reload button. Closing
740
1009
  // the chat returns to idle and clears the active vibe.
741
- if (visible && activeVibe) {
1010
+ if (activeVibe) {
742
1011
  const client = YaverFeedback_1.YaverFeedback.getP2PClient();
743
1012
  return (<>
744
1013
  <AuthOverlay_1.AuthOverlay />
745
1014
  <QuickActionIcon_1.QuickActionIcon />
746
- <react_native_1.Modal visible={visible} animationType="slide" transparent onRequestClose={() => setActiveVibe(null)}>
747
- {client ? (<VibeChatScreen_1.VibeChatScreen client={client} initialTaskId={activeVibe.taskId} initialUserPrompt={activeVibe.initialPrompt} project={activeVibe.project} runner={activeVibe.runner} model={activeVibe.model} onClose={() => setActiveVibe(null)} onReload={async () => {
748
- const c = YaverFeedback_1.YaverFeedback.getP2PClient();
1015
+ <YaverModeBadgeGate />
1016
+ <react_native_1.Modal visible={visible} animationType="slide" transparent onRequestClose={handleClose}>
1017
+ {client ? (<VibeChatScreen_1.VibeChatScreen client={client} initialTaskId={activeVibe.taskId} initialUserPrompt={activeVibe.initialPrompt} project={activeVibe.project} projectPath={activeVibe.projectPath} runner={activeVibe.runner} model={activeVibe.model} voiceInputEnabled={YaverFeedback_1.YaverFeedback.getConfig()?.voiceInputEnabled === true} onClose={() => setActiveVibe(null)} onNewTopic={() => {
1018
+ setActiveVibe(null);
1019
+ setShowVibeInput(true);
1020
+ }} onMinimize={handleClose} codingMachine={YaverFeedback_1.YaverFeedback.getMachineRouting().codingDeviceId} renderMachine={YaverFeedback_1.YaverFeedback.getMachineRouting().renderDeviceId} onReload={async () => {
1021
+ const c = YaverFeedback_1.YaverFeedback.getRenderP2PClient();
749
1022
  if (!c)
750
1023
  throw new Error('Not connected');
751
1024
  await c.reloadApp();
@@ -756,6 +1029,7 @@ const FeedbackModal = () => {
756
1029
  return (<>
757
1030
  <AuthOverlay_1.AuthOverlay />
758
1031
  <QuickActionIcon_1.QuickActionIcon />
1032
+ <YaverModeBadgeGate />
759
1033
  {visible && (<react_native_1.Modal visible={visible} animationType="slide" transparent onRequestClose={handleClose}>
760
1034
  <react_native_1.Pressable style={styles.overlay} onPress={handleClose}>
761
1035
  <react_native_1.KeyboardAvoidingView behavior={react_native_1.Platform.OS === 'ios' ? 'padding' : 'height'} keyboardVerticalOffset={react_native_1.Platform.OS === 'ios' ? 12 : 0} style={styles.kbAvoider} pointerEvents="box-none">
@@ -778,19 +1052,80 @@ const FeedbackModal = () => {
778
1052
  e.stopPropagation();
779
1053
  react_native_1.Keyboard.dismiss();
780
1054
  }}>
781
- <react_native_1.ScrollView style={styles.scroll} contentContainerStyle={[
1055
+ <react_native_1.ScrollView ref={scrollRef} style={styles.scroll} contentContainerStyle={[
782
1056
  styles.scrollContent,
783
1057
  showVibeInput && keyboardInset > 0
784
1058
  ? { paddingBottom: 8 + keyboardInset }
785
1059
  : null,
786
- ]} keyboardShouldPersistTaps="handled" keyboardDismissMode={react_native_1.Platform.OS === 'ios' ? 'interactive' : 'on-drag'}>
1060
+ ]} keyboardShouldPersistTaps="handled" keyboardDismissMode={react_native_1.Platform.OS === 'ios' ? 'interactive' : 'on-drag'} contentInsetAdjustmentBehavior="always" automaticallyAdjustKeyboardInsets={react_native_1.Platform.OS === 'ios'}>
787
1061
  <react_native_1.View style={styles.header}>
788
- <react_native_1.Text style={styles.title}>Send Feedback</react_native_1.Text>
1062
+ <react_native_1.Text style={styles.title}>
1063
+ {dogfoodActive ? `${YaverFeedback_1.YaverFeedback.getDogfoodStatus().label || 'App'} Developer Mode` : 'Send Feedback'}
1064
+ </react_native_1.Text>
789
1065
  <react_native_1.Pressable onPress={handleClose} hitSlop={12} style={styles.closeBtn} accessibilityRole="button" accessibilityLabel="Close">
790
1066
  <react_native_1.Text style={styles.closeIcon}>×</react_native_1.Text>
791
1067
  </react_native_1.Pressable>
792
1068
  </react_native_1.View>
793
1069
 
1070
+ <react_native_1.View style={styles.tabs} accessibilityRole="tablist">
1071
+ {['chat', 'settings'].map((tab) => (<react_native_1.Pressable key={tab} onPress={() => setActiveTab(tab)} style={[styles.tab, activeTab === tab && styles.tabSelected]} accessibilityRole="tab" accessibilityState={{ selected: activeTab === tab }}>
1072
+ <react_native_1.Text style={[styles.tabText, activeTab === tab && styles.tabTextSelected]}>{tab === 'chat' ? 'Chat' : 'Settings'}</react_native_1.Text>
1073
+ </react_native_1.Pressable>))}
1074
+ </react_native_1.View>
1075
+
1076
+ <react_native_1.View style={[styles.tabContent, activeTab !== 'settings' && styles.hidden]}>
1077
+ <>
1078
+ {YaverFeedback_1.YaverFeedback.getDogfoodOnboarding() ? (<react_native_1.View style={styles.dogfoodWizard}>
1079
+ <react_native_1.Text style={styles.dogfoodWizardTitle}>Dogfood this app</react_native_1.Text>
1080
+ <react_native_1.Text style={styles.dogfoodWizardHint}>
1081
+ OAuth ✓ · machine {machineCard.device ? '✓' : 'required'} · installation {dogfoodEnrollment?.status || 'checking'}
1082
+ </react_native_1.Text>
1083
+ {dogfoodEnrollment?.installationId ? (<react_native_1.Text selectable style={styles.dogfoodInstallationId}>
1084
+ This device · {dogfoodEnrollment.installationId}
1085
+ </react_native_1.Text>) : null}
1086
+ {dogfoodEnrollment?.status !== 'active' ? (<react_native_1.View style={styles.dogfoodPendingBox}>
1087
+ <react_native_1.Text style={styles.dogfoodPendingText}>
1088
+ {dogfoodEnrollment?.error
1089
+ || 'Approve this installation from Yaver → Settings → Third-party app testing. A UUID alone never grants access.'}
1090
+ </react_native_1.Text>
1091
+ <react_native_1.Pressable onPress={() => void loadDogfoodOnboarding()} style={({ pressed }) => [styles.runnerRefreshBtn, pressed && styles.buttonPressed]}>
1092
+ <react_native_1.Text style={styles.runnerRefreshBtnText}>{dogfoodSetupLoading ? 'Checking…' : 'Check approval'}</react_native_1.Text>
1093
+ </react_native_1.Pressable>
1094
+ </react_native_1.View>) : (<>
1095
+ <react_native_1.Text style={styles.dogfoodStepLabel}>Project on selected machine</react_native_1.Text>
1096
+ <react_native_1.ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.dogfoodChoiceRow}>
1097
+ {dogfoodProjects.map((project) => (<react_native_1.Pressable key={project.path} onPress={() => {
1098
+ setDogfoodProject(project);
1099
+ setDogfoodLane((0, DogfoodRuntime_1.defaultDogfoodLane)(project.framework || YaverFeedback_1.YaverFeedback.getDogfoodOnboarding()?.framework || 'expo'));
1100
+ setDogfoodRuntime(null);
1101
+ }} style={[styles.dogfoodChoice, dogfoodProject?.path === project.path && styles.dogfoodChoiceSelected]}>
1102
+ <react_native_1.Text style={[styles.dogfoodChoiceText, dogfoodProject?.path === project.path && styles.dogfoodChoiceTextSelected]}>{project.name}</react_native_1.Text>
1103
+ </react_native_1.Pressable>))}
1104
+ </react_native_1.ScrollView>
1105
+ <react_native_1.Text style={styles.dogfoodStepLabel}>Runtime lane</react_native_1.Text>
1106
+ <react_native_1.View style={styles.dogfoodChoiceRow}>
1107
+ {(0, DogfoodRuntime_1.dogfoodLaneOptions)(dogfoodProject?.framework || YaverFeedback_1.YaverFeedback.getDogfoodOnboarding()?.framework || 'expo', { nativeRuntimeAvailable: dogfoodNativeAvailable }).map((option) => (<react_native_1.Pressable key={option.lane} onPress={() => option.supported && setDogfoodLane(option.lane)} style={[
1108
+ styles.dogfoodChoice,
1109
+ dogfoodLane === option.lane && styles.dogfoodChoiceSelected,
1110
+ !option.supported && styles.actionBtnDisabled,
1111
+ ]} accessibilityState={{ disabled: !option.supported, selected: dogfoodLane === option.lane }}>
1112
+ <react_native_1.Text style={[styles.dogfoodChoiceText, dogfoodLane === option.lane && styles.dogfoodChoiceTextSelected]}>{option.label}</react_native_1.Text>
1113
+ </react_native_1.Pressable>))}
1114
+ </react_native_1.View>
1115
+ <react_native_1.Text style={styles.dogfoodWizardHint}>
1116
+ Runner: {[preferredRunner || 'automatic', preferredModel].filter(Boolean).join(' · ')}. Change it under Coding Agents below.
1117
+ </react_native_1.Text>
1118
+ <ActionRow label={dogfoodRuntime && !['idle', 'ready', 'failed', 'stopped'].includes(dogfoodRuntime.phase) ? dogfoodRuntime.message : 'Start Dogfood'} tint="#818cf8" onPress={() => void startDogfoodRuntime()} disabled={!dogfoodProject || !!(dogfoodRuntime && !['idle', 'ready', 'failed', 'stopped'].includes(dogfoodRuntime.phase))} busy={!!dogfoodRuntime && !['idle', 'ready', 'failed', 'stopped'].includes(dogfoodRuntime.phase)}/>
1119
+ {dogfoodRuntime ? (<react_native_1.View style={styles.dogfoodConsole}>
1120
+ <react_native_1.Text style={styles.dogfoodConsoleStatus}>{dogfoodRuntime.message}</react_native_1.Text>
1121
+ {dogfoodRuntime.logs.slice(-80).map((line, index) => (<react_native_1.Text key={`${line.at}-${index}`} selectable style={styles.dogfoodConsoleLine}>{line.text}</react_native_1.Text>))}
1122
+ {dogfoodRuntime.failure ? (<react_native_1.Text style={styles.dogfoodConsoleError}>{dogfoodRuntime.failure.error}{'\n'}{dogfoodRuntime.failure.remedy}</react_native_1.Text>) : null}
1123
+ {dogfoodRuntime.result?.url ? (<react_native_1.Pressable onPress={() => void react_native_1.Linking.openURL(dogfoodRuntime.result.url)} style={styles.dogfoodOpenPreview}>
1124
+ <react_native_1.Text style={styles.dogfoodOpenPreviewText}>Open dogfooded app</react_native_1.Text>
1125
+ </react_native_1.Pressable>) : null}
1126
+ </react_native_1.View>) : null}
1127
+ </>)}
1128
+ </react_native_1.View>) : null}
794
1129
  <react_native_1.Pressable onPress={() => {
795
1130
  if (!YaverFeedback_1.YaverFeedback.isAuthed()) {
796
1131
  YaverFeedback_1.YaverFeedback.showLogin();
@@ -811,7 +1146,7 @@ const FeedbackModal = () => {
811
1146
  machineCard.status === 'attention' && styles.machineDotAttention,
812
1147
  machineCard.status === 'offline' && styles.machineDotOffline,
813
1148
  ]}/>
814
- <react_native_1.Text style={styles.machineLabel}>Selected Machine</react_native_1.Text>
1149
+ <react_native_1.Text style={styles.machineLabel}>Coding Machine</react_native_1.Text>
815
1150
  </react_native_1.View>
816
1151
  <react_native_1.Text style={styles.machineAction}>
817
1152
  {machineCard.loading ? 'Refreshing…' : 'Change'}
@@ -823,6 +1158,17 @@ const FeedbackModal = () => {
823
1158
  <react_native_1.Text style={styles.machineMeta}>{machineCard.detail}</react_native_1.Text>
824
1159
  </react_native_1.Pressable>
825
1160
 
1161
+ <react_native_1.View style={styles.machineRoutes}>
1162
+ <react_native_1.View style={styles.machineRouteCard}>
1163
+ <react_native_1.Text style={styles.machineRouteLabel}>Coding machine</react_native_1.Text>
1164
+ <react_native_1.Text style={styles.machineRouteValue} numberOfLines={1}>{machineRouting.codingDeviceId || machineCard.title}</react_native_1.Text>
1165
+ </react_native_1.View>
1166
+ <react_native_1.View style={styles.machineRouteCard}>
1167
+ <react_native_1.Text style={styles.machineRouteLabel}>Render machine</react_native_1.Text>
1168
+ <react_native_1.Text style={styles.machineRouteValue} numberOfLines={1}>{machineRouting.renderDeviceId || machineRouting.codingDeviceId || machineCard.title}</react_native_1.Text>
1169
+ </react_native_1.View>
1170
+ </react_native_1.View>
1171
+
826
1172
  <react_native_1.View style={styles.runnerSection}>
827
1173
  <react_native_1.View style={styles.runnerSectionHeader}>
828
1174
  <react_native_1.View style={{ flex: 1 }}>
@@ -843,6 +1189,13 @@ const FeedbackModal = () => {
843
1189
  </react_native_1.Pressable>
844
1190
  </react_native_1.View>
845
1191
 
1192
+ <react_native_1.View style={styles.routingSummary}>
1193
+ <react_native_1.Text style={styles.routingSummaryLabel}>Vibing uses</react_native_1.Text>
1194
+ <react_native_1.Text style={styles.routingSummaryValue} numberOfLines={1}>
1195
+ {[preferredRunner || 'automatic runner', preferredModel].filter(Boolean).join(' · ')}
1196
+ </react_native_1.Text>
1197
+ </react_native_1.View>
1198
+
846
1199
  {runnerCards.map((row) => (<react_native_1.View key={row.id} style={[
847
1200
  styles.runnerCard,
848
1201
  row.tone === 'ok' && styles.runnerCardOk,
@@ -861,13 +1214,29 @@ const FeedbackModal = () => {
861
1214
  {row.statusLine}
862
1215
  </react_native_1.Text>
863
1216
  </react_native_1.View>
864
- {row.actionRunner ? (<react_native_1.Pressable onPress={() => setRunnerAuthModal(row.actionRunner ?? null)} style={({ pressed }) => [
865
- styles.runnerActionBtn,
866
- pressed && styles.buttonPressed,
867
- ]} accessibilityRole="button" accessibilityLabel={`${row.actionLabel} ${row.name}`}>
868
- <react_native_1.Text style={styles.runnerActionBtnText}>{row.actionLabel}</react_native_1.Text>
869
- </react_native_1.Pressable>) : null}
1217
+ <react_native_1.View style={styles.runnerCardActions}>
1218
+ {row.ready ? (<react_native_1.Pressable onPress={() => {
1219
+ const nextModel = row.models?.find((model) => model.isDefault)?.id || row.models?.[0]?.id || '';
1220
+ setPreferredRunnerState(row.id);
1221
+ setPreferredModelState(nextModel);
1222
+ void (0, preferences_1.setPreferredRunner)(row.id);
1223
+ void (0, preferences_1.setPreferredModel)(nextModel || null);
1224
+ }} style={({ pressed }) => [styles.runnerActionBtn, preferredRunner === row.id && styles.runnerActionBtnSelected, pressed && styles.buttonPressed]} accessibilityRole="button" accessibilityLabel={`Use ${row.name} for Vibing`}>
1225
+ <react_native_1.Text style={styles.runnerActionBtnText}>{preferredRunner === row.id ? 'Using' : 'Use'}</react_native_1.Text>
1226
+ </react_native_1.Pressable>) : null}
1227
+ {row.actionRunner ? (<react_native_1.Pressable onPress={() => setRunnerAuthModal(row.actionRunner ?? null)} style={({ pressed }) => [styles.runnerActionBtn, pressed && styles.buttonPressed]} accessibilityRole="button" accessibilityLabel={`${row.actionLabel} ${row.name}`}>
1228
+ <react_native_1.Text style={styles.runnerActionBtnText}>{row.actionLabel}</react_native_1.Text>
1229
+ </react_native_1.Pressable>) : null}
1230
+ </react_native_1.View>
870
1231
  </react_native_1.View>
1232
+ {preferredRunner === row.id && (row.models?.length || 0) > 0 ? (<react_native_1.ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.modelChoiceRow}>
1233
+ {row.models.map((model) => (<react_native_1.Pressable key={model.id} onPress={() => {
1234
+ setPreferredModelState(model.id);
1235
+ void (0, preferences_1.setPreferredModel)(model.id);
1236
+ }} style={[styles.modelChoice, preferredModel === model.id && styles.modelChoiceSelected]} accessibilityRole="button" accessibilityLabel={`Use ${model.name || model.id} model`}>
1237
+ <react_native_1.Text style={[styles.modelChoiceText, preferredModel === model.id && styles.modelChoiceTextSelected]}>{model.name || model.id}</react_native_1.Text>
1238
+ </react_native_1.Pressable>))}
1239
+ </react_native_1.ScrollView>) : null}
871
1240
  {row.detail ? (<react_native_1.Text style={styles.runnerCardDetail}>{row.detail}</react_native_1.Text>) : null}
872
1241
  </react_native_1.View>))}
873
1242
 
@@ -926,9 +1295,31 @@ const FeedbackModal = () => {
926
1295
  </react_native_1.View>
927
1296
  </react_native_1.View>
928
1297
 
929
- {/* 1. Hot Reload — the common path */}
930
- <ActionRow label={action === 'hot-reloading' ? 'Reloading…' : 'Hot Reload'} tint="#fbbf24" onPress={handleHotReload} disabled={busy} busy={action === 'hot-reloading'}/>
1298
+ {/* 1. Reload — Hot / Full / Rebuild Bundle.
1299
+ Rendered from the shared decision seam, so a production
1300
+ build (__DEV__ false) renders nothing here at all, and a
1301
+ blocked action shows greyed WITH its reason underneath
1302
+ rather than vanishing. */}
1303
+ {availableReloadActions.map((reloadAction) => (<react_native_1.View key={reloadAction.id} style={styles.reloadRow}>
1304
+ <ActionRow label={reloadingId === reloadAction.id
1305
+ ? `${reloadAction.label}…`
1306
+ : reloadAction.label} tint={reloadAction.id === 'rebuild' ? '#38bdf8' : '#fbbf24'} onPress={() => {
1307
+ void handleReloadAction(reloadAction);
1308
+ }}
1309
+ // Never `disabled` at the Pressable level for a blocked
1310
+ // action: we WANT the tap so we can say why. Only a
1311
+ // genuinely busy modal blocks the press.
1312
+ disabled={busy && reloadingId !== reloadAction.id} busy={reloadingId === reloadAction.id}/>
1313
+ <react_native_1.Text style={styles.reloadHint}>
1314
+ {reloadAction.enabled
1315
+ ? reloadAction.hint
1316
+ : reloadAction.disabledReason}
1317
+ </react_native_1.Text>
1318
+ </react_native_1.View>))}
1319
+ </>
1320
+ </react_native_1.View>
931
1321
 
1322
+ <react_native_1.View style={[styles.tabContent, activeTab !== 'chat' && styles.hidden]}>
932
1323
  {/* 3. Vibing — expands to an input box on first tap
933
1324
  so the user says WHAT they want to vibe on, just
934
1325
  like the Yaver mobile app's Vibing tab. Second
@@ -955,7 +1346,7 @@ const FeedbackModal = () => {
955
1346
  </react_native_1.Text>)}
956
1347
 
957
1348
  {/* Screenshot & Fix */}
958
- <ActionRow label={action === 'capturing' ? 'Working…' : 'Screenshot & Fix'} tint="#22c55e" onPress={handleScreenshotAndFix} disabled={busy} busy={action === 'capturing'}/>
1349
+ {!dogfoodActive ? <ActionRow label={action === 'capturing' ? 'Working…' : 'Screenshot & Fix'} tint="#22c55e" onPress={handleScreenshotAndFix} disabled={busy} busy={action === 'capturing'}/> : null}
959
1350
 
960
1351
  {/* Deploy — opens an inline panel that talks to
961
1352
  /fleet/deploy-options on the agent and lets the user
@@ -963,7 +1354,8 @@ const FeedbackModal = () => {
963
1354
  it on. Capabilities (e.g. "Linux can't TestFlight")
964
1355
  come from the agent's doctor probes — no client-side
965
1356
  platform smarts here. */}
966
- {!showDeploy ? (<ActionRow label="Deploy" tint="#7f8cf7" onPress={() => setShowDeploy(true)} disabled={busy}/>) : (<DeployPanel_1.DeployPanel onClose={() => setShowDeploy(false)}/>)}
1357
+ {!dogfoodActive && (!showDeploy ? (<ActionRow label="Deploy" tint="#7f8cf7" onPress={() => setShowDeploy(true)} disabled={busy}/>) : (<DeployPanel_1.DeployPanel onClose={() => setShowDeploy(false)}/>))}
1358
+ </react_native_1.View>
967
1359
 
968
1360
  {progress !== null && (<react_native_1.View style={styles.progressTrack}>
969
1361
  <react_native_1.View style={[
@@ -1004,6 +1396,33 @@ const ActionRow = ({ label, tint, onPress, disabled, busy, }) => (<react_native_
1004
1396
  {busy ? (<react_native_1.ActivityIndicator color={tint} size="small"/>) : (<react_native_1.Text style={[styles.actionText, { color: tint }]}>{label}</react_native_1.Text>)}
1005
1397
  </react_native_1.Pressable>);
1006
1398
  const styles = react_native_1.StyleSheet.create({
1399
+ dogfoodWizard: { gap: 10, borderRadius: 14, borderWidth: 1, borderColor: 'rgba(129,140,248,0.38)', backgroundColor: 'rgba(129,140,248,0.08)', padding: 13 },
1400
+ dogfoodWizardTitle: { color: '#222229', fontSize: 17, fontWeight: '800' },
1401
+ dogfoodWizardHint: { color: '#6f6f7b', fontSize: 12, lineHeight: 17 },
1402
+ dogfoodInstallationId: { color: '#6555df', fontSize: 11, fontFamily: react_native_1.Platform.select({ ios: 'Menlo', android: 'monospace', default: 'monospace' }) },
1403
+ dogfoodPendingBox: { gap: 9, borderRadius: 11, padding: 10, backgroundColor: 'rgba(245,158,11,0.10)' },
1404
+ dogfoodPendingText: { color: '#8a5a10', fontSize: 12, lineHeight: 17 },
1405
+ dogfoodStepLabel: { color: '#555561', fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.7 },
1406
+ dogfoodChoiceRow: { flexDirection: 'row', gap: 7 },
1407
+ dogfoodChoice: { borderRadius: 9, borderWidth: 1, borderColor: '#d8d8e3', backgroundColor: '#fff', paddingHorizontal: 10, paddingVertical: 8 },
1408
+ dogfoodChoiceSelected: { borderColor: '#818cf8', backgroundColor: 'rgba(129,140,248,0.18)' },
1409
+ dogfoodChoiceText: { color: '#666671', fontSize: 12, fontWeight: '700' },
1410
+ dogfoodChoiceTextSelected: { color: '#5645d8' },
1411
+ dogfoodConsole: { maxHeight: 260, gap: 4, borderRadius: 11, padding: 10, backgroundColor: '#15151b' },
1412
+ dogfoodConsoleStatus: { color: '#a5b4fc', fontSize: 12, fontWeight: '800' },
1413
+ dogfoodConsoleLine: { color: '#d1d5db', fontSize: 10, lineHeight: 14, fontFamily: react_native_1.Platform.select({ ios: 'Menlo', android: 'monospace', default: 'monospace' }) },
1414
+ dogfoodConsoleError: { color: '#fca5a5', fontSize: 11, lineHeight: 16, marginTop: 5 },
1415
+ dogfoodOpenPreview: { alignSelf: 'flex-start', borderRadius: 9, paddingHorizontal: 11, paddingVertical: 8, marginTop: 6, backgroundColor: '#6555df' },
1416
+ dogfoodOpenPreviewText: { color: '#fff', fontSize: 12, fontWeight: '800' },
1417
+ reloadRow: {
1418
+ gap: 4,
1419
+ },
1420
+ reloadHint: {
1421
+ color: '#8b8b93',
1422
+ fontSize: 11,
1423
+ lineHeight: 15,
1424
+ paddingHorizontal: 4,
1425
+ },
1007
1426
  vibeInputRow: {
1008
1427
  backgroundColor: 'rgba(129,140,248,0.08)',
1009
1428
  borderColor: 'rgba(129,140,248,0.4)',
@@ -1013,7 +1432,7 @@ const styles = react_native_1.StyleSheet.create({
1013
1432
  gap: 10,
1014
1433
  },
1015
1434
  vibeInput: {
1016
- color: '#fff',
1435
+ color: '#24242b',
1017
1436
  fontSize: 15,
1018
1437
  minHeight: 64,
1019
1438
  textAlignVertical: 'top',
@@ -1064,7 +1483,7 @@ const styles = react_native_1.StyleSheet.create({
1064
1483
  justifyContent: 'flex-end',
1065
1484
  },
1066
1485
  modal: {
1067
- backgroundColor: '#141422',
1486
+ backgroundColor: '#f8f8fb',
1068
1487
  borderTopLeftRadius: 22,
1069
1488
  borderTopRightRadius: 22,
1070
1489
  padding: 22,
@@ -1088,7 +1507,7 @@ const styles = react_native_1.StyleSheet.create({
1088
1507
  title: {
1089
1508
  fontSize: 20,
1090
1509
  fontWeight: '700',
1091
- color: '#fff',
1510
+ color: '#17171d',
1092
1511
  },
1093
1512
  closeBtn: {
1094
1513
  width: 36,
@@ -1096,10 +1515,10 @@ const styles = react_native_1.StyleSheet.create({
1096
1515
  borderRadius: 18,
1097
1516
  alignItems: 'center',
1098
1517
  justifyContent: 'center',
1099
- backgroundColor: 'rgba(255,255,255,0.08)',
1518
+ backgroundColor: '#ededf3',
1100
1519
  },
1101
1520
  closeIcon: {
1102
- color: '#fff',
1521
+ color: '#656570',
1103
1522
  fontSize: 22,
1104
1523
  lineHeight: 24,
1105
1524
  fontWeight: '400',
@@ -1118,12 +1537,19 @@ const styles = react_native_1.StyleSheet.create({
1118
1537
  fontSize: 15,
1119
1538
  fontWeight: '700',
1120
1539
  },
1540
+ tabs: { flexDirection: 'row', gap: 6, padding: 3, borderRadius: 12, backgroundColor: '#ededf3' },
1541
+ tab: { flex: 1, minHeight: 36, borderRadius: 9, alignItems: 'center', justifyContent: 'center' },
1542
+ tabSelected: { backgroundColor: '#fff' },
1543
+ tabText: { color: '#858590', fontSize: 12, fontWeight: '700' },
1544
+ tabTextSelected: { color: '#6252e8' },
1545
+ tabContent: { gap: 12 },
1546
+ hidden: { display: 'none' },
1121
1547
  machineCard: {
1122
1548
  borderRadius: 14,
1123
1549
  borderWidth: 1,
1124
1550
  padding: 14,
1125
- backgroundColor: 'rgba(255,255,255,0.04)',
1126
- borderColor: 'rgba(255,255,255,0.12)',
1551
+ backgroundColor: '#fff',
1552
+ borderColor: '#e1e1e8',
1127
1553
  },
1128
1554
  machineCardLive: {
1129
1555
  backgroundColor: 'rgba(34,197,94,0.10)',
@@ -1137,6 +1563,10 @@ const styles = react_native_1.StyleSheet.create({
1137
1563
  backgroundColor: 'rgba(239,68,68,0.10)',
1138
1564
  borderColor: 'rgba(239,68,68,0.35)',
1139
1565
  },
1566
+ machineRoutes: { flexDirection: 'row', gap: 8 },
1567
+ machineRouteCard: { flex: 1, minWidth: 0, borderRadius: 11, padding: 10, gap: 3, backgroundColor: '#fff', borderWidth: 1, borderColor: '#e1e1e8' },
1568
+ machineRouteLabel: { color: '#7b7b86', fontSize: 10, fontWeight: '700', textTransform: 'uppercase' },
1569
+ machineRouteValue: { color: '#222229', fontSize: 12, fontWeight: '700' },
1140
1570
  machineHeader: {
1141
1571
  flexDirection: 'row',
1142
1572
  alignItems: 'center',
@@ -1164,7 +1594,7 @@ const styles = react_native_1.StyleSheet.create({
1164
1594
  backgroundColor: '#ef4444',
1165
1595
  },
1166
1596
  machineLabel: {
1167
- color: '#cbd5e1',
1597
+ color: '#73737e',
1168
1598
  fontSize: 12,
1169
1599
  fontWeight: '700',
1170
1600
  textTransform: 'uppercase',
@@ -1176,12 +1606,12 @@ const styles = react_native_1.StyleSheet.create({
1176
1606
  fontWeight: '700',
1177
1607
  },
1178
1608
  machineName: {
1179
- color: '#fff',
1609
+ color: '#222229',
1180
1610
  fontSize: 16,
1181
1611
  fontWeight: '700',
1182
1612
  },
1183
1613
  machineMeta: {
1184
- color: '#cbd5e1',
1614
+ color: '#73737e',
1185
1615
  fontSize: 12,
1186
1616
  marginTop: 4,
1187
1617
  lineHeight: 17,
@@ -1196,33 +1626,33 @@ const styles = react_native_1.StyleSheet.create({
1196
1626
  gap: 10,
1197
1627
  },
1198
1628
  runnerSectionTitle: {
1199
- color: '#f8fafc',
1629
+ color: '#222229',
1200
1630
  fontSize: 16,
1201
1631
  fontWeight: '700',
1202
1632
  },
1203
1633
  runnerSectionSubtitle: {
1204
1634
  marginTop: 2,
1205
- color: '#94a3b8',
1635
+ color: '#83838e',
1206
1636
  fontSize: 12,
1207
1637
  },
1208
1638
  runnerRefreshBtn: {
1209
1639
  borderRadius: 10,
1210
1640
  borderWidth: 1,
1211
- borderColor: 'rgba(148,163,184,0.22)',
1212
- backgroundColor: 'rgba(15,23,42,0.65)',
1641
+ borderColor: '#dedee7',
1642
+ backgroundColor: '#fff',
1213
1643
  paddingHorizontal: 10,
1214
1644
  paddingVertical: 8,
1215
1645
  },
1216
1646
  runnerRefreshBtnText: {
1217
- color: '#cbd5e1',
1647
+ color: '#5f5f69',
1218
1648
  fontSize: 12,
1219
1649
  fontWeight: '600',
1220
1650
  },
1221
1651
  runnerCard: {
1222
1652
  borderRadius: 12,
1223
1653
  borderWidth: 1,
1224
- borderColor: 'rgba(148,163,184,0.14)',
1225
- backgroundColor: 'rgba(15,23,42,0.45)',
1654
+ borderColor: '#e2e2e9',
1655
+ backgroundColor: '#fff',
1226
1656
  paddingHorizontal: 12,
1227
1657
  paddingVertical: 11,
1228
1658
  gap: 6,
@@ -1244,15 +1674,16 @@ const styles = react_native_1.StyleSheet.create({
1244
1674
  alignItems: 'center',
1245
1675
  gap: 10,
1246
1676
  },
1677
+ runnerCardActions: { flexDirection: 'row', alignItems: 'center', gap: 6 },
1247
1678
  runnerCardTitle: {
1248
- color: '#f8fafc',
1679
+ color: '#222229',
1249
1680
  fontSize: 14,
1250
1681
  fontWeight: '700',
1251
1682
  },
1252
1683
  runnerCardStatus: {
1253
1684
  marginTop: 2,
1254
1685
  fontSize: 12,
1255
- color: '#cbd5e1',
1686
+ color: '#666671',
1256
1687
  },
1257
1688
  runnerCardStatusOk: {
1258
1689
  color: '#86efac',
@@ -1264,7 +1695,7 @@ const styles = react_native_1.StyleSheet.create({
1264
1695
  color: '#fca5a5',
1265
1696
  },
1266
1697
  runnerCardDetail: {
1267
- color: '#94a3b8',
1698
+ color: '#858590',
1268
1699
  fontSize: 11,
1269
1700
  lineHeight: 16,
1270
1701
  },
@@ -1281,6 +1712,15 @@ const styles = react_native_1.StyleSheet.create({
1281
1712
  fontSize: 12,
1282
1713
  fontWeight: '700',
1283
1714
  },
1715
+ runnerActionBtnSelected: { borderColor: '#818cf8', backgroundColor: 'rgba(79,70,229,0.44)' },
1716
+ routingSummary: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingHorizontal: 2 },
1717
+ routingSummaryLabel: { color: '#7c7c87', fontSize: 11, fontWeight: '600' },
1718
+ routingSummaryValue: { flex: 1, color: '#6555df', fontSize: 12, fontWeight: '700', textAlign: 'right' },
1719
+ modelChoiceRow: { gap: 6, paddingTop: 2 },
1720
+ modelChoice: { borderRadius: 9, borderWidth: 1, borderColor: 'rgba(148,163,184,0.18)', paddingHorizontal: 9, paddingVertical: 6 },
1721
+ modelChoiceSelected: { borderColor: '#818cf8', backgroundColor: 'rgba(79,70,229,0.30)' },
1722
+ modelChoiceText: { color: '#73737e', fontSize: 11 },
1723
+ modelChoiceTextSelected: { color: '#5e4ce6', fontWeight: '700' },
1284
1724
  runnerSectionError: {
1285
1725
  color: '#fca5a5',
1286
1726
  fontSize: 12,
@@ -1292,7 +1732,7 @@ const styles = react_native_1.StyleSheet.create({
1292
1732
  progressTrack: {
1293
1733
  height: 6,
1294
1734
  borderRadius: 3,
1295
- backgroundColor: 'rgba(255,255,255,0.08)',
1735
+ backgroundColor: '#e5e5eb',
1296
1736
  overflow: 'hidden',
1297
1737
  marginTop: 4,
1298
1738
  },
@@ -1319,10 +1759,10 @@ const styles = react_native_1.StyleSheet.create({
1319
1759
  paddingVertical: 6,
1320
1760
  paddingHorizontal: 10,
1321
1761
  borderRadius: 10,
1322
- backgroundColor: 'rgba(255,255,255,0.05)',
1762
+ backgroundColor: '#ededf3',
1323
1763
  },
1324
1764
  quickIconToggleText: {
1325
- color: '#cbd5e1',
1765
+ color: '#5f5f69',
1326
1766
  fontSize: 12,
1327
1767
  fontWeight: '700',
1328
1768
  },
@@ -1334,25 +1774,25 @@ const styles = react_native_1.StyleSheet.create({
1334
1774
  padding: 12,
1335
1775
  },
1336
1776
  quickIconNoteText: {
1337
- color: '#fde68a',
1777
+ color: '#8a5a12',
1338
1778
  fontSize: 12,
1339
1779
  lineHeight: 17,
1340
1780
  },
1341
1781
  iconSelector: {
1342
1782
  borderRadius: 12,
1343
1783
  borderWidth: 1,
1344
- borderColor: 'rgba(255,255,255,0.09)',
1345
- backgroundColor: 'rgba(255,255,255,0.03)',
1784
+ borderColor: '#e1e1e8',
1785
+ backgroundColor: '#fff',
1346
1786
  padding: 12,
1347
1787
  gap: 10,
1348
1788
  },
1349
1789
  iconSelectorTitle: {
1350
- color: '#f8fafc',
1790
+ color: '#222229',
1351
1791
  fontSize: 13,
1352
1792
  fontWeight: '700',
1353
1793
  },
1354
1794
  iconSelectorText: {
1355
- color: '#cbd5e1',
1795
+ color: '#73737e',
1356
1796
  fontSize: 12,
1357
1797
  lineHeight: 17,
1358
1798
  },
@@ -1366,8 +1806,8 @@ const styles = react_native_1.StyleSheet.create({
1366
1806
  minWidth: 84,
1367
1807
  borderRadius: 12,
1368
1808
  borderWidth: 1,
1369
- borderColor: 'rgba(255,255,255,0.08)',
1370
- backgroundColor: 'rgba(255,255,255,0.02)',
1809
+ borderColor: '#e4e4ea',
1810
+ backgroundColor: '#f8f8fb',
1371
1811
  paddingVertical: 10,
1372
1812
  paddingHorizontal: 8,
1373
1813
  alignItems: 'center',
@@ -1394,7 +1834,7 @@ const styles = react_native_1.StyleSheet.create({
1394
1834
  fontWeight: '700',
1395
1835
  },
1396
1836
  iconOptionText: {
1397
- color: '#e2e8f0',
1837
+ color: '#555561',
1398
1838
  fontSize: 11,
1399
1839
  fontWeight: '600',
1400
1840
  },
@@ -1402,14 +1842,14 @@ const styles = react_native_1.StyleSheet.create({
1402
1842
  marginTop: 4,
1403
1843
  borderRadius: 14,
1404
1844
  borderWidth: 1,
1405
- borderColor: 'rgba(255,255,255,0.1)',
1845
+ borderColor: '#e1e1e8',
1406
1846
  paddingVertical: 15,
1407
1847
  alignItems: 'center',
1408
1848
  justifyContent: 'center',
1409
- backgroundColor: 'rgba(255,255,255,0.04)',
1849
+ backgroundColor: '#fff',
1410
1850
  },
1411
1851
  cancelBtnText: {
1412
- color: '#e5e7eb',
1852
+ color: '#555561',
1413
1853
  fontSize: 15,
1414
1854
  fontWeight: '700',
1415
1855
  },