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
package/src/FloatingButton.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
TextInput,
|
|
12
12
|
TouchableOpacity,
|
|
13
13
|
View,
|
|
14
|
+
useWindowDimensions,
|
|
14
15
|
} from 'react-native';
|
|
15
16
|
import { YaverFeedback } from './YaverFeedback';
|
|
16
17
|
import { FixReport } from './FixReport';
|
|
@@ -58,6 +59,15 @@ export interface FloatingButtonProps {
|
|
|
58
59
|
|
|
59
60
|
const DEFAULT_SIZE = 40;
|
|
60
61
|
const DEFAULT_COLOR = '#6366f1';
|
|
62
|
+
|
|
63
|
+
// Tablet detection — short-edge dp >= 600 means iPad / 7"+ Android
|
|
64
|
+
// tablet / Z Fold open. The SDK has no app-side responsive context
|
|
65
|
+
// to lean on (it's a guest in third-party apps), so we infer
|
|
66
|
+
// locally and bump the button + panel to tablet sizes.
|
|
67
|
+
const TABLET_SHORT_EDGE = 600;
|
|
68
|
+
function isTabletWindow(width: number, height: number): boolean {
|
|
69
|
+
return Math.min(width, height) >= TABLET_SHORT_EDGE;
|
|
70
|
+
}
|
|
61
71
|
const DEFAULT_PANEL_BG = '#2d2d2d';
|
|
62
72
|
|
|
63
73
|
/**
|
|
@@ -101,7 +111,16 @@ export const FloatingButton: React.FC<FloatingButtonProps> = ({
|
|
|
101
111
|
healthCheckInterval = 5000,
|
|
102
112
|
panelBackgroundColor,
|
|
103
113
|
}) => {
|
|
104
|
-
|
|
114
|
+
// Read window size live so the SDK overlay re-pins itself when
|
|
115
|
+
// the host app rotates or splits. The legacy snapshot via
|
|
116
|
+
// Dimensions.get only ran once and parked the button off-screen
|
|
117
|
+
// after orientation changes on iPad.
|
|
118
|
+
const { width: screenWidth, height: screenHeight } = useWindowDimensions();
|
|
119
|
+
const isTablet = isTabletWindow(screenWidth, screenHeight);
|
|
120
|
+
// Tablets get a larger touch target and a wider panel — phones
|
|
121
|
+
// keep the existing 40 / 280 defaults so guest apps aren't
|
|
122
|
+
// disrupted on small screens.
|
|
123
|
+
const effectiveSize = isTablet ? Math.max(size, 52) : size;
|
|
105
124
|
const defaultX = initialPosition?.x ?? 10;
|
|
106
125
|
const defaultY = initialPosition?.y ?? 90;
|
|
107
126
|
|
|
@@ -535,7 +554,12 @@ export const FloatingButton: React.FC<FloatingButtonProps> = ({
|
|
|
535
554
|
const buttonIcon = icon ?? 'y';
|
|
536
555
|
const btnBg = isConnected ? color : `${color}88`;
|
|
537
556
|
|
|
538
|
-
|
|
557
|
+
// Panel sizing — tablets get a wider compact panel (420) and a
|
|
558
|
+
// capped full-size panel (max 720 instead of full window) so the
|
|
559
|
+
// overlay doesn't dwarf the host app on a 12.9" iPad.
|
|
560
|
+
const compactPanelWidth = isTablet ? 420 : 280;
|
|
561
|
+
const fullPanelWidth = isTablet ? Math.min(screenWidth - 24, 720) : screenWidth - 24;
|
|
562
|
+
const panelWidth = fullSize ? fullPanelWidth : compactPanelWidth;
|
|
539
563
|
|
|
540
564
|
return (
|
|
541
565
|
<Animated.View
|
|
@@ -730,7 +754,7 @@ export const FloatingButton: React.FC<FloatingButtonProps> = ({
|
|
|
730
754
|
style={[
|
|
731
755
|
s.button,
|
|
732
756
|
isTerminal ? s.buttonTerminal : s.buttonMinimal,
|
|
733
|
-
{ backgroundColor: btnBg, width:
|
|
757
|
+
{ backgroundColor: btnBg, width: effectiveSize, height: effectiveSize },
|
|
734
758
|
!isTerminal && { borderRadius: size / 2 },
|
|
735
759
|
]}
|
|
736
760
|
activeOpacity={0.7}
|
|
@@ -102,7 +102,14 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
|
|
|
102
102
|
return;
|
|
103
103
|
}
|
|
104
104
|
const direct = await probeDeviceReachability(device);
|
|
105
|
-
|
|
105
|
+
// Do not hard-block selection just because the LAN /health probe
|
|
106
|
+
// failed. The standalone SDK can still reach a healthy machine via
|
|
107
|
+
// the normal selected-device discovery path (including relay), and
|
|
108
|
+
// the Yaver host path may already be proving the machine works.
|
|
109
|
+
// Only treat the machine as unpickable when BOTH:
|
|
110
|
+
// 1. Convex says it is offline, and
|
|
111
|
+
// 2. the direct probe also failed.
|
|
112
|
+
if (!device.isOnline && !direct.reachable && !device.needsAuth) {
|
|
106
113
|
setError('Selected machine is not responding. Start `yaver serve` on it and try again.');
|
|
107
114
|
setReachability((prev) => ({ ...prev, [device.deviceId]: direct }));
|
|
108
115
|
return;
|
|
@@ -130,7 +137,9 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
|
|
|
130
137
|
? '#f59e0b'
|
|
131
138
|
: effectivelyReachable
|
|
132
139
|
? '#22c55e'
|
|
133
|
-
:
|
|
140
|
+
: device.isOnline
|
|
141
|
+
? '#f59e0b'
|
|
142
|
+
: explicitlyOffline || !device.isOnline
|
|
134
143
|
? '#ef4444'
|
|
135
144
|
: '#22c55e';
|
|
136
145
|
// Derive a single short status phrase the user can act on.
|
|
@@ -145,7 +154,7 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
|
|
|
145
154
|
statusLine =
|
|
146
155
|
'Needs pairing — open the Yaver app to adopt this machine';
|
|
147
156
|
} else if (explicitlyOffline) {
|
|
148
|
-
statusLine = '
|
|
157
|
+
statusLine = 'Online, but direct probe failed — relay / selected-machine path may still work';
|
|
149
158
|
} else if (device.runnerDown) {
|
|
150
159
|
statusLine = 'Runner down — restart the coding agent on the Mac';
|
|
151
160
|
} else {
|
package/src/P2PClient.ts
CHANGED
|
@@ -3,8 +3,10 @@ import {
|
|
|
3
3
|
CapabilitySnapshot,
|
|
4
4
|
FeedbackBundle,
|
|
5
5
|
IncidentEvent,
|
|
6
|
+
OpenCodeConfigSummary,
|
|
6
7
|
OperationState,
|
|
7
8
|
RunnerBrowserAuthSession,
|
|
9
|
+
RunnerAuthStatusRow,
|
|
8
10
|
TestSession,
|
|
9
11
|
VoiceCapability,
|
|
10
12
|
} from './types';
|
|
@@ -31,7 +33,7 @@ export interface ReloadAck {
|
|
|
31
33
|
* NativeModules. None of the lookups throw — missing data just means
|
|
32
34
|
* the agent will fall back to its own dev-server resolution.
|
|
33
35
|
*/
|
|
34
|
-
function resolveAppIdentity(opts?: {
|
|
36
|
+
export function resolveAppIdentity(opts?: {
|
|
35
37
|
projectName?: string;
|
|
36
38
|
bundleId?: string;
|
|
37
39
|
projectPath?: string;
|
|
@@ -154,6 +156,25 @@ export class P2PClient {
|
|
|
154
156
|
this.relayPassword = password;
|
|
155
157
|
}
|
|
156
158
|
|
|
159
|
+
/** Read-only base URL — used by the voice vibe-coding path to probe
|
|
160
|
+
* GET /voice/status before opening the stream. */
|
|
161
|
+
get agentBaseUrl(): string {
|
|
162
|
+
return this.baseUrl;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** WebSocket URL for the agent's voice stream (WS /voice/stream). The
|
|
166
|
+
* voice vibe-coding loop streams mic audio here and receives the
|
|
167
|
+
* transcript + agent task + TTS frames back. */
|
|
168
|
+
voiceStreamUrl(): string {
|
|
169
|
+
return this.baseUrl.replace(/^http/, 'ws') + '/voice/stream';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Auth headers for the voice WS + status probe — same bearer (and
|
|
173
|
+
* relay password) as every other agent request. */
|
|
174
|
+
voiceAuthHeaders(): Record<string, string> {
|
|
175
|
+
return this.authHeaders();
|
|
176
|
+
}
|
|
177
|
+
|
|
157
178
|
/** Merge in Authorization + (optional) X-Relay-Password on top of a header block. */
|
|
158
179
|
private authHeaders(extra: Record<string, string> = {}): Record<string, string> {
|
|
159
180
|
const h: Record<string, string> = { ...extra };
|
|
@@ -222,6 +243,60 @@ export class P2PClient {
|
|
|
222
243
|
return data.session as RunnerBrowserAuthSession;
|
|
223
244
|
}
|
|
224
245
|
|
|
246
|
+
async getRunnerAuthStatus(): Promise<RunnerAuthStatusRow[]> {
|
|
247
|
+
const resp = await fetch(`${this.baseUrl}/runner-auth/status`, {
|
|
248
|
+
headers: this.authHeaders(),
|
|
249
|
+
});
|
|
250
|
+
if (!resp.ok) {
|
|
251
|
+
const text = await resp.text().catch(() => '');
|
|
252
|
+
throw new Error(`getRunnerAuthStatus HTTP ${resp.status}: ${text}`);
|
|
253
|
+
}
|
|
254
|
+
const data = await resp.json().catch(() => ({} as Record<string, unknown>));
|
|
255
|
+
return Array.isArray(data.runners) ? (data.runners as RunnerAuthStatusRow[]) : [];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async getOpenCodeConfig(): Promise<OpenCodeConfigSummary | null> {
|
|
259
|
+
const resp = await fetch(`${this.baseUrl}/runner/opencode/config`, {
|
|
260
|
+
headers: this.authHeaders(),
|
|
261
|
+
});
|
|
262
|
+
if (!resp.ok) {
|
|
263
|
+
const text = await resp.text().catch(() => '');
|
|
264
|
+
throw new Error(`getOpenCodeConfig HTTP ${resp.status}: ${text}`);
|
|
265
|
+
}
|
|
266
|
+
const data = await resp.json().catch(() => ({} as Record<string, unknown>));
|
|
267
|
+
return (data.config ?? null) as OpenCodeConfigSummary | null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async saveOpenCodeConfig(patch: {
|
|
271
|
+
defaultAgent?: string;
|
|
272
|
+
model?: string;
|
|
273
|
+
smallModel?: string;
|
|
274
|
+
buildModel?: string;
|
|
275
|
+
planModel?: string;
|
|
276
|
+
providers?: Array<{
|
|
277
|
+
id: string;
|
|
278
|
+
name?: string;
|
|
279
|
+
baseUrl?: string;
|
|
280
|
+
apiKey?: string;
|
|
281
|
+
delete?: boolean;
|
|
282
|
+
}>;
|
|
283
|
+
}): Promise<{ ok: boolean; config?: OpenCodeConfigSummary; error?: string }> {
|
|
284
|
+
try {
|
|
285
|
+
const resp = await fetch(`${this.baseUrl}/runner/opencode/config`, {
|
|
286
|
+
method: 'POST',
|
|
287
|
+
headers: { ...this.authHeaders(), 'Content-Type': 'application/json' },
|
|
288
|
+
body: JSON.stringify(patch),
|
|
289
|
+
});
|
|
290
|
+
const data = await resp.json().catch(() => ({} as Record<string, unknown>));
|
|
291
|
+
if (!resp.ok) {
|
|
292
|
+
return { ok: false, error: (data.error as string | undefined) || `HTTP ${resp.status}` };
|
|
293
|
+
}
|
|
294
|
+
return { ok: true, config: data.config as OpenCodeConfigSummary | undefined };
|
|
295
|
+
} catch (err) {
|
|
296
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
225
300
|
async capabilitySnapshot(): Promise<CapabilitySnapshot | null> {
|
|
226
301
|
try {
|
|
227
302
|
const resp = await fetch(`${this.baseUrl}/capabilities/snapshot`, { headers: this.authHeaders() });
|
|
@@ -422,6 +497,10 @@ export class P2PClient {
|
|
|
422
497
|
s2sReady: data.s2sReady ?? false,
|
|
423
498
|
sttProvider: data.sttProvider ?? undefined,
|
|
424
499
|
sttReady: data.sttReady ?? false,
|
|
500
|
+
ttsProvider: data.ttsProvider ?? undefined,
|
|
501
|
+
ttsReady: data.ttsReady ?? false,
|
|
502
|
+
enabled: data.enabled ?? false,
|
|
503
|
+
defaultProject: data.defaultProject ?? undefined,
|
|
425
504
|
};
|
|
426
505
|
}
|
|
427
506
|
|
|
@@ -807,6 +886,202 @@ export class P2PClient {
|
|
|
807
886
|
}
|
|
808
887
|
|
|
809
888
|
/** Internal helper for authenticated GET/POST requests. */
|
|
889
|
+
/**
|
|
890
|
+
* Convergence point for ALL feedback surfaces — Tasks tab, in-Yaver
|
|
891
|
+
* native pane, and this standalone SDK all POST the same shape to
|
|
892
|
+
* `/tasks`. Wraps the user's text with the shared prompt builder
|
|
893
|
+
* (see `_core/buildFeedbackPrompt`) so every surface conditions
|
|
894
|
+
* the AI the same way.
|
|
895
|
+
*
|
|
896
|
+
* Returns the agent's response payload (`taskId`, etc.) so callers
|
|
897
|
+
* can wire `streamTaskOutput()` next for live transcript.
|
|
898
|
+
*
|
|
899
|
+
* Inputs:
|
|
900
|
+
* - userPrompt what the user typed
|
|
901
|
+
* - projectName / path optional Hot-Reload project context
|
|
902
|
+
* - runner / model optional preferred coding agent + model
|
|
903
|
+
* - screenshotBase64 optional JPEG base64 (no `data:` prefix)
|
|
904
|
+
* - imageMimeType defaults to "image/jpeg"
|
|
905
|
+
*/
|
|
906
|
+
async createFeedbackTask(input: {
|
|
907
|
+
userPrompt: string;
|
|
908
|
+
projectName?: string;
|
|
909
|
+
projectPath?: string;
|
|
910
|
+
runner?: string;
|
|
911
|
+
model?: string;
|
|
912
|
+
screenshotBase64?: string;
|
|
913
|
+
imageMimeType?: string;
|
|
914
|
+
}): Promise<{ taskId: string; raw?: unknown }> {
|
|
915
|
+
const { buildFeedbackPrompt } = await import('./_core/buildFeedbackPrompt');
|
|
916
|
+
const hasScreenshot = !!(input.screenshotBase64 && input.screenshotBase64.length > 0);
|
|
917
|
+
const description = buildFeedbackPrompt({
|
|
918
|
+
userPrompt: input.userPrompt,
|
|
919
|
+
projectName: input.projectName,
|
|
920
|
+
projectPath: input.projectPath,
|
|
921
|
+
hasScreenshot,
|
|
922
|
+
});
|
|
923
|
+
const images: Array<{ base64: string; mimeType: string; filename: string }> = [];
|
|
924
|
+
if (hasScreenshot && input.screenshotBase64) {
|
|
925
|
+
images.push({
|
|
926
|
+
base64: input.screenshotBase64,
|
|
927
|
+
mimeType: input.imageMimeType ?? 'image/jpeg',
|
|
928
|
+
filename: `yaver-feedback-${Math.floor(Date.now() / 1000)}.jpg`,
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
const body: Record<string, unknown> = {
|
|
932
|
+
title: input.userPrompt.slice(0, 80),
|
|
933
|
+
description,
|
|
934
|
+
userPrompt: input.userPrompt,
|
|
935
|
+
source: 'mobile-feedback',
|
|
936
|
+
images,
|
|
937
|
+
};
|
|
938
|
+
if (input.projectPath && input.projectPath.trim()) body.workDir = input.projectPath.trim();
|
|
939
|
+
if (input.projectName && input.projectName.trim()) body.projectName = input.projectName.trim();
|
|
940
|
+
if (input.runner && input.runner.trim()) body.runner = input.runner.trim();
|
|
941
|
+
if (input.model && input.model.trim()) body.model = input.model.trim();
|
|
942
|
+
|
|
943
|
+
const resp = await fetch(`${this.baseUrl}/tasks`, {
|
|
944
|
+
method: 'POST',
|
|
945
|
+
headers: this.authHeaders({ 'Content-Type': 'application/json' }),
|
|
946
|
+
body: JSON.stringify(body),
|
|
947
|
+
});
|
|
948
|
+
if (!resp.ok) {
|
|
949
|
+
const text = await resp.text().catch(() => '');
|
|
950
|
+
throw new Error(`createFeedbackTask HTTP ${resp.status}: ${text}`);
|
|
951
|
+
}
|
|
952
|
+
const json = (await resp.json().catch(() => ({}))) as { taskId?: string };
|
|
953
|
+
if (!json.taskId) {
|
|
954
|
+
throw new Error('createFeedbackTask: agent did not return taskId');
|
|
955
|
+
}
|
|
956
|
+
return { taskId: json.taskId, raw: json };
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* Subscribe to a task's live stdout/stderr stream. Returns an abort
|
|
961
|
+
* function — call it to detach. The agent emits NDJSON lines on
|
|
962
|
+
* `/tasks/{id}/output`; we surface each line via `onLine`.
|
|
963
|
+
*
|
|
964
|
+
* `onComplete` fires when the agent reports the task entered a
|
|
965
|
+
* terminal status (completed / failed / stopped). After that the
|
|
966
|
+
* caller should stop calling abort().
|
|
967
|
+
*
|
|
968
|
+
* Robust to fetch streaming on Hermes (streams Body via Response.
|
|
969
|
+
* body.getReader on platforms that support it; falls back to
|
|
970
|
+
* polling `/tasks/{id}` every 750 ms if streaming isn't available).
|
|
971
|
+
*/
|
|
972
|
+
streamTaskOutput(
|
|
973
|
+
taskId: string,
|
|
974
|
+
onLine: (line: string) => void,
|
|
975
|
+
onComplete: (status: string) => void,
|
|
976
|
+
): () => void {
|
|
977
|
+
const ctrl = new AbortController();
|
|
978
|
+
let closed = false;
|
|
979
|
+
const close = () => {
|
|
980
|
+
if (closed) return;
|
|
981
|
+
closed = true;
|
|
982
|
+
try { ctrl.abort(); } catch { /* ignore */ }
|
|
983
|
+
};
|
|
984
|
+
|
|
985
|
+
(async () => {
|
|
986
|
+
try {
|
|
987
|
+
const resp = await fetch(`${this.baseUrl}/tasks/${encodeURIComponent(taskId)}/output`, {
|
|
988
|
+
method: 'GET',
|
|
989
|
+
headers: this.authHeaders({ Accept: 'text/event-stream' }),
|
|
990
|
+
signal: ctrl.signal,
|
|
991
|
+
});
|
|
992
|
+
if (!resp.ok) {
|
|
993
|
+
throw new Error(`streamTaskOutput HTTP ${resp.status}`);
|
|
994
|
+
}
|
|
995
|
+
// RN Hermes: Response.body may be undefined. Fall back to
|
|
996
|
+
// polling final state.
|
|
997
|
+
const body = (resp as unknown as { body?: ReadableStream<Uint8Array> }).body;
|
|
998
|
+
if (!body || typeof body.getReader !== 'function') {
|
|
999
|
+
await pollTaskUntilDone(this, taskId, onLine, onComplete, () => closed);
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
const reader = body.getReader();
|
|
1003
|
+
const decoder = new TextDecoder();
|
|
1004
|
+
let buf = '';
|
|
1005
|
+
while (!closed) {
|
|
1006
|
+
const { value, done } = await reader.read();
|
|
1007
|
+
if (done) break;
|
|
1008
|
+
buf += decoder.decode(value, { stream: true });
|
|
1009
|
+
// SSE frames are separated by \n\n; payloads are `data: <json>\n`.
|
|
1010
|
+
let idx = buf.indexOf('\n\n');
|
|
1011
|
+
while (idx >= 0) {
|
|
1012
|
+
const frame = buf.slice(0, idx);
|
|
1013
|
+
buf = buf.slice(idx + 2);
|
|
1014
|
+
for (const line of frame.split('\n')) {
|
|
1015
|
+
if (line.startsWith('data:')) {
|
|
1016
|
+
const payload = line.slice(5).trim();
|
|
1017
|
+
if (payload) onLine(payload);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
idx = buf.indexOf('\n\n');
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
// Stream closed cleanly — query final status.
|
|
1024
|
+
try {
|
|
1025
|
+
const final = await fetch(
|
|
1026
|
+
`${this.baseUrl}/tasks/${encodeURIComponent(taskId)}`,
|
|
1027
|
+
{ headers: this.authHeaders() },
|
|
1028
|
+
);
|
|
1029
|
+
const j = (await final.json().catch(() => ({}))) as { status?: string };
|
|
1030
|
+
onComplete(j.status ?? 'completed');
|
|
1031
|
+
} catch {
|
|
1032
|
+
onComplete('completed');
|
|
1033
|
+
}
|
|
1034
|
+
} catch (e) {
|
|
1035
|
+
if (!closed) {
|
|
1036
|
+
// Surface the error via onLine so the UI shows it inline,
|
|
1037
|
+
// then mark complete so the caller stops waiting.
|
|
1038
|
+
onLine(`__error__: ${e instanceof Error ? e.message : String(e)}`);
|
|
1039
|
+
onComplete('failed');
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
})();
|
|
1043
|
+
return close;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
/**
|
|
1047
|
+
* Send a follow-up message into an existing task — multi-turn vibe
|
|
1048
|
+
* chat. The agent's `/tasks/{id}/resume` accepts the same shape as
|
|
1049
|
+
* `/tasks` (description / userPrompt / images), and the existing
|
|
1050
|
+
* task picks back up with the same runner + project context.
|
|
1051
|
+
*/
|
|
1052
|
+
async resumeTask(input: {
|
|
1053
|
+
taskId: string;
|
|
1054
|
+
userPrompt: string;
|
|
1055
|
+
screenshotBase64?: string;
|
|
1056
|
+
imageMimeType?: string;
|
|
1057
|
+
}): Promise<void> {
|
|
1058
|
+
const images: Array<{ base64: string; mimeType: string; filename: string }> = [];
|
|
1059
|
+
if (input.screenshotBase64 && input.screenshotBase64.length > 0) {
|
|
1060
|
+
images.push({
|
|
1061
|
+
base64: input.screenshotBase64,
|
|
1062
|
+
mimeType: input.imageMimeType ?? 'image/jpeg',
|
|
1063
|
+
filename: `yaver-feedback-followup-${Math.floor(Date.now() / 1000)}.jpg`,
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
const resp = await fetch(
|
|
1067
|
+
`${this.baseUrl}/tasks/${encodeURIComponent(input.taskId)}/resume`,
|
|
1068
|
+
{
|
|
1069
|
+
method: 'POST',
|
|
1070
|
+
headers: this.authHeaders({ 'Content-Type': 'application/json' }),
|
|
1071
|
+
body: JSON.stringify({
|
|
1072
|
+
description: input.userPrompt,
|
|
1073
|
+
userPrompt: input.userPrompt,
|
|
1074
|
+
source: 'mobile-feedback',
|
|
1075
|
+
images,
|
|
1076
|
+
}),
|
|
1077
|
+
},
|
|
1078
|
+
);
|
|
1079
|
+
if (!resp.ok) {
|
|
1080
|
+
const text = await resp.text().catch(() => '');
|
|
1081
|
+
throw new Error(`resumeTask HTTP ${resp.status}: ${text}`);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
810
1085
|
private async request(method: string, path: string): Promise<Response> {
|
|
811
1086
|
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
812
1087
|
method,
|
|
@@ -823,3 +1098,53 @@ export class P2PClient {
|
|
|
823
1098
|
return response;
|
|
824
1099
|
}
|
|
825
1100
|
}
|
|
1101
|
+
|
|
1102
|
+
/**
|
|
1103
|
+
* Fallback path used by streamTaskOutput when the platform's fetch
|
|
1104
|
+
* returns a Response with no streaming body (older Hermes builds).
|
|
1105
|
+
* Polls `/tasks/{id}` every 750 ms; surfaces newly-appended output
|
|
1106
|
+
* lines via `onLine`, fires `onComplete` when status is terminal.
|
|
1107
|
+
*/
|
|
1108
|
+
async function pollTaskUntilDone(
|
|
1109
|
+
client: P2PClient,
|
|
1110
|
+
taskId: string,
|
|
1111
|
+
onLine: (line: string) => void,
|
|
1112
|
+
onComplete: (status: string) => void,
|
|
1113
|
+
isClosed: () => boolean,
|
|
1114
|
+
): Promise<void> {
|
|
1115
|
+
// Use bracket access to read the private baseUrl/authHeaders without
|
|
1116
|
+
// making them public — confined to this file's scope.
|
|
1117
|
+
const c = client as unknown as {
|
|
1118
|
+
baseUrl: string;
|
|
1119
|
+
authHeaders: (extra?: Record<string, string>) => Record<string, string>;
|
|
1120
|
+
};
|
|
1121
|
+
let lastLen = 0;
|
|
1122
|
+
for (;;) {
|
|
1123
|
+
if (isClosed()) return;
|
|
1124
|
+
try {
|
|
1125
|
+
const r = await fetch(`${c.baseUrl}/tasks/${encodeURIComponent(taskId)}`, {
|
|
1126
|
+
headers: c.authHeaders(),
|
|
1127
|
+
});
|
|
1128
|
+
const j = (await r.json().catch(() => ({}))) as {
|
|
1129
|
+
status?: string;
|
|
1130
|
+
output?: string[] | string;
|
|
1131
|
+
};
|
|
1132
|
+
const all = Array.isArray(j.output) ? j.output : (j.output ? [j.output] : []);
|
|
1133
|
+
const flat = all.join('\n');
|
|
1134
|
+
if (flat.length > lastLen) {
|
|
1135
|
+
const fresh = flat.slice(lastLen);
|
|
1136
|
+
lastLen = flat.length;
|
|
1137
|
+
for (const ln of fresh.split('\n')) {
|
|
1138
|
+
if (ln.length > 0) onLine(ln);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
if (j.status && ['completed', 'failed', 'stopped'].includes(j.status)) {
|
|
1142
|
+
onComplete(j.status);
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
} catch {
|
|
1146
|
+
// Transient — keep polling.
|
|
1147
|
+
}
|
|
1148
|
+
await new Promise((res) => setTimeout(res, 750));
|
|
1149
|
+
}
|
|
1150
|
+
}
|