yaver-feedback-react-native 0.8.12 → 0.9.0
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/app.plugin.js +13 -0
- package/dist/DeployPanel.d.ts +7 -0
- package/dist/DeployPanel.js +354 -0
- package/dist/FeedbackModal.js +408 -63
- package/dist/FloatingButton.js +25 -3
- package/dist/MachinePickerScreen.js +14 -5
- package/dist/P2PClient.d.ts +102 -1
- package/dist/P2PClient.js +313 -0
- package/dist/VibeChatScreen.d.ts +25 -0
- package/dist/VibeChatScreen.js +531 -0
- package/dist/_core/buildFeedbackPrompt.d.ts +13 -0
- package/dist/_core/buildFeedbackPrompt.js +77 -0
- package/dist/capture.d.ts +20 -0
- package/dist/capture.js +82 -0
- package/dist/preferences.d.ts +4 -0
- package/dist/preferences.js +62 -0
- package/dist/types.d.ts +66 -2
- package/dist/voice.d.ts +61 -0
- package/dist/voice.js +246 -0
- package/package.json +26 -3
- package/src/DeployPanel.tsx +403 -0
- package/src/FeedbackModal.tsx +510 -76
- package/src/FloatingButton.tsx +27 -3
- package/src/MachinePickerScreen.tsx +12 -3
- package/src/P2PClient.ts +326 -1
- package/src/VibeChatScreen.tsx +581 -0
- package/src/_core/buildFeedbackPrompt.ts +102 -0
- package/src/capture.ts +82 -0
- package/src/preferences.ts +56 -0
- package/src/types.ts +62 -2
- package/src/voice.ts +270 -0
|
@@ -104,7 +104,14 @@ const YaverMachinePickerScreen = ({ token, currentDeviceId, onPick, onCancel, })
|
|
|
104
104
|
return;
|
|
105
105
|
}
|
|
106
106
|
const direct = await (0, auth_1.probeDeviceReachability)(device);
|
|
107
|
-
|
|
107
|
+
// Do not hard-block selection just because the LAN /health probe
|
|
108
|
+
// failed. The standalone SDK can still reach a healthy machine via
|
|
109
|
+
// the normal selected-device discovery path (including relay), and
|
|
110
|
+
// the Yaver host path may already be proving the machine works.
|
|
111
|
+
// Only treat the machine as unpickable when BOTH:
|
|
112
|
+
// 1. Convex says it is offline, and
|
|
113
|
+
// 2. the direct probe also failed.
|
|
114
|
+
if (!device.isOnline && !direct.reachable && !device.needsAuth) {
|
|
108
115
|
setError('Selected machine is not responding. Start `yaver serve` on it and try again.');
|
|
109
116
|
setReachability((prev) => ({ ...prev, [device.deviceId]: direct }));
|
|
110
117
|
return;
|
|
@@ -131,9 +138,11 @@ const YaverMachinePickerScreen = ({ token, currentDeviceId, onPick, onCancel, })
|
|
|
131
138
|
? '#f59e0b'
|
|
132
139
|
: effectivelyReachable
|
|
133
140
|
? '#22c55e'
|
|
134
|
-
:
|
|
135
|
-
? '#
|
|
136
|
-
:
|
|
141
|
+
: device.isOnline
|
|
142
|
+
? '#f59e0b'
|
|
143
|
+
: explicitlyOffline || !device.isOnline
|
|
144
|
+
? '#ef4444'
|
|
145
|
+
: '#22c55e';
|
|
137
146
|
// Derive a single short status phrase the user can act on.
|
|
138
147
|
let statusLine = device.platform;
|
|
139
148
|
if (probe === undefined) {
|
|
@@ -150,7 +159,7 @@ const YaverMachinePickerScreen = ({ token, currentDeviceId, onPick, onCancel, })
|
|
|
150
159
|
'Needs pairing — open the Yaver app to adopt this machine';
|
|
151
160
|
}
|
|
152
161
|
else if (explicitlyOffline) {
|
|
153
|
-
statusLine = '
|
|
162
|
+
statusLine = 'Online, but direct probe failed — relay / selected-machine path may still work';
|
|
154
163
|
}
|
|
155
164
|
else if (device.runnerDown) {
|
|
156
165
|
statusLine = 'Runner down — restart the coding agent on the Mac';
|
package/dist/P2PClient.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CapabilitySnapshot, FeedbackBundle, IncidentEvent, OperationState, RunnerBrowserAuthSession, TestSession, VoiceCapability } from './types';
|
|
1
|
+
import { CapabilitySnapshot, FeedbackBundle, IncidentEvent, OpenCodeConfigSummary, OperationState, RunnerBrowserAuthSession, RunnerAuthStatusRow, TestSession, VoiceCapability } from './types';
|
|
2
2
|
export interface FeedbackEvent {
|
|
3
3
|
type: string;
|
|
4
4
|
timestamp: string;
|
|
@@ -12,6 +12,22 @@ export interface ReloadAck {
|
|
|
12
12
|
nativeChangesDetected?: boolean;
|
|
13
13
|
changeClass?: string;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Try to resolve `{projectName, bundleId}` for the running app so the
|
|
17
|
+
* agent can map the reload request to a MobileProject in its scan
|
|
18
|
+
* cache. Order: caller-supplied opts → Expo Constants → react-native
|
|
19
|
+
* NativeModules. None of the lookups throw — missing data just means
|
|
20
|
+
* the agent will fall back to its own dev-server resolution.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveAppIdentity(opts?: {
|
|
23
|
+
projectName?: string;
|
|
24
|
+
bundleId?: string;
|
|
25
|
+
projectPath?: string;
|
|
26
|
+
}): {
|
|
27
|
+
projectName?: string;
|
|
28
|
+
bundleId?: string;
|
|
29
|
+
projectPath?: string;
|
|
30
|
+
};
|
|
15
31
|
/**
|
|
16
32
|
* Lightweight P2P HTTP client for communicating with a Yaver agent.
|
|
17
33
|
*
|
|
@@ -35,6 +51,16 @@ export declare class P2PClient {
|
|
|
35
51
|
setAuthToken(token: string): void;
|
|
36
52
|
/** Update the relay password (used for managed-relay baseUrls). */
|
|
37
53
|
setRelayPassword(password: string): void;
|
|
54
|
+
/** Read-only base URL — used by the voice vibe-coding path to probe
|
|
55
|
+
* GET /voice/status before opening the stream. */
|
|
56
|
+
get agentBaseUrl(): string;
|
|
57
|
+
/** WebSocket URL for the agent's voice stream (WS /voice/stream). The
|
|
58
|
+
* voice vibe-coding loop streams mic audio here and receives the
|
|
59
|
+
* transcript + agent task + TTS frames back. */
|
|
60
|
+
voiceStreamUrl(): string;
|
|
61
|
+
/** Auth headers for the voice WS + status probe — same bearer (and
|
|
62
|
+
* relay password) as every other agent request. */
|
|
63
|
+
voiceAuthHeaders(): Record<string, string>;
|
|
38
64
|
/** Merge in Authorization + (optional) X-Relay-Password on top of a header block. */
|
|
39
65
|
private authHeaders;
|
|
40
66
|
/**
|
|
@@ -53,6 +79,26 @@ export declare class P2PClient {
|
|
|
53
79
|
* the SDK still exposes it for symmetry with mobile/src/components/
|
|
54
80
|
* RunnerAuthModal.tsx and the Swift YaverRunnerAuthFlowPane. */
|
|
55
81
|
submitRunnerBrowserAuthCode(sessionId: string, code: string): Promise<RunnerBrowserAuthSession>;
|
|
82
|
+
getRunnerAuthStatus(): Promise<RunnerAuthStatusRow[]>;
|
|
83
|
+
getOpenCodeConfig(): Promise<OpenCodeConfigSummary | null>;
|
|
84
|
+
saveOpenCodeConfig(patch: {
|
|
85
|
+
defaultAgent?: string;
|
|
86
|
+
model?: string;
|
|
87
|
+
smallModel?: string;
|
|
88
|
+
buildModel?: string;
|
|
89
|
+
planModel?: string;
|
|
90
|
+
providers?: Array<{
|
|
91
|
+
id: string;
|
|
92
|
+
name?: string;
|
|
93
|
+
baseUrl?: string;
|
|
94
|
+
apiKey?: string;
|
|
95
|
+
delete?: boolean;
|
|
96
|
+
}>;
|
|
97
|
+
}): Promise<{
|
|
98
|
+
ok: boolean;
|
|
99
|
+
config?: OpenCodeConfigSummary;
|
|
100
|
+
error?: string;
|
|
101
|
+
}>;
|
|
56
102
|
capabilitySnapshot(): Promise<CapabilitySnapshot | null>;
|
|
57
103
|
incidents(opts?: {
|
|
58
104
|
category?: string;
|
|
@@ -227,5 +273,60 @@ export declare class P2PClient {
|
|
|
227
273
|
timestamp?: number;
|
|
228
274
|
}): Promise<boolean>;
|
|
229
275
|
/** Internal helper for authenticated GET/POST requests. */
|
|
276
|
+
/**
|
|
277
|
+
* Convergence point for ALL feedback surfaces — Tasks tab, in-Yaver
|
|
278
|
+
* native pane, and this standalone SDK all POST the same shape to
|
|
279
|
+
* `/tasks`. Wraps the user's text with the shared prompt builder
|
|
280
|
+
* (see `_core/buildFeedbackPrompt`) so every surface conditions
|
|
281
|
+
* the AI the same way.
|
|
282
|
+
*
|
|
283
|
+
* Returns the agent's response payload (`taskId`, etc.) so callers
|
|
284
|
+
* can wire `streamTaskOutput()` next for live transcript.
|
|
285
|
+
*
|
|
286
|
+
* Inputs:
|
|
287
|
+
* - userPrompt what the user typed
|
|
288
|
+
* - projectName / path optional Hot-Reload project context
|
|
289
|
+
* - runner / model optional preferred coding agent + model
|
|
290
|
+
* - screenshotBase64 optional JPEG base64 (no `data:` prefix)
|
|
291
|
+
* - imageMimeType defaults to "image/jpeg"
|
|
292
|
+
*/
|
|
293
|
+
createFeedbackTask(input: {
|
|
294
|
+
userPrompt: string;
|
|
295
|
+
projectName?: string;
|
|
296
|
+
projectPath?: string;
|
|
297
|
+
runner?: string;
|
|
298
|
+
model?: string;
|
|
299
|
+
screenshotBase64?: string;
|
|
300
|
+
imageMimeType?: string;
|
|
301
|
+
}): Promise<{
|
|
302
|
+
taskId: string;
|
|
303
|
+
raw?: unknown;
|
|
304
|
+
}>;
|
|
305
|
+
/**
|
|
306
|
+
* Subscribe to a task's live stdout/stderr stream. Returns an abort
|
|
307
|
+
* function — call it to detach. The agent emits NDJSON lines on
|
|
308
|
+
* `/tasks/{id}/output`; we surface each line via `onLine`.
|
|
309
|
+
*
|
|
310
|
+
* `onComplete` fires when the agent reports the task entered a
|
|
311
|
+
* terminal status (completed / failed / stopped). After that the
|
|
312
|
+
* caller should stop calling abort().
|
|
313
|
+
*
|
|
314
|
+
* Robust to fetch streaming on Hermes (streams Body via Response.
|
|
315
|
+
* body.getReader on platforms that support it; falls back to
|
|
316
|
+
* polling `/tasks/{id}` every 750 ms if streaming isn't available).
|
|
317
|
+
*/
|
|
318
|
+
streamTaskOutput(taskId: string, onLine: (line: string) => void, onComplete: (status: string) => void): () => void;
|
|
319
|
+
/**
|
|
320
|
+
* Send a follow-up message into an existing task — multi-turn vibe
|
|
321
|
+
* chat. The agent's `/tasks/{id}/resume` accepts the same shape as
|
|
322
|
+
* `/tasks` (description / userPrompt / images), and the existing
|
|
323
|
+
* task picks back up with the same runner + project context.
|
|
324
|
+
*/
|
|
325
|
+
resumeTask(input: {
|
|
326
|
+
taskId: string;
|
|
327
|
+
userPrompt: string;
|
|
328
|
+
screenshotBase64?: string;
|
|
329
|
+
imageMimeType?: string;
|
|
330
|
+
}): Promise<void>;
|
|
230
331
|
private request;
|
|
231
332
|
}
|
package/dist/P2PClient.js
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.P2PClient = void 0;
|
|
37
|
+
exports.resolveAppIdentity = resolveAppIdentity;
|
|
4
38
|
const react_native_1 = require("react-native");
|
|
5
39
|
/**
|
|
6
40
|
* Try to resolve `{projectName, bundleId}` for the running app so the
|
|
@@ -109,6 +143,22 @@ class P2PClient {
|
|
|
109
143
|
setRelayPassword(password) {
|
|
110
144
|
this.relayPassword = password;
|
|
111
145
|
}
|
|
146
|
+
/** Read-only base URL — used by the voice vibe-coding path to probe
|
|
147
|
+
* GET /voice/status before opening the stream. */
|
|
148
|
+
get agentBaseUrl() {
|
|
149
|
+
return this.baseUrl;
|
|
150
|
+
}
|
|
151
|
+
/** WebSocket URL for the agent's voice stream (WS /voice/stream). The
|
|
152
|
+
* voice vibe-coding loop streams mic audio here and receives the
|
|
153
|
+
* transcript + agent task + TTS frames back. */
|
|
154
|
+
voiceStreamUrl() {
|
|
155
|
+
return this.baseUrl.replace(/^http/, 'ws') + '/voice/stream';
|
|
156
|
+
}
|
|
157
|
+
/** Auth headers for the voice WS + status probe — same bearer (and
|
|
158
|
+
* relay password) as every other agent request. */
|
|
159
|
+
voiceAuthHeaders() {
|
|
160
|
+
return this.authHeaders();
|
|
161
|
+
}
|
|
112
162
|
/** Merge in Authorization + (optional) X-Relay-Password on top of a header block. */
|
|
113
163
|
authHeaders(extra = {}) {
|
|
114
164
|
const h = { ...extra };
|
|
@@ -174,6 +224,45 @@ class P2PClient {
|
|
|
174
224
|
const data = await resp.json();
|
|
175
225
|
return data.session;
|
|
176
226
|
}
|
|
227
|
+
async getRunnerAuthStatus() {
|
|
228
|
+
const resp = await fetch(`${this.baseUrl}/runner-auth/status`, {
|
|
229
|
+
headers: this.authHeaders(),
|
|
230
|
+
});
|
|
231
|
+
if (!resp.ok) {
|
|
232
|
+
const text = await resp.text().catch(() => '');
|
|
233
|
+
throw new Error(`getRunnerAuthStatus HTTP ${resp.status}: ${text}`);
|
|
234
|
+
}
|
|
235
|
+
const data = await resp.json().catch(() => ({}));
|
|
236
|
+
return Array.isArray(data.runners) ? data.runners : [];
|
|
237
|
+
}
|
|
238
|
+
async getOpenCodeConfig() {
|
|
239
|
+
const resp = await fetch(`${this.baseUrl}/runner/opencode/config`, {
|
|
240
|
+
headers: this.authHeaders(),
|
|
241
|
+
});
|
|
242
|
+
if (!resp.ok) {
|
|
243
|
+
const text = await resp.text().catch(() => '');
|
|
244
|
+
throw new Error(`getOpenCodeConfig HTTP ${resp.status}: ${text}`);
|
|
245
|
+
}
|
|
246
|
+
const data = await resp.json().catch(() => ({}));
|
|
247
|
+
return (data.config ?? null);
|
|
248
|
+
}
|
|
249
|
+
async saveOpenCodeConfig(patch) {
|
|
250
|
+
try {
|
|
251
|
+
const resp = await fetch(`${this.baseUrl}/runner/opencode/config`, {
|
|
252
|
+
method: 'POST',
|
|
253
|
+
headers: { ...this.authHeaders(), 'Content-Type': 'application/json' },
|
|
254
|
+
body: JSON.stringify(patch),
|
|
255
|
+
});
|
|
256
|
+
const data = await resp.json().catch(() => ({}));
|
|
257
|
+
if (!resp.ok) {
|
|
258
|
+
return { ok: false, error: data.error || `HTTP ${resp.status}` };
|
|
259
|
+
}
|
|
260
|
+
return { ok: true, config: data.config };
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
177
266
|
async capabilitySnapshot() {
|
|
178
267
|
try {
|
|
179
268
|
const resp = await fetch(`${this.baseUrl}/capabilities/snapshot`, { headers: this.authHeaders() });
|
|
@@ -356,6 +445,10 @@ class P2PClient {
|
|
|
356
445
|
s2sReady: data.s2sReady ?? false,
|
|
357
446
|
sttProvider: data.sttProvider ?? undefined,
|
|
358
447
|
sttReady: data.sttReady ?? false,
|
|
448
|
+
ttsProvider: data.ttsProvider ?? undefined,
|
|
449
|
+
ttsReady: data.ttsReady ?? false,
|
|
450
|
+
enabled: data.enabled ?? false,
|
|
451
|
+
defaultProject: data.defaultProject ?? undefined,
|
|
359
452
|
};
|
|
360
453
|
}
|
|
361
454
|
/**
|
|
@@ -670,6 +763,186 @@ class P2PClient {
|
|
|
670
763
|
}
|
|
671
764
|
}
|
|
672
765
|
/** Internal helper for authenticated GET/POST requests. */
|
|
766
|
+
/**
|
|
767
|
+
* Convergence point for ALL feedback surfaces — Tasks tab, in-Yaver
|
|
768
|
+
* native pane, and this standalone SDK all POST the same shape to
|
|
769
|
+
* `/tasks`. Wraps the user's text with the shared prompt builder
|
|
770
|
+
* (see `_core/buildFeedbackPrompt`) so every surface conditions
|
|
771
|
+
* the AI the same way.
|
|
772
|
+
*
|
|
773
|
+
* Returns the agent's response payload (`taskId`, etc.) so callers
|
|
774
|
+
* can wire `streamTaskOutput()` next for live transcript.
|
|
775
|
+
*
|
|
776
|
+
* Inputs:
|
|
777
|
+
* - userPrompt what the user typed
|
|
778
|
+
* - projectName / path optional Hot-Reload project context
|
|
779
|
+
* - runner / model optional preferred coding agent + model
|
|
780
|
+
* - screenshotBase64 optional JPEG base64 (no `data:` prefix)
|
|
781
|
+
* - imageMimeType defaults to "image/jpeg"
|
|
782
|
+
*/
|
|
783
|
+
async createFeedbackTask(input) {
|
|
784
|
+
const { buildFeedbackPrompt } = await Promise.resolve().then(() => __importStar(require('./_core/buildFeedbackPrompt')));
|
|
785
|
+
const hasScreenshot = !!(input.screenshotBase64 && input.screenshotBase64.length > 0);
|
|
786
|
+
const description = buildFeedbackPrompt({
|
|
787
|
+
userPrompt: input.userPrompt,
|
|
788
|
+
projectName: input.projectName,
|
|
789
|
+
projectPath: input.projectPath,
|
|
790
|
+
hasScreenshot,
|
|
791
|
+
});
|
|
792
|
+
const images = [];
|
|
793
|
+
if (hasScreenshot && input.screenshotBase64) {
|
|
794
|
+
images.push({
|
|
795
|
+
base64: input.screenshotBase64,
|
|
796
|
+
mimeType: input.imageMimeType ?? 'image/jpeg',
|
|
797
|
+
filename: `yaver-feedback-${Math.floor(Date.now() / 1000)}.jpg`,
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
const body = {
|
|
801
|
+
title: input.userPrompt.slice(0, 80),
|
|
802
|
+
description,
|
|
803
|
+
userPrompt: input.userPrompt,
|
|
804
|
+
source: 'mobile-feedback',
|
|
805
|
+
images,
|
|
806
|
+
};
|
|
807
|
+
if (input.projectPath && input.projectPath.trim())
|
|
808
|
+
body.workDir = input.projectPath.trim();
|
|
809
|
+
if (input.projectName && input.projectName.trim())
|
|
810
|
+
body.projectName = input.projectName.trim();
|
|
811
|
+
if (input.runner && input.runner.trim())
|
|
812
|
+
body.runner = input.runner.trim();
|
|
813
|
+
if (input.model && input.model.trim())
|
|
814
|
+
body.model = input.model.trim();
|
|
815
|
+
const resp = await fetch(`${this.baseUrl}/tasks`, {
|
|
816
|
+
method: 'POST',
|
|
817
|
+
headers: this.authHeaders({ 'Content-Type': 'application/json' }),
|
|
818
|
+
body: JSON.stringify(body),
|
|
819
|
+
});
|
|
820
|
+
if (!resp.ok) {
|
|
821
|
+
const text = await resp.text().catch(() => '');
|
|
822
|
+
throw new Error(`createFeedbackTask HTTP ${resp.status}: ${text}`);
|
|
823
|
+
}
|
|
824
|
+
const json = (await resp.json().catch(() => ({})));
|
|
825
|
+
if (!json.taskId) {
|
|
826
|
+
throw new Error('createFeedbackTask: agent did not return taskId');
|
|
827
|
+
}
|
|
828
|
+
return { taskId: json.taskId, raw: json };
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Subscribe to a task's live stdout/stderr stream. Returns an abort
|
|
832
|
+
* function — call it to detach. The agent emits NDJSON lines on
|
|
833
|
+
* `/tasks/{id}/output`; we surface each line via `onLine`.
|
|
834
|
+
*
|
|
835
|
+
* `onComplete` fires when the agent reports the task entered a
|
|
836
|
+
* terminal status (completed / failed / stopped). After that the
|
|
837
|
+
* caller should stop calling abort().
|
|
838
|
+
*
|
|
839
|
+
* Robust to fetch streaming on Hermes (streams Body via Response.
|
|
840
|
+
* body.getReader on platforms that support it; falls back to
|
|
841
|
+
* polling `/tasks/{id}` every 750 ms if streaming isn't available).
|
|
842
|
+
*/
|
|
843
|
+
streamTaskOutput(taskId, onLine, onComplete) {
|
|
844
|
+
const ctrl = new AbortController();
|
|
845
|
+
let closed = false;
|
|
846
|
+
const close = () => {
|
|
847
|
+
if (closed)
|
|
848
|
+
return;
|
|
849
|
+
closed = true;
|
|
850
|
+
try {
|
|
851
|
+
ctrl.abort();
|
|
852
|
+
}
|
|
853
|
+
catch { /* ignore */ }
|
|
854
|
+
};
|
|
855
|
+
(async () => {
|
|
856
|
+
try {
|
|
857
|
+
const resp = await fetch(`${this.baseUrl}/tasks/${encodeURIComponent(taskId)}/output`, {
|
|
858
|
+
method: 'GET',
|
|
859
|
+
headers: this.authHeaders({ Accept: 'text/event-stream' }),
|
|
860
|
+
signal: ctrl.signal,
|
|
861
|
+
});
|
|
862
|
+
if (!resp.ok) {
|
|
863
|
+
throw new Error(`streamTaskOutput HTTP ${resp.status}`);
|
|
864
|
+
}
|
|
865
|
+
// RN Hermes: Response.body may be undefined. Fall back to
|
|
866
|
+
// polling final state.
|
|
867
|
+
const body = resp.body;
|
|
868
|
+
if (!body || typeof body.getReader !== 'function') {
|
|
869
|
+
await pollTaskUntilDone(this, taskId, onLine, onComplete, () => closed);
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
const reader = body.getReader();
|
|
873
|
+
const decoder = new TextDecoder();
|
|
874
|
+
let buf = '';
|
|
875
|
+
while (!closed) {
|
|
876
|
+
const { value, done } = await reader.read();
|
|
877
|
+
if (done)
|
|
878
|
+
break;
|
|
879
|
+
buf += decoder.decode(value, { stream: true });
|
|
880
|
+
// SSE frames are separated by \n\n; payloads are `data: <json>\n`.
|
|
881
|
+
let idx = buf.indexOf('\n\n');
|
|
882
|
+
while (idx >= 0) {
|
|
883
|
+
const frame = buf.slice(0, idx);
|
|
884
|
+
buf = buf.slice(idx + 2);
|
|
885
|
+
for (const line of frame.split('\n')) {
|
|
886
|
+
if (line.startsWith('data:')) {
|
|
887
|
+
const payload = line.slice(5).trim();
|
|
888
|
+
if (payload)
|
|
889
|
+
onLine(payload);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
idx = buf.indexOf('\n\n');
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
// Stream closed cleanly — query final status.
|
|
896
|
+
try {
|
|
897
|
+
const final = await fetch(`${this.baseUrl}/tasks/${encodeURIComponent(taskId)}`, { headers: this.authHeaders() });
|
|
898
|
+
const j = (await final.json().catch(() => ({})));
|
|
899
|
+
onComplete(j.status ?? 'completed');
|
|
900
|
+
}
|
|
901
|
+
catch {
|
|
902
|
+
onComplete('completed');
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
catch (e) {
|
|
906
|
+
if (!closed) {
|
|
907
|
+
// Surface the error via onLine so the UI shows it inline,
|
|
908
|
+
// then mark complete so the caller stops waiting.
|
|
909
|
+
onLine(`__error__: ${e instanceof Error ? e.message : String(e)}`);
|
|
910
|
+
onComplete('failed');
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
})();
|
|
914
|
+
return close;
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Send a follow-up message into an existing task — multi-turn vibe
|
|
918
|
+
* chat. The agent's `/tasks/{id}/resume` accepts the same shape as
|
|
919
|
+
* `/tasks` (description / userPrompt / images), and the existing
|
|
920
|
+
* task picks back up with the same runner + project context.
|
|
921
|
+
*/
|
|
922
|
+
async resumeTask(input) {
|
|
923
|
+
const images = [];
|
|
924
|
+
if (input.screenshotBase64 && input.screenshotBase64.length > 0) {
|
|
925
|
+
images.push({
|
|
926
|
+
base64: input.screenshotBase64,
|
|
927
|
+
mimeType: input.imageMimeType ?? 'image/jpeg',
|
|
928
|
+
filename: `yaver-feedback-followup-${Math.floor(Date.now() / 1000)}.jpg`,
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
const resp = await fetch(`${this.baseUrl}/tasks/${encodeURIComponent(input.taskId)}/resume`, {
|
|
932
|
+
method: 'POST',
|
|
933
|
+
headers: this.authHeaders({ 'Content-Type': 'application/json' }),
|
|
934
|
+
body: JSON.stringify({
|
|
935
|
+
description: input.userPrompt,
|
|
936
|
+
userPrompt: input.userPrompt,
|
|
937
|
+
source: 'mobile-feedback',
|
|
938
|
+
images,
|
|
939
|
+
}),
|
|
940
|
+
});
|
|
941
|
+
if (!resp.ok) {
|
|
942
|
+
const text = await resp.text().catch(() => '');
|
|
943
|
+
throw new Error(`resumeTask HTTP ${resp.status}: ${text}`);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
673
946
|
async request(method, path) {
|
|
674
947
|
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
675
948
|
method,
|
|
@@ -683,3 +956,43 @@ class P2PClient {
|
|
|
683
956
|
}
|
|
684
957
|
}
|
|
685
958
|
exports.P2PClient = P2PClient;
|
|
959
|
+
/**
|
|
960
|
+
* Fallback path used by streamTaskOutput when the platform's fetch
|
|
961
|
+
* returns a Response with no streaming body (older Hermes builds).
|
|
962
|
+
* Polls `/tasks/{id}` every 750 ms; surfaces newly-appended output
|
|
963
|
+
* lines via `onLine`, fires `onComplete` when status is terminal.
|
|
964
|
+
*/
|
|
965
|
+
async function pollTaskUntilDone(client, taskId, onLine, onComplete, isClosed) {
|
|
966
|
+
// Use bracket access to read the private baseUrl/authHeaders without
|
|
967
|
+
// making them public — confined to this file's scope.
|
|
968
|
+
const c = client;
|
|
969
|
+
let lastLen = 0;
|
|
970
|
+
for (;;) {
|
|
971
|
+
if (isClosed())
|
|
972
|
+
return;
|
|
973
|
+
try {
|
|
974
|
+
const r = await fetch(`${c.baseUrl}/tasks/${encodeURIComponent(taskId)}`, {
|
|
975
|
+
headers: c.authHeaders(),
|
|
976
|
+
});
|
|
977
|
+
const j = (await r.json().catch(() => ({})));
|
|
978
|
+
const all = Array.isArray(j.output) ? j.output : (j.output ? [j.output] : []);
|
|
979
|
+
const flat = all.join('\n');
|
|
980
|
+
if (flat.length > lastLen) {
|
|
981
|
+
const fresh = flat.slice(lastLen);
|
|
982
|
+
lastLen = flat.length;
|
|
983
|
+
for (const ln of fresh.split('\n')) {
|
|
984
|
+
if (ln.length > 0)
|
|
985
|
+
onLine(ln);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
if (j.status && ['completed', 'failed', 'stopped'].includes(j.status)) {
|
|
989
|
+
onComplete(j.status);
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
catch {
|
|
994
|
+
// Transient — keep polling.
|
|
995
|
+
}
|
|
996
|
+
await new Promise((res) => setTimeout(res, 750));
|
|
997
|
+
}
|
|
998
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { P2PClient } from './P2PClient';
|
|
3
|
+
export type VibeTurnRole = 'user' | 'assistant' | 'status';
|
|
4
|
+
export interface VibeTurn {
|
|
5
|
+
id: string;
|
|
6
|
+
role: VibeTurnRole;
|
|
7
|
+
text: string;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
}
|
|
10
|
+
interface Props {
|
|
11
|
+
client: P2PClient;
|
|
12
|
+
initialTaskId: string;
|
|
13
|
+
initialUserPrompt: string;
|
|
14
|
+
onClose?: () => void;
|
|
15
|
+
/** Called when the user taps Reload after a task completes — uses
|
|
16
|
+
* P2PClient.reloadApp() with the active project context. */
|
|
17
|
+
onReload?: () => Promise<void>;
|
|
18
|
+
/** Optional context forwarded to the voice stream so the agent runs
|
|
19
|
+
* the task against the right project / runner / model. */
|
|
20
|
+
project?: string;
|
|
21
|
+
model?: string;
|
|
22
|
+
runner?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function VibeChatScreen({ client, initialTaskId, initialUserPrompt, onClose, onReload, project, model, runner, }: Props): React.JSX.Element;
|
|
25
|
+
export {};
|