yaver-feedback-react-native 0.9.2 → 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 (103) hide show
  1. package/README.md +102 -1
  2. package/dist/AuthOverlay.d.ts +1 -14
  3. package/dist/AuthOverlay.js +9 -62
  4. package/dist/Discovery.js +0 -6
  5. package/dist/DogfoodRuntime.d.ts +124 -0
  6. package/dist/DogfoodRuntime.js +273 -0
  7. package/dist/FeedbackModal.js +347 -61
  8. package/dist/LoginScreen.d.ts +1 -5
  9. package/dist/LoginScreen.js +2 -6
  10. package/dist/MachinePickerScreen.d.ts +1 -3
  11. package/dist/MachinePickerScreen.js +6 -18
  12. package/dist/P2PClient.d.ts +116 -3
  13. package/dist/P2PClient.js +273 -3
  14. package/dist/P2PDogfoodDriver.d.ts +12 -0
  15. package/dist/P2PDogfoodDriver.js +118 -0
  16. package/dist/PairDeviceModal.d.ts +2 -3
  17. package/dist/VibeChatScreen.d.ts +11 -1
  18. package/dist/VibeChatScreen.js +200 -41
  19. package/dist/YaverFeedback.d.ts +34 -0
  20. package/dist/YaverFeedback.js +124 -19
  21. package/dist/YaverModeBadge.d.ts +22 -0
  22. package/dist/YaverModeBadge.js +219 -0
  23. package/dist/__tests__/AuthDevices.test.js +1 -48
  24. package/dist/__tests__/DogfoodRuntime.test.d.ts +1 -0
  25. package/dist/__tests__/DogfoodRuntime.test.js +116 -0
  26. package/dist/__tests__/P2PDogfoodDriver.test.d.ts +1 -0
  27. package/dist/__tests__/P2PDogfoodDriver.test.js +64 -0
  28. package/dist/__tests__/ReportIdentity.test.d.ts +25 -1
  29. package/dist/__tests__/ReportIdentity.test.js +34 -22
  30. package/dist/__tests__/YaverFeedback.test.js +13 -3
  31. package/dist/__tests__/deviceDogfood.test.d.ts +1 -0
  32. package/dist/__tests__/deviceDogfood.test.js +86 -0
  33. package/dist/__tests__/dogfoodPolicy.test.d.ts +1 -0
  34. package/dist/__tests__/dogfoodPolicy.test.js +33 -0
  35. package/dist/_core/ansi.d.ts +117 -0
  36. package/dist/_core/ansi.js +468 -0
  37. package/dist/_core/ansi.test.d.ts +1 -0
  38. package/dist/_core/ansi.test.js +225 -0
  39. package/dist/_core/buildFeedbackPrompt.d.ts +4 -9
  40. package/dist/_core/buildFeedbackPrompt.js +18 -72
  41. package/dist/_core/constants.d.ts +19 -6
  42. package/dist/_core/constants.js +20 -7
  43. package/dist/_core/device.d.ts +9 -23
  44. package/dist/_core/device.js +13 -28
  45. package/dist/_core/endpoints.d.ts +0 -7
  46. package/dist/_core/endpoints.js +0 -7
  47. package/dist/_core/index.d.ts +4 -0
  48. package/dist/_core/index.js +4 -0
  49. package/dist/_core/remoteless.d.ts +44 -0
  50. package/dist/_core/remoteless.js +75 -0
  51. package/dist/_core/trace.d.ts +47 -0
  52. package/dist/_core/trace.js +38 -0
  53. package/dist/_core/trace.test.d.ts +1 -0
  54. package/dist/_core/trace.test.js +60 -0
  55. package/dist/auth.d.ts +3 -60
  56. package/dist/auth.js +6 -88
  57. package/dist/deviceDogfood.d.ts +53 -0
  58. package/dist/deviceDogfood.js +137 -0
  59. package/dist/dogfoodPolicy.d.ts +25 -0
  60. package/dist/dogfoodPolicy.js +24 -0
  61. package/dist/index.d.ts +13 -4
  62. package/dist/index.js +24 -8
  63. package/dist/reloadActions.js +2 -2
  64. package/dist/types.d.ts +50 -7
  65. package/package.json +12 -3
  66. package/src/AuthOverlay.tsx +20 -106
  67. package/src/Discovery.ts +0 -6
  68. package/src/DogfoodRuntime.ts +373 -0
  69. package/src/FeedbackModal.tsx +445 -67
  70. package/src/LoginScreen.tsx +2 -22
  71. package/src/MachinePickerScreen.tsx +6 -21
  72. package/src/P2PClient.ts +347 -4
  73. package/src/P2PDogfoodDriver.ts +132 -0
  74. package/src/PairDeviceModal.tsx +2 -3
  75. package/src/VibeChatScreen.tsx +232 -42
  76. package/src/YaverFeedback.ts +133 -22
  77. package/src/YaverModeBadge.tsx +234 -0
  78. package/src/__tests__/AuthDevices.test.ts +1 -52
  79. package/src/__tests__/DogfoodRuntime.test.ts +135 -0
  80. package/src/__tests__/P2PDogfoodDriver.test.ts +80 -0
  81. package/src/__tests__/ReportIdentity.test.ts +36 -27
  82. package/src/__tests__/YaverFeedback.test.ts +15 -3
  83. package/src/__tests__/deviceDogfood.test.ts +77 -0
  84. package/src/__tests__/dogfoodPolicy.test.ts +34 -0
  85. package/src/_core/ansi.test.ts +250 -0
  86. package/src/_core/ansi.ts +475 -0
  87. package/src/_core/buildFeedbackPrompt.ts +18 -95
  88. package/src/_core/constants.ts +20 -6
  89. package/src/_core/device.ts +13 -30
  90. package/src/_core/endpoints.ts +0 -7
  91. package/src/_core/index.ts +4 -0
  92. package/src/_core/remoteless.ts +110 -0
  93. package/src/_core/trace.test.ts +58 -0
  94. package/src/_core/trace.ts +75 -0
  95. package/src/auth.ts +7 -156
  96. package/src/deviceDogfood.ts +171 -0
  97. package/src/dogfoodPolicy.ts +40 -0
  98. package/src/index.ts +40 -11
  99. package/src/reloadActions.ts +2 -2
  100. package/src/types.ts +45 -7
  101. package/dist/GuestOnboardingScreen.d.ts +0 -8
  102. package/dist/GuestOnboardingScreen.js +0 -282
  103. package/src/GuestOnboardingScreen.tsx +0 -307
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createP2PDogfoodDriver = createP2PDogfoodDriver;
4
+ const DogfoodRuntime_1 = require("./DogfoodRuntime");
5
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
6
+ function reportedPreview(status) {
7
+ return String(status.previewUrl || status.bundleUrl || '').trim();
8
+ }
9
+ /**
10
+ * Ready-to-use driver for app owners who authenticate with Yaver's normal
11
+ * account flow. Their UI owns the trigger and preview surface; this adapter
12
+ * owns the existing Projects endpoints and raw build log stream.
13
+ */
14
+ function createP2PDogfoodDriver(client, options = {}) {
15
+ const startupTimeoutMs = Math.max(1, options.startupTimeoutMs ?? 155000);
16
+ const pollIntervalMs = Math.max(1, options.pollIntervalMs ?? 600);
17
+ return {
18
+ async prepare(context) {
19
+ const close = client.subscribeDogfoodDevEvents((event) => (0, DogfoodRuntime_1.runtimeLogLinesFromDevEvent)(event).forEach((line) => context.log(line)), (health) => {
20
+ if (health)
21
+ context.log({ text: `[logs] ${health.message}`, at: Date.now(), stream: 'system' });
22
+ });
23
+ context.registerCleanup(close, 'transient');
24
+ },
25
+ async start(context) {
26
+ const { project } = context;
27
+ if (project.lane === 'webrtc') {
28
+ context.setPhase('starting', `Finding a native runtime for ${project.name}…`);
29
+ const capabilities = await client.getDogfoodRemoteRuntimeCapabilities(project.workDir, project.framework);
30
+ const nativeTargets = capabilities.targets.filter((target) => target.enabled && target.id !== 'browser-window');
31
+ const target = project.nativeTargetId
32
+ ? nativeTargets.find((candidate) => candidate.id === project.nativeTargetId)
33
+ : nativeTargets[0];
34
+ if (!target) {
35
+ throw new DogfoodRuntime_1.DogfoodRuntimeError({
36
+ code: 'DOGFOOD_NATIVE_RUNTIME_UNAVAILABLE',
37
+ error: project.nativeTargetId
38
+ ? `Native runtime ${project.nativeTargetId} is not available.`
39
+ : 'No native simulator, emulator, or device runtime is available.',
40
+ remedy: 'Start or install a native runtime on the selected machine, then retry WebRTC; Browser lane remains available.',
41
+ retryable: true,
42
+ });
43
+ }
44
+ context.setPhase('starting', `Starting ${target.label} over WebRTC…`);
45
+ const session = await client.startDogfoodRemoteRuntime(project.workDir, project.framework, target.id);
46
+ context.registerCleanup(() => client.stopDogfoodRemoteRuntime(session.id), 'session');
47
+ return {
48
+ lane: 'webrtc',
49
+ sessionId: session.id,
50
+ metadata: { target, session, framework: project.framework, workDir: project.workDir },
51
+ };
52
+ }
53
+ context.setPhase(project.lane === 'hermes' ? 'compiling' : 'starting', project.lane === 'hermes' ? `Compiling ${project.name} with Hermes…` : `Starting ${project.name} in the browser…`);
54
+ const status = await client.startDogfoodDevServer({
55
+ framework: project.framework,
56
+ workDir: project.workDir,
57
+ lane: project.lane,
58
+ });
59
+ // Browser Dogfood owns a long-lived dev server. Hermes is a one-shot
60
+ // build + delivery to the Yaver container; calling /dev/stop when that
61
+ // guest exits can kill an unrelated browser preview on the same box.
62
+ if (project.lane === 'browser') {
63
+ context.registerCleanup(() => client.stopDogfoodDevServer(), 'session');
64
+ }
65
+ if (status.error) {
66
+ throw new DogfoodRuntime_1.DogfoodRuntimeError({
67
+ code: 'DOGFOOD_DEV_SERVER_FAILED', error: status.error,
68
+ remedy: 'Fix the named project/runtime error, then retry Dogfood.', retryable: true,
69
+ });
70
+ }
71
+ let latest = status;
72
+ let reported = reportedPreview(latest);
73
+ if (project.lane === 'browser' && !reported) {
74
+ context.setPhase('compiling', `Compiling ${project.name} for the browser…`);
75
+ const deadline = Date.now() + startupTimeoutMs;
76
+ while (context.isCurrent() && Date.now() < deadline) {
77
+ await delay(pollIntervalMs);
78
+ const polled = await client.getDogfoodDevServerStatus();
79
+ if (!polled)
80
+ continue;
81
+ latest = polled;
82
+ if (latest.error && !latest.building) {
83
+ throw new DogfoodRuntime_1.DogfoodRuntimeError({
84
+ code: 'DOGFOOD_DEV_SERVER_FAILED', error: latest.error,
85
+ remedy: 'Fix the named project/runtime error, then retry Dogfood.', retryable: true,
86
+ });
87
+ }
88
+ reported = reportedPreview(latest);
89
+ if (reported && (latest.running || latest.serving))
90
+ break;
91
+ }
92
+ if (!context.isCurrent()) {
93
+ throw new DogfoodRuntime_1.DogfoodRuntimeError({
94
+ code: 'DOGFOOD_ATTEMPT_REPLACED', error: 'A newer Dogfood attempt replaced this compile.',
95
+ remedy: 'Wait for the newer attempt.', retryable: true,
96
+ });
97
+ }
98
+ if (!reported) {
99
+ throw new DogfoodRuntime_1.DogfoodRuntimeError({
100
+ code: 'DOGFOOD_NO_RENDER_URL',
101
+ error: `The dev server did not report a browser preview URL within ${Math.ceil(startupTimeoutMs / 1000)} seconds.`,
102
+ remedy: 'Read the live npm/compiler output above, fix the named failure, then retry. Flutter uses `-d web-server`; Expo/RN needs react-native-web.',
103
+ retryable: true,
104
+ });
105
+ }
106
+ }
107
+ return {
108
+ lane: project.lane,
109
+ url: reported ? client.resolveDogfoodUrl(reported) : undefined,
110
+ metadata: {
111
+ framework: latest.framework || project.framework,
112
+ workDir: latest.workDir || project.workDir,
113
+ ...(project.lane === 'hermes' ? { delivered: latest.running === true } : {}),
114
+ },
115
+ };
116
+ },
117
+ };
118
+ }
@@ -16,9 +16,8 @@ import type { RemoteDevice } from './auth';
16
16
  * report `needsAuth=false` in /devices/list.
