yaver-feedback-react-native 0.9.2 → 0.9.4
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.
- package/README.md +102 -1
- package/dist/AuthOverlay.d.ts +1 -14
- package/dist/AuthOverlay.js +9 -62
- package/dist/Discovery.js +0 -6
- package/dist/DogfoodRuntime.d.ts +124 -0
- package/dist/DogfoodRuntime.js +273 -0
- package/dist/FeedbackModal.js +347 -61
- package/dist/LoginScreen.d.ts +1 -5
- package/dist/LoginScreen.js +2 -6
- package/dist/MachinePickerScreen.d.ts +1 -3
- package/dist/MachinePickerScreen.js +6 -18
- package/dist/P2PClient.d.ts +116 -3
- package/dist/P2PClient.js +273 -3
- package/dist/P2PDogfoodDriver.d.ts +12 -0
- package/dist/P2PDogfoodDriver.js +118 -0
- package/dist/PairDeviceModal.d.ts +2 -3
- package/dist/ShakeDetector.js +31 -17
- package/dist/VibeChatScreen.d.ts +11 -1
- package/dist/VibeChatScreen.js +200 -41
- package/dist/YaverFeedback.d.ts +34 -0
- package/dist/YaverFeedback.js +124 -19
- package/dist/YaverModeBadge.d.ts +22 -0
- package/dist/YaverModeBadge.js +219 -0
- package/dist/__tests__/AuthDevices.test.js +1 -48
- package/dist/__tests__/DogfoodRuntime.test.d.ts +1 -0
- package/dist/__tests__/DogfoodRuntime.test.js +116 -0
- package/dist/__tests__/P2PDogfoodDriver.test.d.ts +1 -0
- package/dist/__tests__/P2PDogfoodDriver.test.js +64 -0
- package/dist/__tests__/ReportIdentity.test.d.ts +25 -1
- package/dist/__tests__/ReportIdentity.test.js +34 -22
- package/dist/__tests__/ShakeDetectorWeb.test.d.ts +1 -0
- package/dist/__tests__/ShakeDetectorWeb.test.js +27 -0
- package/dist/__tests__/YaverFeedback.test.js +13 -3
- package/dist/__tests__/deviceDogfood.test.d.ts +1 -0
- package/dist/__tests__/deviceDogfood.test.js +86 -0
- package/dist/__tests__/dogfoodPolicy.test.d.ts +1 -0
- package/dist/__tests__/dogfoodPolicy.test.js +33 -0
- package/dist/_core/ansi.d.ts +117 -0
- package/dist/_core/ansi.js +468 -0
- package/dist/_core/ansi.test.d.ts +1 -0
- package/dist/_core/ansi.test.js +225 -0
- package/dist/_core/buildFeedbackPrompt.d.ts +4 -9
- package/dist/_core/buildFeedbackPrompt.js +18 -72
- package/dist/_core/constants.d.ts +19 -6
- package/dist/_core/constants.js +20 -7
- package/dist/_core/device.d.ts +9 -23
- package/dist/_core/device.js +13 -28
- package/dist/_core/endpoints.d.ts +0 -7
- package/dist/_core/endpoints.js +0 -7
- package/dist/_core/index.d.ts +4 -0
- package/dist/_core/index.js +4 -0
- package/dist/_core/remoteless.d.ts +44 -0
- package/dist/_core/remoteless.js +75 -0
- package/dist/_core/trace.d.ts +47 -0
- package/dist/_core/trace.js +38 -0
- package/dist/_core/trace.test.d.ts +1 -0
- package/dist/_core/trace.test.js +60 -0
- package/dist/auth.d.ts +3 -60
- package/dist/auth.js +6 -88
- package/dist/deviceDogfood.d.ts +53 -0
- package/dist/deviceDogfood.js +137 -0
- package/dist/dogfoodPolicy.d.ts +25 -0
- package/dist/dogfoodPolicy.js +24 -0
- package/dist/index.d.ts +13 -4
- package/dist/index.js +24 -8
- package/dist/reloadActions.js +2 -2
- package/dist/types.d.ts +50 -7
- package/package.json +12 -3
- package/src/AuthOverlay.tsx +20 -106
- package/src/Discovery.ts +0 -6
- package/src/DogfoodRuntime.ts +373 -0
- package/src/FeedbackModal.tsx +445 -67
- package/src/LoginScreen.tsx +2 -22
- package/src/MachinePickerScreen.tsx +6 -21
- package/src/P2PClient.ts +347 -4
- package/src/P2PDogfoodDriver.ts +132 -0
- package/src/PairDeviceModal.tsx +2 -3
- package/src/ShakeDetector.ts +31 -18
- package/src/VibeChatScreen.tsx +232 -42
- package/src/YaverFeedback.ts +133 -22
- package/src/YaverModeBadge.tsx +234 -0
- package/src/__tests__/AuthDevices.test.ts +1 -52
- package/src/__tests__/DogfoodRuntime.test.ts +135 -0
- package/src/__tests__/P2PDogfoodDriver.test.ts +80 -0
- package/src/__tests__/ReportIdentity.test.ts +36 -27
- package/src/__tests__/ShakeDetectorWeb.test.ts +30 -0
- package/src/__tests__/YaverFeedback.test.ts +15 -3
- package/src/__tests__/deviceDogfood.test.ts +77 -0
- package/src/__tests__/dogfoodPolicy.test.ts +34 -0
- package/src/_core/ansi.test.ts +250 -0
- package/src/_core/ansi.ts +475 -0
- package/src/_core/buildFeedbackPrompt.ts +18 -95
- package/src/_core/constants.ts +20 -6
- package/src/_core/device.ts +13 -30
- package/src/_core/endpoints.ts +0 -7
- package/src/_core/index.ts +4 -0
- package/src/_core/remoteless.ts +110 -0
- package/src/_core/trace.test.ts +58 -0
- package/src/_core/trace.ts +75 -0
- package/src/auth.ts +7 -156
- package/src/deviceDogfood.ts +171 -0
- package/src/dogfoodPolicy.ts +40 -0
- package/src/index.ts +40 -11
- package/src/reloadActions.ts +2 -2
- package/src/types.ts +45 -7
- package/dist/GuestOnboardingScreen.d.ts +0 -8
- package/dist/GuestOnboardingScreen.js +0 -282
- package/src/GuestOnboardingScreen.tsx +0 -307
package/src/LoginScreen.tsx
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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: []
|
|
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 =
|
|
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
|
|
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
|
|
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 `
|
|
741
|
-
*
|
|
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
|
|
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
|
+
}
|
package/src/PairDeviceModal.tsx
CHANGED
|
@@ -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
|
|
32
|
-
*
|
|
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;
|