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
@@ -113,11 +113,9 @@ const iconStyles = StyleSheet.create({
113
113
 
114
114
  export interface YaverLoginScreenProps {
115
115
  /** Invoked once a session token is issued and the user is loaded. */
116
- onLoggedIn: (token: string, opts?: { inviteCode?: string }) => void;
116
+ onLoggedIn: (token: string) => void;
117
117
  /** Optional cancel button shown in header. */
118
118
  onCancel?: () => void;
119
- /** Optional prefilled guest invite code from config / deep link. */
120
- initialInviteCode?: string;
121
119
  }
122
120
 
123
121
  /**
@@ -128,7 +126,6 @@ export interface YaverLoginScreenProps {
128
126
  export const YaverLoginScreen: React.FC<YaverLoginScreenProps> = ({
129
127
  onLoggedIn,
130
128
  onCancel,
131
- initialInviteCode,
132
129
  }) => {
133
130
  const [busyProvider, setBusyProvider] = useState<OAuthProvider | 'apple' | null>(null);
134
131
  const [showEmailForm, setShowEmailForm] = useState(false);
@@ -137,7 +134,6 @@ export const YaverLoginScreen: React.FC<YaverLoginScreenProps> = ({
137
134
  const [email, setEmail] = useState('');
138
135
  const [password, setPassword] = useState('');
139
136
  const [confirmPassword, setConfirmPassword] = useState('');
140
- const [inviteCode, setInviteCode] = useState((initialInviteCode ?? '').toUpperCase());
141
137
  const [emailBusy, setEmailBusy] = useState(false);
142
138
  const [emailError, setEmailError] = useState('');
143
139
 
@@ -145,8 +141,7 @@ export const YaverLoginScreen: React.FC<YaverLoginScreenProps> = ({
145
141
  const user = await validateToken(token);
146
142
  await saveToken(token);
147
143
  if (user) await saveUser(user);
148
- const cleanedInviteCode = inviteCode.toUpperCase().replace(/[^A-Z0-9]/g, '').slice(0, 6);
149
- onLoggedIn(token, cleanedInviteCode ? { inviteCode: cleanedInviteCode } : undefined);
144
+ onLoggedIn(token);
150
145
  };
151
146
 
152
147
  const handleApple = async () => {
@@ -330,21 +325,6 @@ export const YaverLoginScreen: React.FC<YaverLoginScreenProps> = ({
330
325
  secureTextEntry
331
326
  />
332
327
  )}
333
- {isSignUp && (
334
- <TextInput
335
- style={styles.input}
336
- placeholder="Invite Code (optional)"
337
- placeholderTextColor="#666"
338
- value={inviteCode}
339
- onChangeText={(value) =>
340
- setInviteCode(value.toUpperCase().replace(/[^A-Z0-9]/g, '').slice(0, 6))
341
- }
342
- autoCapitalize="characters"
343
- autoCorrect={false}
344
- maxLength={6}
345
- />
346
- )}
347
-
348
328
  {emailError ? (
349
329
  <Text style={styles.errorText}>{emailError}</Text>
350
330
  ) : null}
@@ -28,9 +28,7 @@ export interface YaverMachinePickerProps {
28
28
  }
29
29
 
30
30
  /**
31
- * List of remote dev machines the signed-in user can reach. Split into
32
- * - Owned machines (user is the host)
33
- * - Shared machines (host invited them as a guest)
31
+ * List of remote dev machines owned by the signed-in user.
34
32
  *
35
33
  * Tapping a device persists it to AsyncStorage and invokes `onPick`. The
36
34
  * SDK then uses that device's deviceId for agent discovery (LAN probe +
@@ -45,7 +43,7 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
45
43
  const [loading, setLoading] = useState(true);
46
44
  const [refreshing, setRefreshing] = useState(false);
47
45
  const [error, setError] = useState<string | null>(null);
48
- const [list, setList] = useState<DeviceList>({ owned: [], shared: [] });
46
+ const [list, setList] = useState<DeviceList>({ owned: [] });
49
47
  const [pairingDevice, setPairingDevice] = useState<RemoteDevice | null>(null);
50
48
  const [reachability, setReachability] = useState<Record<string, DeviceReachability | undefined>>({});
51
49
 
@@ -57,7 +55,7 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
57
55
  setList(result);
58
56
  setReachability({});
59
57
  void (async () => {
60
- const devices = [...result.owned, ...result.shared];
58
+ const devices = result.owned;
61
59
  const settled = await Promise.allSettled(
62
60
  devices.map(async (device) => ({
63
61
  deviceId: device.deviceId,
@@ -74,10 +72,8 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
74
72
  return next;
75
73
  });
76
74
  })();
77
- if (result.owned.length === 0 && result.shared.length === 0) {
78
- setError(
79
- 'No machines found yet. If you do not have your own computer, redeem a host invite code first. Otherwise run `yaver auth` + `yaver serve` on your machine.',
80
- );
75
+ if (result.owned.length === 0) {
76
+ setError('No machines found yet. Run `yaver auth` + `yaver serve` on your machine.');
81
77
  }
82
78
  } catch (err) {
83
79
  setError(err instanceof Error ? err.message : String(err));
@@ -158,13 +154,8 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
158
154
  } else if (device.runnerDown) {
159
155
  statusLine = 'Runner down — restart the coding agent on the Mac';
160
156
  } else {
161
- // Happy-path subtitle: platform + optional host/share hint.
157
+ // Happy-path subtitle.
162
158
  statusLine = device.platform;
163
- if (device.isGuest && device.hostEmail) {
164
- statusLine = `${statusLine} • ${device.hostEmail}`;
165
- } else if (device.accessScope === 'shared-scoped') {
166
- statusLine = `${statusLine} • paylaşılan`;
167
- }
168
159
  }
169
160
  return (
170
161
  <TouchableOpacity
@@ -216,12 +207,6 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
216
207
  {list.owned.map(renderDevice)}
217
208
  </View>
218
209
  )}
219
- {list.shared.length > 0 && (
220
- <View style={styles.section}>
221
- <Text style={styles.sectionTitle}>Paylaşılan (guest)</Text>
222
- {list.shared.map(renderDevice)}
223
- </View>
224
- )}
225
210
  {error && <Text style={styles.error}>{error}</Text>}
226
211
  </>
227
212
  )}
package/src/P2PClient.ts CHANGED
@@ -23,6 +23,17 @@ function unrefTimer(timer: ReturnType<typeof setTimeout>): void {
23
23
  }
24
24
  }
25
25
 
26
+ async function dogfoodFetch(url: string, init: RequestInit, timeoutMs: number): Promise<Response> {
27
+ const ctrl = new AbortController();
28
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
29
+ unrefTimer(timer);
30
+ try {
31
+ return await fetch(url, { ...init, signal: ctrl.signal });
32
+ } finally {
33
+ clearTimeout(timer);
34
+ }
35
+ }
36
+
26
37
  export interface FeedbackEvent {
27
38
  type: string;
28
39
  timestamp: string;
@@ -38,6 +49,56 @@ export interface ReloadAck {
38
49
  changeClass?: string;
39
50
  }
40
51
 
52
+ export interface DogfoodDevServerStatus {
53
+ running?: boolean;
54
+ serving?: boolean;
55
+ starting?: boolean;
56
+ building?: boolean;
57
+ framework?: string;
58
+ workDir?: string;
59
+ bundleUrl?: string;
60
+ previewUrl?: string;
61
+ error?: string;
62
+ capabilityGap?: unknown;
63
+ }
64
+
65
+ export interface DogfoodDevEvent {
66
+ type: string;
67
+ framework?: string;
68
+ logLine?: string;
69
+ message?: string;
70
+ phase?: string;
71
+ pct?: number;
72
+ currentFile?: string;
73
+ snapshot?: { recentLogs?: string[]; [key: string]: unknown };
74
+ [key: string]: unknown;
75
+ }
76
+
77
+ export interface DogfoodRemoteRuntimeTarget {
78
+ id: string;
79
+ label: string;
80
+ enabled: boolean;
81
+ reason?: string;
82
+ platform?: string;
83
+ surface?: string;
84
+ displaySurface?: string;
85
+ }
86
+
87
+ export interface DogfoodRemoteRuntimeCapabilities {
88
+ remoteRuntimeEligible?: boolean;
89
+ targets: DogfoodRemoteRuntimeTarget[];
90
+ }
91
+
92
+ export interface DogfoodRemoteRuntimeSession {
93
+ id: string;
94
+ status: string;
95
+ targetId?: string;
96
+ targetLabel?: string;
97
+ transportMode?: string;
98
+ note?: string;
99
+ [key: string]: unknown;
100
+ }
101
+
41
102
  /**
42
103
  * Try to resolve `{projectName, bundleId}` for the running app so the
43
104
  * agent can map the reload request to a MobileProject in its scan
@@ -409,6 +470,37 @@ export class P2PClient {
409
470
  return Array.isArray(data.runners) ? (data.runners as RunnerAuthStatusRow[]) : [];
410
471
  }
411
472
 
473
+ /** Canonical runner + model catalogue used by Vibing routing controls. */
474
+ async getAvailableRunners(): Promise<RunnerAuthStatusRow[]> {
475
+ const resp = await fetch(`${this.baseUrl}/agent/runners`, {
476
+ headers: this.authHeaders(),
477
+ });
478
+ if (!resp.ok) {
479
+ const text = await resp.text().catch(() => '');
480
+ throw new Error(`getAvailableRunners HTTP ${resp.status}: ${text}`);
481
+ }
482
+ const data = await resp.json().catch(() => ({} as Record<string, unknown>));
483
+ return Array.isArray(data.runners) ? (data.runners as RunnerAuthStatusRow[]) : [];
484
+ }
485
+
486
+ /** Discovered runnable projects on the selected owner machine. Paths stay
487
+ * on that machine/transport; they are never registered in Yaver's backend. */
488
+ async listDogfoodProjects(): Promise<Array<{
489
+ name: string;
490
+ path: string;
491
+ framework?: string;
492
+ frameworks?: string[];
493
+ surfaces?: string[];
494
+ }>> {
495
+ const resp = await fetch(`${this.baseUrl}/projects`, { headers: this.authHeaders() });
496
+ if (!resp.ok) {
497
+ const body = await resp.text().catch(() => '');
498
+ throw new Error(`listDogfoodProjects HTTP ${resp.status}: ${body}`);
499
+ }
500
+ const data = await resp.json().catch(() => ({} as Record<string, unknown>));
501
+ return Array.isArray(data.projects) ? data.projects : [];
502
+ }
503
+
412
504
  async getOpenCodeConfig(): Promise<OpenCodeConfigSummary | null> {
413
505
  const resp = await fetch(`${this.baseUrl}/runner/opencode/config`, {
414
506
  headers: this.authHeaders(),
@@ -727,6 +819,205 @@ export class P2PClient {
727
819
  }
728
820
  }
729
821
 
822
+ /**
823
+ * Start the ordinary Projects runtime for an embedded Dogfood host.
824
+ * Requires a full signed-in-user token because /dev/start can spawn tools;
825
+ * a narrow feedback SDK token intentionally cannot use it.
826
+ */
827
+ async startDogfoodDevServer(input: {
828
+ framework: string;
829
+ workDir: string;
830
+ lane: 'browser' | 'hermes';
831
+ }): Promise<DogfoodDevServerStatus> {
832
+ if (input.lane === 'hermes') {
833
+ const ack = await this.reloadApp('bundle', { projectPath: input.workDir });
834
+ return { running: ack.ok, framework: input.framework, workDir: input.workDir };
835
+ }
836
+ const ctrl = new AbortController();
837
+ const timer = setTimeout(() => ctrl.abort(), 45_000);
838
+ unrefTimer(timer);
839
+ try {
840
+ const response = await fetch(`${this.baseUrl}/dev/start`, {
841
+ method: 'POST',
842
+ headers: this.authHeaders({ 'Content-Type': 'application/json' }),
843
+ body: JSON.stringify({
844
+ framework: input.framework,
845
+ workDir: input.workDir,
846
+ platform: 'web',
847
+ caller: 'sdk',
848
+ }),
849
+ signal: ctrl.signal,
850
+ });
851
+ const data = (await response.json().catch(() => ({}))) as DogfoodDevServerStatus & {
852
+ code?: string; remedy?: string; retryable?: boolean;
853
+ };
854
+ if (!response.ok) {
855
+ const error = new Error(data.error || `Dogfood preview start failed with HTTP ${response.status}`) as Error & {
856
+ code?: string; remedy?: string; retryable?: boolean; capabilityGap?: unknown;
857
+ };
858
+ error.code = data.code || `DOGFOOD_DEV_START_HTTP_${response.status}`;
859
+ error.remedy = data.remedy || 'Fix the named dev-server failure, then retry Dogfood.';
860
+ error.retryable = data.retryable !== false;
861
+ error.capabilityGap = data.capabilityGap;
862
+ throw error;
863
+ }
864
+ return data;
865
+ } finally {
866
+ clearTimeout(timer);
867
+ }
868
+ }
869
+
870
+ /** Full status for Dogfood startup; unlike the compact feedback snapshot,
871
+ * this retains render URLs and structured startup failures. */
872
+ async getDogfoodDevServerStatus(): Promise<DogfoodDevServerStatus | null> {
873
+ const ctrl = new AbortController();
874
+ const timer = setTimeout(() => ctrl.abort(), 10_000);
875
+ unrefTimer(timer);
876
+ try {
877
+ const response = await fetch(`${this.baseUrl}${AGENT_ENDPOINTS.devStatus}`, {
878
+ headers: this.authHeaders(),
879
+ signal: ctrl.signal,
880
+ });
881
+ if (!response.ok) return null;
882
+ return await response.json().catch(() => null) as DogfoodDevServerStatus | null;
883
+ } catch {
884
+ return null;
885
+ } finally {
886
+ clearTimeout(timer);
887
+ }
888
+ }
889
+
890
+ async getDogfoodRemoteRuntimeCapabilities(
891
+ workDir: string,
892
+ framework: string,
893
+ ): Promise<DogfoodRemoteRuntimeCapabilities> {
894
+ const query = `?workDir=${encodeURIComponent(workDir)}&framework=${encodeURIComponent(framework)}`;
895
+ const response = await dogfoodFetch(`${this.baseUrl}/remote-runtime/capabilities${query}`, {
896
+ headers: this.authHeaders(),
897
+ }, 20_000);
898
+ const data = await response.json().catch(() => ({}));
899
+ if (!response.ok) throw new Error(data?.error || `Remote-runtime capabilities failed with HTTP ${response.status}`);
900
+ return { ...data, targets: Array.isArray(data?.targets) ? data.targets : [] };
901
+ }
902
+
903
+ async startDogfoodRemoteRuntime(
904
+ workDir: string,
905
+ framework: string,
906
+ targetId: string,
907
+ ): Promise<DogfoodRemoteRuntimeSession> {
908
+ const response = await dogfoodFetch(`${this.baseUrl}/remote-runtime/sessions`, {
909
+ method: 'POST',
910
+ headers: this.authHeaders({ 'Content-Type': 'application/json' }),
911
+ body: JSON.stringify({ workDir, framework, targetId, surface: 'sdk' }),
912
+ }, 45_000);
913
+ const data = await response.json().catch(() => ({}));
914
+ if (!response.ok) throw new Error(data?.error || `Remote-runtime start failed with HTTP ${response.status}`);
915
+ return data as DogfoodRemoteRuntimeSession;
916
+ }
917
+
918
+ async stopDogfoodRemoteRuntime(sessionId: string): Promise<void> {
919
+ const response = await dogfoodFetch(`${this.baseUrl}/remote-runtime/sessions/${encodeURIComponent(sessionId)}`, {
920
+ method: 'DELETE', headers: this.authHeaders(),
921
+ }, 15_000);
922
+ if (!response.ok) throw new Error(`Remote-runtime stop failed with HTTP ${response.status}`);
923
+ }
924
+
925
+ /** Stop the runtime this SDK host started. Full user auth, same as start. */
926
+ async stopDogfoodDevServer(): Promise<void> {
927
+ const ctrl = new AbortController();
928
+ const timer = setTimeout(() => ctrl.abort(), 15_000);
929
+ unrefTimer(timer);
930
+ try {
931
+ const response = await fetch(`${this.baseUrl}/dev/stop`, {
932
+ method: 'POST', headers: this.authHeaders(), signal: ctrl.signal,
933
+ });
934
+ if (!response.ok) throw new Error(`Dogfood preview stop failed with HTTP ${response.status}`);
935
+ } finally {
936
+ clearTimeout(timer);
937
+ }
938
+ }
939
+
940
+ /** Resolve an agent-reported /dev/ or /dev-web/ route without putting auth in the URL. */
941
+ resolveDogfoodUrl(path: string): string {
942
+ return new URL(path, `${this.baseUrl.replace(/\/+$/, '')}/`).toString();
943
+ }
944
+
945
+ /**
946
+ * React-Native-safe /dev/events stream. XHR is intentional: Hermes fetch
947
+ * exposes no streaming response body. Reconnects are bounded and every
948
+ * reconnect/loss is surfaced to the host console.
949
+ */
950
+ subscribeDogfoodDevEvents(
951
+ onEvent: (event: DogfoodDevEvent) => void,
952
+ onHealth?: (health: { kind: 'reattaching' | 'lost'; message: string } | null) => void,
953
+ ): () => void {
954
+ let disposed = false;
955
+ let xhr: XMLHttpRequest | null = null;
956
+ let retryTimer: ReturnType<typeof setTimeout> | null = null;
957
+ let attempt = 0;
958
+
959
+ const open = () => {
960
+ if (disposed) return;
961
+ const request = new XMLHttpRequest();
962
+ xhr = request;
963
+ let parsed = 0;
964
+ let carry = '';
965
+ const consume = () => {
966
+ const text = request.responseText || '';
967
+ if (text.length <= parsed) return;
968
+ carry += text.slice(parsed);
969
+ parsed = text.length;
970
+ const normalized = carry.replace(/\r\n/g, '\n');
971
+ const frames = normalized.split('\n\n');
972
+ carry = frames.pop() || '';
973
+ for (const frame of frames) {
974
+ const data = frame.split('\n')
975
+ .filter((line) => line.startsWith('data:'))
976
+ .map((line) => line.slice(5).replace(/^ /, ''))
977
+ .join('\n');
978
+ if (!data) continue;
979
+ try {
980
+ attempt = 0;
981
+ onHealth?.(null);
982
+ onEvent(JSON.parse(data));
983
+ } catch { /* comments and non-JSON keepalives are valid SSE */ }
984
+ }
985
+ };
986
+ const reconnect = (reason: string) => {
987
+ if (disposed) return;
988
+ if (attempt >= 5) {
989
+ onHealth?.({ kind: 'lost', message: `Dev-server logs stopped: ${reason}` });
990
+ return;
991
+ }
992
+ const wait = Math.min(8_000, 500 * (2 ** attempt));
993
+ attempt += 1;
994
+ onHealth?.({ kind: 'reattaching', message: `Dev-server logs interrupted; reconnecting (${attempt}/5): ${reason}` });
995
+ retryTimer = setTimeout(open, wait);
996
+ unrefTimer(retryTimer);
997
+ };
998
+ request.open('GET', `${this.baseUrl}/dev/events`, true);
999
+ for (const [key, value] of Object.entries(this.authHeaders({ Accept: 'text/event-stream' }))) {
1000
+ try { request.setRequestHeader(key, value); } catch { /* restricted RN header */ }
1001
+ }
1002
+ request.onprogress = consume;
1003
+ request.onload = () => { consume(); reconnect(`HTTP ${request.status || 0}`); };
1004
+ request.onerror = () => reconnect('connection failed');
1005
+ request.onabort = () => reconnect('connection aborted');
1006
+ request.ontimeout = () => reconnect('connection timed out');
1007
+ try {
1008
+ request.send();
1009
+ } catch (error) {
1010
+ reconnect(error instanceof Error ? error.message : 'connection could not start');
1011
+ }
1012
+ };
1013
+ open();
1014
+ return () => {
1015
+ disposed = true;
1016
+ if (retryTimer) clearTimeout(retryTimer);
1017
+ try { xhr?.abort(); } catch { /* idempotent */ }
1018
+ };
1019
+ }
1020
+
730
1021
  /**
731
1022
  * Trigger a reload with an EXPLICIT fast/full mode — no bundle fallback.
732
1023
  *
@@ -737,8 +1028,8 @@ export class P2PClient {
737
1028
  * told. So this method reports the failure instead, with a named cause.
738
1029
  *
739
1030
  * Auth: the SAME bearer used for the feedback POST. `/dev/reload` is
740
- * registered under `authSDKOrGuest` in desktop/agent/httpserver.go and is
741
- * already in the `guest-reload` SDK-token scope list — nothing widens.
1031
+ * registered under `authSDK` in desktop/agent/httpserver.go and is already
1032
+ * in the `reload` SDK-token scope list — nothing widens.
742
1033
  */
743
1034
  async reloadWithMode(
744
1035
  mode: ReloadWireMode,
@@ -864,14 +1155,14 @@ export class P2PClient {
864
1155
  * the project context plus the user's prompt. Returns the task id the
865
1156
  * caller can poll via `/tasks/{id}` if needed.
866
1157
  *
867
- * Requires an owner/CLI/paired token — the `/vibing*` routes do not
1158
+ * Requires an owner/CLI token — the `/vibing*` routes do not
868
1159
  * currently accept SDK-minted tokens. Power users typically drive
869
1160
  * vibing from Claude Code / the Yaver mobile app; this method is a
870
1161
  * convenience for the SDK's one-tap bug-report-to-vibing path.
871
1162
  */
872
1163
  async vibing(
873
1164
  prompt: string,
874
- opts?: { projectName?: string; bundleId?: string; projectPath?: string },
1165
+ opts?: { projectName?: string; bundleId?: string; projectPath?: string; runner?: string; model?: string },
875
1166
  ): Promise<{ taskId: string }> {
876
1167
  // Resolve app identity exactly the same way we do for
877
1168
  // reloadApp — bundle ID from expo-constants or native config.
@@ -889,6 +1180,8 @@ export class P2PClient {
889
1180
  projectPath: identity.projectPath ?? opts?.projectPath ?? '',
890
1181
  projectName: identity.projectName,
891
1182
  bundleId: identity.bundleId,
1183
+ runner: opts?.runner,
1184
+ model: opts?.model,
892
1185
  }),
893
1186
  });
894
1187
  if (!response.ok) {
@@ -1189,6 +1482,56 @@ export class P2PClient {
1189
1482
  return { taskId: json.taskId, raw: json };
1190
1483
  }
1191
1484
 
1485
+ /** Existing task history, filtered by the agent to Vibing/feedback topics. */
1486
+ async listVibeThreads(input?: { projectName?: string; projectPath?: string }): Promise<Array<{
1487
+ id: string;
1488
+ title: string;
1489
+ status: string;
1490
+ createdAt?: string;
1491
+ projectName?: string;
1492
+ turnCount?: number;
1493
+ }>> {
1494
+ const params = new URLSearchParams();
1495
+ if (input?.projectName) params.set('projectName', input.projectName);
1496
+ if (input?.projectPath) params.set('projectPath', input.projectPath);
1497
+ const query = params.toString();
1498
+ const resp = await fetch(`${this.baseUrl}/vibing/tasks${query ? `?${query}` : ''}`, {
1499
+ headers: this.authHeaders(),
1500
+ });
1501
+ if (!resp.ok) throw new Error(`listVibeThreads HTTP ${resp.status}`);
1502
+ const json = (await resp.json().catch(() => ({}))) as { tasks?: Array<any> };
1503
+ return (json.tasks || []).map((task) => ({
1504
+ id: String(task.id),
1505
+ title: String(task.title || 'New topic'),
1506
+ status: String(task.status || 'completed'),
1507
+ createdAt: task.createdAt,
1508
+ projectName: task.projectName,
1509
+ turnCount: task.turnCount,
1510
+ }));
1511
+ }
1512
+
1513
+ async getVibeThread(taskId: string): Promise<{
1514
+ id: string;
1515
+ title: string;
1516
+ status: string;
1517
+ turns?: Array<{ role: 'user' | 'assistant'; content: string; timestamp?: string }>;
1518
+ }> {
1519
+ const resp = await fetch(`${this.baseUrl}/vibing/task/${encodeURIComponent(taskId)}`, {
1520
+ headers: this.authHeaders(),
1521
+ });
1522
+ if (!resp.ok) throw new Error(`getVibeThread HTTP ${resp.status}`);
1523
+ const json = (await resp.json().catch(() => ({}))) as { task?: any };
1524
+ return json.task || json as any;
1525
+ }
1526
+
1527
+ async deleteVibeThread(taskId: string): Promise<void> {
1528
+ const resp = await fetch(`${this.baseUrl}/vibing/task/${encodeURIComponent(taskId)}`, {
1529
+ method: 'DELETE',
1530
+ headers: this.authHeaders(),
1531
+ });
1532
+ if (!resp.ok) throw new Error(`deleteVibeThread HTTP ${resp.status}`);
1533
+ }
1534
+
1192
1535
  /**
1193
1536
  * Subscribe to a task's live stdout/stderr stream. Returns an abort
1194
1537
  * function — call it to detach. The agent emits NDJSON lines on
@@ -0,0 +1,132 @@
1
+ import type { P2PClient } from './P2PClient';
2
+ import {
3
+ DogfoodRuntimeError,
4
+ runtimeLogLinesFromDevEvent,
5
+ type DogfoodDriver,
6
+ } from './DogfoodRuntime';
7
+
8
+ const delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
9
+
10
+ function reportedPreview(status: { previewUrl?: string; bundleUrl?: string }): string {
11
+ return String(status.previewUrl || status.bundleUrl || '').trim();
12
+ }
13
+
14
+ export interface P2PDogfoodDriverOptions {
15
+ startupTimeoutMs?: number;
16
+ pollIntervalMs?: number;
17
+ }
18
+
19
+ /**
20
+ * Ready-to-use driver for app owners who authenticate with Yaver's normal
21
+ * account flow. Their UI owns the trigger and preview surface; this adapter
22
+ * owns the existing Projects endpoints and raw build log stream.
23
+ */
24
+ export function createP2PDogfoodDriver(
25
+ client: P2PClient,
26
+ options: P2PDogfoodDriverOptions = {},
27
+ ): DogfoodDriver {
28
+ const startupTimeoutMs = Math.max(1, options.startupTimeoutMs ?? 155_000);
29
+ const pollIntervalMs = Math.max(1, options.pollIntervalMs ?? 600);
30
+ return {
31
+ async prepare(context) {
32
+ const close = client.subscribeDogfoodDevEvents(
33
+ (event) => runtimeLogLinesFromDevEvent(event).forEach((line) => context.log(line)),
34
+ (health) => {
35
+ if (health) context.log({ text: `[logs] ${health.message}`, at: Date.now(), stream: 'system' });
36
+ },
37
+ );
38
+ context.registerCleanup(close, 'transient');
39
+ },
40
+ async start(context) {
41
+ const { project } = context;
42
+ if (project.lane === 'webrtc') {
43
+ context.setPhase('starting', `Finding a native runtime for ${project.name}…`);
44
+ const capabilities = await client.getDogfoodRemoteRuntimeCapabilities(project.workDir, project.framework);
45
+ const nativeTargets = capabilities.targets.filter((target) => target.enabled && target.id !== 'browser-window');
46
+ const target = project.nativeTargetId
47
+ ? nativeTargets.find((candidate) => candidate.id === project.nativeTargetId)
48
+ : nativeTargets[0];
49
+ if (!target) {
50
+ throw new DogfoodRuntimeError({
51
+ code: 'DOGFOOD_NATIVE_RUNTIME_UNAVAILABLE',
52
+ error: project.nativeTargetId
53
+ ? `Native runtime ${project.nativeTargetId} is not available.`
54
+ : 'No native simulator, emulator, or device runtime is available.',
55
+ remedy: 'Start or install a native runtime on the selected machine, then retry WebRTC; Browser lane remains available.',
56
+ retryable: true,
57
+ });
58
+ }
59
+ context.setPhase('starting', `Starting ${target.label} over WebRTC…`);
60
+ const session = await client.startDogfoodRemoteRuntime(project.workDir, project.framework, target.id);
61
+ context.registerCleanup(() => client.stopDogfoodRemoteRuntime(session.id), 'session');
62
+ return {
63
+ lane: 'webrtc',
64
+ sessionId: session.id,
65
+ metadata: { target, session, framework: project.framework, workDir: project.workDir },
66
+ };
67
+ }
68
+ context.setPhase(project.lane === 'hermes' ? 'compiling' : 'starting',
69
+ project.lane === 'hermes' ? `Compiling ${project.name} with Hermes…` : `Starting ${project.name} in the browser…`);
70
+ const status = await client.startDogfoodDevServer({
71
+ framework: project.framework,
72
+ workDir: project.workDir,
73
+ lane: project.lane,
74
+ });
75
+ // Browser Dogfood owns a long-lived dev server. Hermes is a one-shot
76
+ // build + delivery to the Yaver container; calling /dev/stop when that
77
+ // guest exits can kill an unrelated browser preview on the same box.
78
+ if (project.lane === 'browser') {
79
+ context.registerCleanup(() => client.stopDogfoodDevServer(), 'session');
80
+ }
81
+ if (status.error) {
82
+ throw new DogfoodRuntimeError({
83
+ code: 'DOGFOOD_DEV_SERVER_FAILED', error: status.error,
84
+ remedy: 'Fix the named project/runtime error, then retry Dogfood.', retryable: true,
85
+ });
86
+ }
87
+ let latest = status;
88
+ let reported = reportedPreview(latest);
89
+ if (project.lane === 'browser' && !reported) {
90
+ context.setPhase('compiling', `Compiling ${project.name} for the browser…`);
91
+ const deadline = Date.now() + startupTimeoutMs;
92
+ while (context.isCurrent() && Date.now() < deadline) {
93
+ await delay(pollIntervalMs);
94
+ const polled = await client.getDogfoodDevServerStatus();
95
+ if (!polled) continue;
96
+ latest = polled;
97
+ if (latest.error && !latest.building) {
98
+ throw new DogfoodRuntimeError({
99
+ code: 'DOGFOOD_DEV_SERVER_FAILED', error: latest.error,
100
+ remedy: 'Fix the named project/runtime error, then retry Dogfood.', retryable: true,
101
+ });
102
+ }
103
+ reported = reportedPreview(latest);
104
+ if (reported && (latest.running || latest.serving)) break;
105
+ }
106
+ if (!context.isCurrent()) {
107
+ throw new DogfoodRuntimeError({
108
+ code: 'DOGFOOD_ATTEMPT_REPLACED', error: 'A newer Dogfood attempt replaced this compile.',
109
+ remedy: 'Wait for the newer attempt.', retryable: true,
110
+ });
111
+ }
112
+ if (!reported) {
113
+ throw new DogfoodRuntimeError({
114
+ code: 'DOGFOOD_NO_RENDER_URL',
115
+ error: `The dev server did not report a browser preview URL within ${Math.ceil(startupTimeoutMs / 1000)} seconds.`,
116
+ 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.',
117
+ retryable: true,
118
+ });
119
+ }
120
+ }
121
+ return {
122
+ lane: project.lane,
123
+ url: reported ? client.resolveDogfoodUrl(reported) : undefined,
124
+ metadata: {
125
+ framework: latest.framework || project.framework,
126
+ workDir: latest.workDir || project.workDir,
127
+ ...(project.lane === 'hermes' ? { delivered: latest.running === true } : {}),
128
+ },
129
+ };
130
+ },
131
+ };
132
+ }
@@ -28,9 +28,8 @@ import { getConvexSiteUrl, getToken } from './auth';
28
28
  * report `needsAuth=false` in /devices/list.
29
29
  *
30
30
  * This avoids making the user bounce to the Yaver mobile app just to
31
- * adopt a machine. Works for owners and shared-scope guests since the
32
- * pair endpoint accepts any valid Convex session that matches the
33
- * expected account type.
31
+ * adopt one of their own machines. The pair endpoint verifies the signed-in
32
+ * account as the machine owner.
34
33
  */
35
34
  export interface PairDeviceModalProps {
36
35
  device: RemoteDevice | null;