17
17
  *
18
18
  * This avoids making the user bounce to the Yaver mobile app just to
19
- * adopt a machine. Works for owners and shared-scope guests since the
20
- * pair endpoint accepts any valid Convex session that matches the
21
- * expected account type.
19
+ * adopt one of their own machines. The pair endpoint verifies the signed-in
20
+ * account as the machine owner.
22
21
  */
23
22
  export interface PairDeviceModalProps {
24
23
  device: RemoteDevice | null;
@@ -18,8 +18,18 @@ interface Props {
18
18
  /** Optional context forwarded to the voice stream so the agent runs
19
19
  * the task against the right project / runner / model. */
20
20
  project?: string;
21
+ projectPath?: string;
21
22
  model?: string;
22
23
  runner?: string;
24
+ /** Show the optional voice/STT controls. Defaults to keyboard-only. */
25
+ voiceInputEnabled?: boolean;
26
+ /** Standalone SDK hosts use this to fold back to the floating Y without
27
+ * destroying the live task subscription or transcript state. */
28
+ onMinimize?: () => void;
29
+ codingMachine?: string;
30
+ renderMachine?: string;
31
+ /** Return to the SDK prompt composer to create a separate task/topic. */
32
+ onNewTopic?: () => void;
23
33
  }
24
- export declare function VibeChatScreen({ client, initialTaskId, initialUserPrompt, onClose, onReload, project, model, runner, }: Props): React.JSX.Element;
34
+ export declare function VibeChatScreen({ client, initialTaskId, initialUserPrompt, onClose, onReload, project, projectPath, model, runner, voiceInputEnabled, onMinimize, codingMachine, renderMachine, onNewTopic, }: Props): React.JSX.Element;
25
35
  export {};
@@ -55,7 +55,8 @@ const react_1 = __importStar(require("react"));
55
55
  const react_native_1 = require("react-native");
56
56
  const voice_1 = require("./voice");
57
57
  const capture_1 = require("./capture");
58
- function VibeChatScreen({ client, initialTaskId, initialUserPrompt, onClose, onReload, project, model, runner, }) {
58
+ function VibeChatScreen({ client, initialTaskId, initialUserPrompt, onClose, onReload, project, projectPath, model, runner, voiceInputEnabled = false, onMinimize, codingMachine, renderMachine, onNewTopic, }) {
59
+ const [activeTab, setActiveTab] = (0, react_1.useState)('chat');
59
60
  const [taskId, setTaskId] = (0, react_1.useState)(initialTaskId);
60
61
  const [turns, setTurns] = (0, react_1.useState)(() => [
61
62
  {
@@ -72,12 +73,27 @@ function VibeChatScreen({ client, initialTaskId, initialUserPrompt, onClose, onR
72
73
  },
73
74
  ]);
74
75
  const [streamBuffer, setStreamBuffer] = (0, react_1.useState)('');
76
+ const [streamEpoch, setStreamEpoch] = (0, react_1.useState)(0);
75
77
  const [status, setStatus] = (0, react_1.useState)('running');
76
78
  const [followUp, setFollowUp] = (0, react_1.useState)('');
77
79
  const [isResuming, setIsResuming] = (0, react_1.useState)(false);
78
80
  const [isReloading, setIsReloading] = (0, react_1.useState)(false);
81
+ const [reloadQueued, setReloadQueued] = (0, react_1.useState)(false);
82
+ const [threads, setThreads] = (0, react_1.useState)(() => [{
83
+ id: initialTaskId,
84
+ title: initialUserPrompt || 'New topic',
85
+ status: 'running',
86
+ }]);
79
87
  const scrollRef = (0, react_1.useRef)(null);
80
88
  const abortRef = (0, react_1.useRef)(null);
89
+ const refreshThreads = (0, react_1.useCallback)(async () => {
90
+ try {
91
+ const list = await client.listVibeThreads({ projectName: project, projectPath });
92
+ setThreads(list.slice(0, 12));
93
+ }
94
+ catch { /* history is advisory; keep the active conversation usable */ }
95
+ }, [client, project, projectPath]);
96
+ (0, react_1.useEffect)(() => { void refreshThreads(); }, [refreshThreads]);
81
97
  // ── Voice vibe coding ──────────────────────────────────────────────
82
98
  const [voiceState, setVoiceState] = (0, react_1.useState)('idle');
83
99
  const [voiceAvailable, setVoiceAvailable] = (0, react_1.useState)(false);
@@ -97,6 +113,8 @@ function VibeChatScreen({ client, initialTaskId, initialUserPrompt, onClose, onR
97
113
  const close = client.streamTaskOutput(taskId, (line) => {
98
114
  if (!live)
99
115
  return;
116
+ if (line.includes('YAVER_THREAD_TITLE'))
117
+ return;
100
118
  // Filter our internal error sentinel from the SSE helper.
101
119
  if (line.startsWith('__error__:')) {
102
120
  setStatus('failed');
@@ -109,7 +127,7 @@ function VibeChatScreen({ client, initialTaskId, initialUserPrompt, onClose, onR
109
127
  }, (terminal) => {
110
128
  if (!live)
111
129
  return;
112
- setStatus(terminal === 'completed' ? 'done' : 'failed');
130
+ setStatus(terminal === 'completed' || terminal === 'review' ? 'done' : 'failed');
113
131
  // Move the buffered stream into a real assistant turn so the
114
132
  // user sees a stable render and can scroll back, then clear
115
133
  // the buffer for any follow-up.
@@ -127,6 +145,8 @@ function VibeChatScreen({ client, initialTaskId, initialUserPrompt, onClose, onR
127
145
  return next;
128
146
  });
129
147
  setStreamBuffer('');
148
+ setThreads((prev) => prev.map((thread) => thread.id === taskId ? { ...thread, status: terminal } : thread));
149
+ void refreshThreads();
130
150
  });
131
151
  abortRef.current = close;
132
152
  return () => {
@@ -136,7 +156,44 @@ function VibeChatScreen({ client, initialTaskId, initialUserPrompt, onClose, onR
136
156
  }
137
157
  catch { /* ignore */ }
138
158
  };
139
- }, [client, taskId]);
159
+ }, [client, refreshThreads, streamEpoch, taskId]);
160
+ const selectThread = (0, react_1.useCallback)(async (thread) => {
161
+ abortRef.current?.();
162
+ try {
163
+ const task = await client.getVibeThread(thread.id);
164
+ setTaskId(task.id);
165
+ setStatus(task.status === 'running' || task.status === 'queued' ? 'running' : task.status === 'completed' || task.status === 'review' ? 'done' : 'failed');
166
+ setStreamBuffer('');
167
+ setTurns((task.turns || []).filter((turn) => turn.role === 'user' || turn.role === 'assistant').map((turn, index) => ({
168
+ id: `${task.id}-${index}`,
169
+ role: turn.role,
170
+ text: turn.content,
171
+ timestamp: turn.timestamp ? new Date(turn.timestamp).getTime() : Date.now() + index,
172
+ })));
173
+ }
174
+ catch (error) {
175
+ setTurns((prev) => [...prev, { id: `thread-error-${Date.now()}`, role: 'status', text: error instanceof Error ? error.message : 'Could not open topic', timestamp: Date.now() }]);
176
+ }
177
+ }, [client]);
178
+ const removeThread = (0, react_1.useCallback)((thread) => {
179
+ const message = thread.status === 'running' || thread.status === 'queued'
180
+ ? 'This also stops the coding turn that is still running.'
181
+ : 'This removes the conversation from your history.';
182
+ react_native_1.Alert.alert('Remove topic?', message, [
183
+ { text: 'Cancel', style: 'cancel' },
184
+ { text: 'Remove', style: 'destructive', onPress: () => {
185
+ void client.deleteVibeThread(thread.id).then(() => {
186
+ setThreads((prev) => prev.filter((item) => item.id !== thread.id));
187
+ if (thread.id === taskId)
188
+ onNewTopic?.();
189
+ }).catch((error) => {
190
+ setTurns((prev) => [...prev, { id: `delete-error-${Date.now()}`, role: 'status', text: error instanceof Error ? error.message : 'Could not remove topic', timestamp: Date.now() }]);
191
+ });
192
+ } },
193
+ ]);
194
+ }, [client, onNewTopic, taskId]);
195
+ const runningThread = threads.find((thread) => thread.status === 'running' || thread.status === 'queued');
196
+ const codingLocked = status === 'running' || !!runningThread;
140
197
  // Auto-scroll the transcript when new content lands.
141
198
  (0, react_1.useEffect)(() => {
142
199
  const t = setTimeout(() => {
@@ -146,7 +203,7 @@ function VibeChatScreen({ client, initialTaskId, initialUserPrompt, onClose, onR
146
203
  }, [streamBuffer, turns]);
147
204
  const handleSendFollowUp = (0, react_1.useCallback)(async () => {
148
205
  const text = followUp.trim();
149
- if (!text || isResuming)
206
+ if (!text || isResuming || codingLocked)
150
207
  return;
151
208
  setIsResuming(true);
152
209
  // Add user turn immediately for snappy UX.
@@ -157,17 +214,13 @@ function VibeChatScreen({ client, initialTaskId, initialUserPrompt, onClose, onR
157
214
  ]);
158
215
  setFollowUp('');
159
216
  setStatus('running');
217
+ setThreads((prev) => prev.map((thread) => thread.id === taskId ? { ...thread, status: 'running' } : thread));
160
218
  setStreamBuffer('');
161
219
  try {
162
220
  await client.resumeTask({ taskId, userPrompt: text });
163
- // resumeTask reuses the same taskId, so the SSE subscription
164
- // above will pick up the new output stream automatically. To
165
- // force a fresh subscription we momentarily flip taskId to a
166
- // sentinel and back; cleaner than tearing down + re-attaching
167
- // the SSE manually.
168
- const same = taskId;
169
- setTaskId(`${same}#`);
170
- setTimeout(() => setTaskId(same), 0);
221
+ // resumeTask reuses the same task id; advance the subscription epoch so
222
+ // the existing SSE path reconnects without inventing an invalid id.
223
+ setStreamEpoch((value) => value + 1);
171
224
  }
172
225
  catch (e) {
173
226
  setStatus('failed');
@@ -184,10 +237,14 @@ function VibeChatScreen({ client, initialTaskId, initialUserPrompt, onClose, onR
184
237
  finally {
185
238
  setIsResuming(false);
186
239
  }
187
- }, [client, followUp, isResuming, taskId]);
240
+ }, [client, codingLocked, followUp, isResuming, taskId]);
188
241
  const handleReload = (0, react_1.useCallback)(async () => {
189
242
  if (isReloading || !onReload)
190
243
  return;
244
+ if (status === 'running') {
245
+ setReloadQueued(true);
246
+ return;
247
+ }
191
248
  setIsReloading(true);
192
249
  try {
193
250
  await onReload();
@@ -206,13 +263,21 @@ function VibeChatScreen({ client, initialTaskId, initialUserPrompt, onClose, onR
206
263
  finally {
207
264
  setIsReloading(false);
208
265
  }
209
- }, [isReloading, onReload]);
266
+ }, [isReloading, onReload, status]);
267
+ (0, react_1.useEffect)(() => {
268
+ if (!reloadQueued || status === 'running' || isReloading)
269
+ return;
270
+ setReloadQueued(false);
271
+ void handleReload();
272
+ }, [handleReload, isReloading, reloadQueued, status]);
210
273
  // Probe whether voice is usable: deps present (expo-av + expo-file-
211
274
  // system + buffer) AND the agent reports STT/TTS ready. Hide the mic
212
275
  // entirely otherwise so users never tap a dead button.
213
276
  (0, react_1.useEffect)(() => {
214
277
  let cancelled = false;
215
278
  (async () => {
279
+ if (!voiceInputEnabled)
280
+ return;
216
281
  if (!(0, capture_1.isVoiceCaptureSupported)() || !(0, voice_1.isVoiceStreamSupported)())
217
282
  return;
218
283
  try {
@@ -235,7 +300,7 @@ function VibeChatScreen({ client, initialTaskId, initialUserPrompt, onClose, onR
235
300
  catch { /* leave hidden */ }
236
301
  })();
237
302
  return () => { cancelled = true; };
238
- }, [client]);
303
+ }, [client, voiceInputEnabled]);
239
304
  (0, react_1.useEffect)(() => () => { voiceSessionRef.current?.close(); }, []);
240
305
  // Local TTS: the agent streams no audio for "local"/"device" engines,
241
306
  // so the client speaks the result text with the device synthesizer.
@@ -371,15 +436,50 @@ function VibeChatScreen({ client, initialTaskId, initialUserPrompt, onClose, onR
371
436
  thinking: 'thinking…',
372
437
  speaking: 'speaking…',
373
438
  };
374
- return (<react_native_1.View style={styles.container}>
439
+ return (<react_native_1.KeyboardAvoidingView style={styles.container} behavior={react_native_1.Platform.OS === 'ios' ? 'padding' : 'height'} keyboardVerticalOffset={0}>
375
440
  <react_native_1.View style={styles.header}>
376
- <react_native_1.Text style={styles.title}>Vibe</react_native_1.Text>
377
- {onClose && (<react_native_1.TouchableOpacity onPress={onClose} accessibilityLabel="Close vibe chat">
378
- <react_native_1.Text style={styles.close}>✕</react_native_1.Text>
379
- </react_native_1.TouchableOpacity>)}
441
+ <react_native_1.View>
442
+ <react_native_1.Text style={styles.title}>Vibing</react_native_1.Text>
443
+ <react_native_1.Text style={styles.routeCaption} numberOfLines={1}>
444
+ {[runner || 'automatic runner', model].filter(Boolean).join(' · ')}
445
+ </react_native_1.Text>
446
+ </react_native_1.View>
447
+ <react_native_1.View style={styles.headerActions}>
448
+ {onMinimize ? <react_native_1.TouchableOpacity onPress={onMinimize} accessibilityLabel="Minimize Vibing"><react_native_1.Text style={styles.close}>−</react_native_1.Text></react_native_1.TouchableOpacity> : null}
449
+ {onClose ? <react_native_1.TouchableOpacity onPress={onClose} accessibilityLabel="End vibe chat"><react_native_1.Text style={styles.close}>✕</react_native_1.Text></react_native_1.TouchableOpacity> : null}
450
+ </react_native_1.View>
380
451
  </react_native_1.View>
381
452
 
382
- <react_native_1.ScrollView ref={scrollRef} style={styles.transcript} contentContainerStyle={styles.transcriptContent} keyboardShouldPersistTaps="handled">
453
+ <react_native_1.View style={styles.tabs} accessibilityRole="tablist">
454
+ {['chat', 'settings'].map((tab) => (<react_native_1.TouchableOpacity key={tab} onPress={() => setActiveTab(tab)} style={[styles.tab, activeTab === tab && styles.tabSelected]} accessibilityRole="tab" accessibilityState={{ selected: activeTab === tab }}>
455
+ <react_native_1.Text style={[styles.tabText, activeTab === tab && styles.tabTextSelected]}>{tab === 'chat' ? 'Chat' : 'Settings'}</react_native_1.Text>
456
+ </react_native_1.TouchableOpacity>))}
457
+ </react_native_1.View>
458
+
459
+ {activeTab === 'chat' && threads.length > 1 ? <react_native_1.View style={styles.topicRailWrap}>
460
+ <react_native_1.ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.topicRail}>
461
+ <react_native_1.TouchableOpacity style={styles.newTopicCard} onPress={onNewTopic} accessibilityLabel="Start a new topic">
462
+ <react_native_1.Text style={styles.newTopicPlus}>+</react_native_1.Text>
463
+ <react_native_1.Text style={styles.newTopicText}>New</react_native_1.Text>
464
+ </react_native_1.TouchableOpacity>
465
+ {threads.map((thread) => (<react_native_1.TouchableOpacity key={thread.id} style={[styles.topicCard, thread.id === taskId && styles.topicCardSelected]} onPress={() => { void selectThread(thread); }} accessibilityLabel={`Open ${thread.title}`}>
466
+ <react_native_1.View style={styles.topicCardTopline}>
467
+ <react_native_1.View style={[styles.topicDot, (thread.status === 'running' || thread.status === 'queued') && styles.topicDotLive]}/>
468
+ <react_native_1.Text style={styles.topicStatus}>{thread.status === 'completed' ? 'done' : thread.status}</react_native_1.Text>
469
+ <react_native_1.TouchableOpacity onPress={() => removeThread(thread)} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} accessibilityLabel={`Remove ${thread.title}`}><react_native_1.Text style={styles.topicRemove}>×</react_native_1.Text></react_native_1.TouchableOpacity>
470
+ </react_native_1.View>
471
+ <react_native_1.Text style={styles.topicTitle} numberOfLines={2}>{thread.title}</react_native_1.Text>
472
+ </react_native_1.TouchableOpacity>))}
473
+ </react_native_1.ScrollView>
474
+ </react_native_1.View> : activeTab === 'chat' && threads.length === 1 ? <react_native_1.View style={styles.singleTopicBar}>
475
+ <react_native_1.TouchableOpacity style={styles.singleTopicTitleButton} onPress={() => { void selectThread(threads[0]); }} accessibilityLabel={`Open ${threads[0].title}`}>
476
+ <react_native_1.Text style={styles.singleTopicTitle} numberOfLines={1}>{threads[0].title || 'Current topic'}</react_native_1.Text>
477
+ </react_native_1.TouchableOpacity>
478
+ {onNewTopic ? <react_native_1.TouchableOpacity style={styles.singleTopicAction} onPress={onNewTopic} accessibilityLabel="Start a new topic"><react_native_1.Text style={styles.singleTopicActionText}>+</react_native_1.Text></react_native_1.TouchableOpacity> : null}
479
+ <react_native_1.TouchableOpacity style={styles.singleTopicAction} onPress={() => removeThread(threads[0])} accessibilityLabel={`Remove ${threads[0].title}`}><react_native_1.Text style={styles.singleTopicRemove}>×</react_native_1.Text></react_native_1.TouchableOpacity>
480
+ </react_native_1.View> : null}
481
+
482
+ <react_native_1.ScrollView ref={scrollRef} style={[styles.transcript, activeTab !== 'chat' && styles.hidden]} contentContainerStyle={styles.transcriptContent} keyboardShouldPersistTaps="handled">
383
483
  {turns.map((turn) => (<react_native_1.View key={turn.id} style={[
384
484
  styles.turn,
385
485
  turn.role === 'user' && styles.turnUser,
@@ -401,14 +501,34 @@ function VibeChatScreen({ client, initialTaskId, initialUserPrompt, onClose, onR
401
501
  </react_native_1.View>)}
402
502
  </react_native_1.ScrollView>
403
503
 
404
- <react_native_1.View style={styles.footer}>
405
- {voiceAvailable && voiceState !== 'idle' && (<react_native_1.Text style={styles.engineCaption}>
504
+ <react_native_1.ScrollView style={[styles.settings, activeTab !== 'settings' && styles.hidden]} contentContainerStyle={styles.settingsContent}>
505
+ <react_native_1.Text style={styles.settingsLabel}>Coding route</react_native_1.Text>
506
+ <react_native_1.View style={styles.settingsCard}>
507
+ <react_native_1.Text style={styles.settingsMeta}>Coding machine · {codingMachine || 'selected machine'}</react_native_1.Text>
508
+ <react_native_1.Text style={styles.settingsMeta}>Render machine · {renderMachine || codingMachine || 'same machine'}</react_native_1.Text>
509
+ <react_native_1.Text style={styles.settingsTitle}>{runner || 'Automatic runner'}</react_native_1.Text>
510
+ <react_native_1.Text style={styles.settingsMeta}>{model || 'Runner default model'}</react_native_1.Text>
511
+ {project ? <react_native_1.Text style={styles.settingsMeta}>{project}</react_native_1.Text> : null}
512
+ </react_native_1.View>
513
+ <react_native_1.Text style={styles.settingsLabel}>Preview</react_native_1.Text>
514
+ <react_native_1.View style={styles.settingsCard}>
515
+ <react_native_1.Text style={styles.settingsTitle}>Reload rendered app</react_native_1.Text>
516
+ <react_native_1.Text style={styles.settingsMeta}>{reloadQueued ? 'Reload queued. It will run when coding finishes.' : status === 'running' ? 'Reload will run when the current coding turn finishes.' : 'Apply the latest rendered output without ending this chat.'}</react_native_1.Text>
517
+ {onReload ? <react_native_1.TouchableOpacity style={[styles.settingsButton, isReloading && styles.actionBtnDisabled]} onPress={handleReload} disabled={isReloading}>
518
+ <react_native_1.Text style={styles.settingsButtonText}>{isReloading ? 'Reloading…' : reloadQueued ? 'Reload Queued' : status === 'running' ? 'Queue Reload' : 'Reload'}</react_native_1.Text>
519
+ </react_native_1.TouchableOpacity> : null}
520
+ </react_native_1.View>
521
+ {onMinimize ? <react_native_1.TouchableOpacity style={styles.returnButton} onPress={onMinimize} accessibilityLabel="Return to app and keep Vibing running"><react_native_1.Text style={styles.returnButtonText}>Return to App</react_native_1.Text></react_native_1.TouchableOpacity> : null}
522
+ </react_native_1.ScrollView>
523
+
524
+ <react_native_1.View style={[styles.footer, activeTab !== 'chat' && styles.hidden]}>
525
+ {voiceInputEnabled && voiceAvailable && voiceState !== 'idle' && (<react_native_1.Text style={styles.engineCaption}>
406
526
  {voiceState === 'recording' ? 'listening' : voiceState === 'uploading' ? 'sending' : voiceState === 'thinking' ? 'agent working' : 'speaking'}
407
527
  {activeEngine ? ` · ${activeEngine}` : ` · ${voiceMode === 'flux' ? 'Flux (Deepgram)' : 'Local (whisper)'}`}
408
528
  </react_native_1.Text>)}
409
- <react_native_1.TextInput style={styles.input} value={followUp} onChangeText={setFollowUp} placeholder={status === 'running' ? 'wait for the agent…' : 'follow up…'} placeholderTextColor="#666" editable={status !== 'running' && !isResuming} multiline/>
529
+ <react_native_1.TextInput style={styles.input} value={followUp} onChangeText={setFollowUp} placeholder={codingLocked ? runningThread?.id !== taskId ? 'another topic is coding…' : 'wait for the agent…' : 'follow up…'} placeholderTextColor="#666" editable={!codingLocked && !isResuming} multiline/>
410
530
  <react_native_1.View style={styles.actions}>
411
- {voiceAvailable && (<>
531
+ {voiceInputEnabled && voiceAvailable && (<>
412
532
  {/* Local ↔ Flux engine toggle. Only shows Flux when the
413
533
  agent has a Deepgram key; otherwise the label just
414
534
  states "Local" so the active engine is always clear. */}
@@ -438,18 +558,18 @@ function VibeChatScreen({ client, initialTaskId, initialUserPrompt, onClose, onR
438
558
  <react_native_1.TouchableOpacity style={[
439
559
  styles.actionBtn,
440
560
  styles.sendBtn,
441
- (isResuming || status === 'running' || !followUp.trim()) && styles.actionBtnDisabled,
442
- ]} onPress={handleSendFollowUp} disabled={isResuming || status === 'running' || !followUp.trim()}>
561
+ (isResuming || codingLocked || !followUp.trim()) && styles.actionBtnDisabled,
562
+ ]} onPress={handleSendFollowUp} disabled={isResuming || codingLocked || !followUp.trim()}>
443
563
  <react_native_1.Text style={styles.actionText}>
444
564
  {isResuming ? '…' : '↑ send'}
445
565
  </react_native_1.Text>
446
566
  </react_native_1.TouchableOpacity>
447
567
  </react_native_1.View>
448
568
  </react_native_1.View>
449
- </react_native_1.View>);
569
+ </react_native_1.KeyboardAvoidingView>);
450
570
  }
451
571
  const styles = react_native_1.StyleSheet.create({
452
- container: { flex: 1, backgroundColor: '#0a0a0a' },
572
+ container: { flex: 1, backgroundColor: '#f8f8fb' },
453
573
  header: {
454
574
  flexDirection: 'row',
455
575
  alignItems: 'center',
@@ -458,10 +578,36 @@ const styles = react_native_1.StyleSheet.create({
458
578
  paddingTop: 14,
459
579
  paddingBottom: 8,
460
580
  borderBottomWidth: 1,
461
- borderBottomColor: 'rgba(255,255,255,0.08)',
581
+ borderBottomColor: '#e5e5ec',
462
582
  },
463
- title: { color: '#fff', fontSize: 17, fontWeight: '600' },
464
- close: { color: '#9ca3af', fontSize: 18 },
583
+ title: { color: '#17171d', fontSize: 17, fontWeight: '700' },
584
+ routeCaption: { color: '#858590', fontSize: 10, marginTop: 2, maxWidth: 250 },
585
+ headerActions: { flexDirection: 'row', alignItems: 'center', gap: 22 },
586
+ close: { color: '#656570', fontSize: 20, fontWeight: '700' },
587
+ tabs: { flexDirection: 'row', gap: 6, marginHorizontal: 12, marginTop: 8, padding: 3, borderRadius: 12, backgroundColor: '#ededf3' },
588
+ tab: { flex: 1, minHeight: 34, borderRadius: 9, alignItems: 'center', justifyContent: 'center' },
589
+ tabSelected: { backgroundColor: '#fff' },
590
+ tabText: { color: '#858590', fontSize: 12, fontWeight: '700' },
591
+ tabTextSelected: { color: '#6252e8' },
592
+ topicRailWrap: { minHeight: 102, borderBottomWidth: 1, borderBottomColor: '#e5e5ec' },
593
+ topicRail: { paddingHorizontal: 12, paddingVertical: 10, gap: 9 },
594
+ singleTopicBar: { minHeight: 40, paddingLeft: 12, paddingRight: 8, flexDirection: 'row', alignItems: 'center', borderBottomWidth: 1, borderBottomColor: '#e5e5ec' },
595
+ singleTopicTitleButton: { flex: 1, minWidth: 0, minHeight: 38, justifyContent: 'center' },
596
+ singleTopicTitle: { color: '#656570', fontSize: 11, fontWeight: '700' },
597
+ singleTopicAction: { width: 34, height: 34, alignItems: 'center', justifyContent: 'center' },
598
+ singleTopicActionText: { color: '#6252e8', fontSize: 20, lineHeight: 22 },
599
+ singleTopicRemove: { color: '#9a9aa5', fontSize: 20, lineHeight: 22 },
600
+ newTopicCard: { width: 72, minHeight: 80, borderRadius: 14, alignItems: 'center', justifyContent: 'center', backgroundColor: '#ebe8ff', borderWidth: 1, borderColor: '#d9d3ff' },
601
+ newTopicPlus: { color: '#6252e8', fontSize: 22, lineHeight: 24 },
602
+ newTopicText: { color: '#6252e8', fontSize: 12, fontWeight: '800' },
603
+ topicCard: { width: 172, minHeight: 80, borderRadius: 14, padding: 10, gap: 7, backgroundColor: '#fff', borderWidth: 1.5, borderColor: '#e1e1e8' },
604
+ topicCardSelected: { borderColor: '#7568f8', backgroundColor: '#faf9ff' },
605
+ topicCardTopline: { flexDirection: 'row', alignItems: 'center', gap: 6 },
606
+ topicDot: { width: 7, height: 7, borderRadius: 4, backgroundColor: '#9ca3af' },
607
+ topicDotLive: { backgroundColor: '#f59e0b' },
608
+ topicStatus: { flex: 1, color: '#858590', fontSize: 10, fontWeight: '700', textTransform: 'uppercase' },
609
+ topicRemove: { color: '#9a9aa5', fontSize: 18, lineHeight: 18 },
610
+ topicTitle: { color: '#24242b', fontSize: 13, lineHeight: 17, fontWeight: '700' },
465
611
  transcript: { flex: 1 },
466
612
  transcriptContent: { padding: 12, paddingBottom: 24 },
467
613
  turn: {
@@ -475,8 +621,8 @@ const styles = react_native_1.StyleSheet.create({
475
621
  alignSelf: 'flex-end',
476
622
  },
477
623
  turnAssistant: {
478
- backgroundColor: 'rgba(255,255,255,0.06)',
479
- borderColor: 'rgba(255,255,255,0.10)',
624
+ backgroundColor: '#fff',
625
+ borderColor: '#e4e4ea',
480
626
  borderWidth: 1,
481
627
  alignSelf: 'flex-start',
482
628
  },
@@ -485,7 +631,7 @@ const styles = react_native_1.StyleSheet.create({
485
631
  alignSelf: 'flex-start',
486
632
  paddingHorizontal: 4,
487
633
  },
488
- turnText: { color: '#f1f5f9', fontSize: 14, lineHeight: 20 },
634
+ turnText: { color: '#28282f', fontSize: 14, lineHeight: 20 },
489
635
  spinnerRow: {
490
636
  flexDirection: 'row',
491
637
  alignItems: 'center',
@@ -495,15 +641,17 @@ const styles = react_native_1.StyleSheet.create({
495
641
  spinnerText: { color: '#9ca3af', fontSize: 12, marginLeft: 8 },
496
642
  footer: {
497
643
  borderTopWidth: 1,
498
- borderTopColor: 'rgba(255,255,255,0.08)',
644
+ borderTopColor: '#e5e5ec',
499
645
  padding: 10,
500
646
  },
501
647
  input: {
502
648
  minHeight: 40,
503
649
  maxHeight: 120,
504
- color: '#f1f5f9',
650
+ color: '#28282f',
505
651
  fontSize: 14,
506
- backgroundColor: 'rgba(255,255,255,0.04)',
652
+ backgroundColor: '#fff',
653
+ borderWidth: 1,
654
+ borderColor: '#e2e2e9',
507
655
  borderRadius: 10,
508
656
  paddingHorizontal: 12,
509
657
  paddingVertical: 8,
@@ -520,12 +668,23 @@ const styles = react_native_1.StyleSheet.create({
520
668
  marginLeft: 8,
521
669
  },
522
670
  actionBtnDisabled: { opacity: 0.5 },
523
- reloadBtn: { backgroundColor: 'rgba(255,255,255,0.08)' },
671
+ reloadBtn: { backgroundColor: '#e9e9ef' },
524
672
  voiceBtn: { backgroundColor: 'rgba(16,185,129,0.18)' },
525
673
  voiceBtnActive: { backgroundColor: '#ef4444' },
526
- engineToggle: { backgroundColor: 'rgba(255,255,255,0.06)', marginRight: 'auto', marginLeft: 0 },
527
- engineToggleText: { color: '#cbd5e1', fontSize: 12, fontWeight: '600' },
674
+ engineToggle: { backgroundColor: '#e9e9ef', marginRight: 'auto', marginLeft: 0 },
675
+ engineToggleText: { color: '#555561', fontSize: 12, fontWeight: '600' },
528
676
  engineCaption: { color: '#9ca3af', fontSize: 11, marginBottom: 6, marginLeft: 4 },
529
677
  sendBtn: { backgroundColor: '#7582f5' },
530
678
  actionText: { color: '#fff', fontSize: 13, fontWeight: '600' },
679
+ settings: { flex: 1 },
680
+ settingsContent: { padding: 14, gap: 10, paddingBottom: 30 },
681
+ settingsLabel: { color: '#777782', fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5, marginTop: 4 },
682
+ settingsCard: { borderRadius: 14, padding: 14, gap: 5, backgroundColor: '#fff', borderWidth: 1, borderColor: '#e3e3e9' },
683
+ settingsTitle: { color: '#222229', fontSize: 14, fontWeight: '800' },
684
+ settingsMeta: { color: '#777782', fontSize: 12, lineHeight: 17 },
685
+ settingsButton: { alignSelf: 'flex-start', marginTop: 8, borderRadius: 10, backgroundColor: '#6f58f5', paddingHorizontal: 14, paddingVertical: 9 },
686
+ settingsButtonText: { color: '#fff', fontSize: 12, fontWeight: '800' },
687
+ returnButton: { minHeight: 46, borderRadius: 13, alignItems: 'center', justifyContent: 'center', backgroundColor: '#6f58f5', marginTop: 4 },
688
+ returnButtonText: { color: '#fff', fontWeight: '800' },
689
+ hidden: { display: 'none' },
531
690
  });
@@ -2,6 +2,14 @@ import { FeedbackConfig, CapturedError } from './types';
2
2
  import { P2PClient } from './P2PClient';
3
3
  import { CaptureStoreScreenshotsOptions, CaptureStoreScreenshotsResult } from './storeShots';
4
4
  import { QuickIconColorPreset } from './preferences';
5
+ import { type SDKDogfoodStatus } from './dogfoodPolicy';
6
+ import { type DeviceDogfoodOptions, type DeviceDogfoodSession, type DeviceDogfoodState } from './deviceDogfood';
7
+ export interface DogfoodOnboardingOptions extends DeviceDogfoodOptions {
8
+ /** Hint used to preselect the matching project returned by the owner machine. */
9
+ projectName?: string;
10
+ /** Framework fallback when the machine has not classified the project yet. */
11
+ framework?: string;
12
+ }
5
13
  /**
6
14
  * Main entry point for the Yaver Feedback SDK.
7
15
  * Call `YaverFeedback.init()` once at app startup.
@@ -100,6 +108,12 @@ export declare class YaverFeedback {
100
108
  * an active session; no-ops otherwise.
101
109
  */
102
110
  static showMachinePicker(): void;
111
+ /** Begin the reusable host-app Dogfood wizard. Owner OAuth and machine
112
+ * selection intentionally happen before installation enrollment/runtime
113
+ * controls because starting builds or runners is owner-level authority. */
114
+ static beginDogfoodOnboarding(options: DogfoodOnboardingOptions): void;
115
+ static getDogfoodOnboarding(): DogfoodOnboardingOptions | null;
116
+ static clearDogfoodOnboarding(): void;
103
117
  /**
104
118
  * Update the selected remote device. Resets the cached agent URL so the
105
119
  * next `startReport()` (or FloatingButton press) rediscovers against the
@@ -199,6 +213,20 @@ export declare class YaverFeedback {
199
213
  static isEnabled(): boolean;
200
214
  /** Returns the current config, or null if not initialized. */
201
215
  static getConfig(): FeedbackConfig | null;
216
+ /** Current third-party SDK mode. Fails closed unless enabled + account match. */
217
+ static getDogfoodStatus(): SDKDogfoodStatus;
218
+ /** One-call, account-free Dogfood bootstrap for third-party apps. On first
219
+ * launch it creates/proves the installation key and returns pending; after
220
+ * owner approval the same call obtains a short-lived scoped Yaver session
221
+ * and enables Dogfood UX without the host app implementing OAuth. */
222
+ static enableDeviceDogfood(options: DeviceDogfoodOptions): Promise<{
223
+ status: DeviceDogfoodState;
224
+ installationId: string;
225
+ session: DeviceDogfoodSession | null;
226
+ }>;
227
+ /** Confirmed by the Y badge/UI before this is called. Reverts this SDK to
228
+ * normal Feedback mode for the remainder of the app run. */
229
+ static exitDogfoodMode(): Promise<void>;
202
230
  /** Returns the resolved relay password the SDK is currently using.
203
231
  * Empty string when no relay routing is in play (direct LAN agent
204
232
  * URLs need no password). Callers attaching it to relay-routed
@@ -253,6 +281,12 @@ export declare class YaverFeedback {
253
281
  * Available after init if agentUrl is set, or after first successful discovery.
254
282
  */
255
283
  static getP2PClient(): P2PClient | null;
284
+ /** Renderer/reload route; intentionally distinct from the coding client. */
285
+ static getRenderP2PClient(): P2PClient | null;
286
+ static getMachineRouting(): {
287
+ codingDeviceId?: string;
288
+ renderDeviceId?: string;
289
+ };
256
290
  /**
257
291
  * Record a business event. Routes through BlackBox so the agent
258
292
  * persists it to the analytics ledger (no dashboards — export