yaver-feedback-react-native 0.8.11 → 0.8.13
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/BlackBox.d.ts +1 -0
- package/dist/BlackBox.js +29 -14
- package/dist/DeployPanel.d.ts +7 -0
- package/dist/DeployPanel.js +354 -0
- package/dist/FeedbackModal.js +94 -6
- package/dist/FloatingButton.js +25 -3
- package/dist/P2PClient.d.ts +71 -0
- package/dist/P2PClient.js +254 -0
- package/dist/VibeChatScreen.d.ts +20 -0
- package/dist/VibeChatScreen.js +328 -0
- package/dist/YaverFeedback.d.ts +7 -0
- package/dist/YaverFeedback.js +10 -1
- package/dist/_core/buildFeedbackPrompt.d.ts +13 -0
- package/dist/_core/buildFeedbackPrompt.js +77 -0
- package/dist/capture.d.ts +14 -0
- package/dist/capture.js +29 -0
- package/dist/preferences.d.ts +4 -0
- package/dist/preferences.js +62 -0
- package/dist/types.d.ts +9 -0
- package/dist/upload.d.ts +1 -1
- package/dist/upload.js +8 -4
- package/package.json +14 -2
- package/src/BlackBox.ts +29 -14
- package/src/DeployPanel.tsx +403 -0
- package/src/FeedbackModal.tsx +126 -5
- package/src/FloatingButton.tsx +27 -3
- package/src/P2PClient.ts +247 -1
- package/src/VibeChatScreen.tsx +362 -0
- package/src/YaverFeedback.ts +11 -1
- package/src/_core/buildFeedbackPrompt.ts +102 -0
- package/src/capture.ts +31 -0
- package/src/preferences.ts +56 -0
- package/src/types.ts +9 -0
- package/src/upload.ts +8 -3
package/app.plugin.js
CHANGED
|
@@ -51,9 +51,22 @@ function withYaverFeedbackAndroid(config) {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
const permissions = manifest["uses-permission"];
|
|
54
|
+
// Camera + mic for screenshot/voice. The rest are required to
|
|
55
|
+
// make `react-native-record-screen` actually start on modern
|
|
56
|
+
// Android — without FOREGROUND_SERVICE_MEDIA_PROJECTION
|
|
57
|
+
// (API 34+, mandatory) startRecording throws SecurityException,
|
|
58
|
+
// and without POST_NOTIFICATIONS (API 33+) the recording
|
|
59
|
+
// notification fails to post which some OEMs use as a signal
|
|
60
|
+
// to kill the projection a few seconds in. Inject all five
|
|
61
|
+
// unconditionally — listing them does NOT trigger any user
|
|
62
|
+
// prompt; the actual runtime dialogs only fire if the app
|
|
63
|
+
// calls startVideoRecording().
|
|
54
64
|
const requiredPermissions = [
|
|
55
65
|
"android.permission.CAMERA",
|
|
56
66
|
"android.permission.RECORD_AUDIO",
|
|
67
|
+
"android.permission.FOREGROUND_SERVICE",
|
|
68
|
+
"android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION",
|
|
69
|
+
"android.permission.POST_NOTIFICATIONS",
|
|
57
70
|
];
|
|
58
71
|
|
|
59
72
|
for (const perm of requiredPermissions) {
|
package/dist/BlackBox.d.ts
CHANGED
|
@@ -53,6 +53,7 @@ export type CommandHandler = (cmd: BlackBoxCommand) => void;
|
|
|
53
53
|
export declare class BlackBox {
|
|
54
54
|
private static baseUrl;
|
|
55
55
|
private static authToken;
|
|
56
|
+
private static relayPassword;
|
|
56
57
|
private static deviceId;
|
|
57
58
|
private static appName;
|
|
58
59
|
private static buffer;
|
package/dist/BlackBox.js
CHANGED
|
@@ -18,6 +18,7 @@ class BlackBox {
|
|
|
18
18
|
}
|
|
19
19
|
BlackBox.baseUrl = feedbackConfig.agentUrl.replace(/\/$/, '');
|
|
20
20
|
BlackBox.authToken = feedbackConfig.authToken ?? null;
|
|
21
|
+
BlackBox.relayPassword = feedbackConfig.relayPassword ?? '';
|
|
21
22
|
BlackBox.deviceId = config?.deviceId ?? BlackBox.generateDeviceId();
|
|
22
23
|
BlackBox.appName = config?.appName ?? '';
|
|
23
24
|
BlackBox.flushInterval = config?.flushInterval ?? 2000;
|
|
@@ -252,15 +253,19 @@ class BlackBox {
|
|
|
252
253
|
BlackBox.sseAbortController = controller;
|
|
253
254
|
const url = `${BlackBox.baseUrl}/blackbox/command-stream?device=${encodeURIComponent(BlackBox.deviceId)}`;
|
|
254
255
|
try {
|
|
256
|
+
const sseHeaders = {
|
|
257
|
+
Authorization: `Bearer ${BlackBox.authToken}`,
|
|
258
|
+
'X-Device-ID': BlackBox.deviceId,
|
|
259
|
+
'X-Platform': react_native_1.Platform.OS,
|
|
260
|
+
'X-App-Name': BlackBox.appName,
|
|
261
|
+
Accept: 'text/event-stream',
|
|
262
|
+
};
|
|
263
|
+
if (BlackBox.relayPassword) {
|
|
264
|
+
sseHeaders['X-Relay-Password'] = BlackBox.relayPassword;
|
|
265
|
+
}
|
|
255
266
|
const response = await fetch(url, {
|
|
256
267
|
method: 'GET',
|
|
257
|
-
headers:
|
|
258
|
-
Authorization: `Bearer ${BlackBox.authToken}`,
|
|
259
|
-
'X-Device-ID': BlackBox.deviceId,
|
|
260
|
-
'X-Platform': react_native_1.Platform.OS,
|
|
261
|
-
'X-App-Name': BlackBox.appName,
|
|
262
|
-
Accept: 'text/event-stream',
|
|
263
|
-
},
|
|
268
|
+
headers: sseHeaders,
|
|
264
269
|
// @ts-ignore — React Native supports signal on fetch
|
|
265
270
|
signal: controller.signal,
|
|
266
271
|
});
|
|
@@ -354,15 +359,19 @@ class BlackBox {
|
|
|
354
359
|
const events = BlackBox.buffer;
|
|
355
360
|
BlackBox.buffer = [];
|
|
356
361
|
try {
|
|
362
|
+
const flushHeaders = {
|
|
363
|
+
Authorization: `Bearer ${BlackBox.authToken}`,
|
|
364
|
+
'Content-Type': 'application/json',
|
|
365
|
+
'X-Device-ID': BlackBox.deviceId,
|
|
366
|
+
'X-Platform': react_native_1.Platform.OS,
|
|
367
|
+
'X-App-Name': BlackBox.appName,
|
|
368
|
+
};
|
|
369
|
+
if (BlackBox.relayPassword) {
|
|
370
|
+
flushHeaders['X-Relay-Password'] = BlackBox.relayPassword;
|
|
371
|
+
}
|
|
357
372
|
await fetch(`${BlackBox.baseUrl}/blackbox/events`, {
|
|
358
373
|
method: 'POST',
|
|
359
|
-
headers:
|
|
360
|
-
Authorization: `Bearer ${BlackBox.authToken}`,
|
|
361
|
-
'Content-Type': 'application/json',
|
|
362
|
-
'X-Device-ID': BlackBox.deviceId,
|
|
363
|
-
'X-Platform': react_native_1.Platform.OS,
|
|
364
|
-
'X-App-Name': BlackBox.appName,
|
|
365
|
-
},
|
|
374
|
+
headers: flushHeaders,
|
|
366
375
|
body: JSON.stringify(events),
|
|
367
376
|
});
|
|
368
377
|
}
|
|
@@ -380,6 +389,12 @@ class BlackBox {
|
|
|
380
389
|
exports.BlackBox = BlackBox;
|
|
381
390
|
BlackBox.baseUrl = null;
|
|
382
391
|
BlackBox.authToken = null;
|
|
392
|
+
// Mirror of the user's relay password — required for EVERY request that
|
|
393
|
+
// crosses the relay (`https://public.yaver.io/d/<id>/...`). Without it
|
|
394
|
+
// the relay rejects with HTTP 401 "invalid relay password" and the
|
|
395
|
+
// event stream silently fails. Set on start() from YaverFeedback's
|
|
396
|
+
// resolved relay password (same source the P2PClient uses).
|
|
397
|
+
BlackBox.relayPassword = '';
|
|
383
398
|
BlackBox.deviceId = '';
|
|
384
399
|
BlackBox.appName = '';
|
|
385
400
|
BlackBox.buffer = [];
|
|
@@ -0,0 +1,354 @@
|
|
|
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
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DeployPanel = void 0;
|
|
37
|
+
const react_1 = __importStar(require("react"));
|
|
38
|
+
const react_native_1 = require("react-native");
|
|
39
|
+
const YaverFeedback_1 = require("./YaverFeedback");
|
|
40
|
+
const TARGET_LABELS = {
|
|
41
|
+
testflight: 'TestFlight',
|
|
42
|
+
playstore: 'Play Store',
|
|
43
|
+
};
|
|
44
|
+
const DeployPanel = ({ onClose }) => {
|
|
45
|
+
const [options, setOptions] = (0, react_1.useState)(null);
|
|
46
|
+
const [loading, setLoading] = (0, react_1.useState)(true);
|
|
47
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
48
|
+
const [status, setStatus] = (0, react_1.useState)(null);
|
|
49
|
+
const [statusTone, setStatusTone] = (0, react_1.useState)('progress');
|
|
50
|
+
const [selected, setSelected] = (0, react_1.useState)('both');
|
|
51
|
+
const [shipping, setShipping] = (0, react_1.useState)(false);
|
|
52
|
+
const resolveAppSlug = (0, react_1.useCallback)(() => {
|
|
53
|
+
const cfg = YaverFeedback_1.YaverFeedback.getConfig();
|
|
54
|
+
const explicit = cfg?.deployAppSlug;
|
|
55
|
+
if (explicit && explicit.trim().length > 0)
|
|
56
|
+
return explicit.trim();
|
|
57
|
+
// Best-effort fallback: bundleId's last dot-segment. iOS gives us
|
|
58
|
+
// `io.yaver.sfmg`; Android gives the same shape. The agent's
|
|
59
|
+
// workspace manifest typically names apps after the project basename
|
|
60
|
+
// which is usually the same word, but the user can override via
|
|
61
|
+
// config.deployAppSlug if it isn't.
|
|
62
|
+
const bundleId = cfg?.bundleId;
|
|
63
|
+
if (bundleId) {
|
|
64
|
+
const tail = bundleId.split('.').pop();
|
|
65
|
+
if (tail)
|
|
66
|
+
return tail;
|
|
67
|
+
}
|
|
68
|
+
return 'main';
|
|
69
|
+
}, []);
|
|
70
|
+
const baseAuthHeaders = (0, react_1.useCallback)(() => {
|
|
71
|
+
const cfg = YaverFeedback_1.YaverFeedback.getConfig();
|
|
72
|
+
const headers = {};
|
|
73
|
+
if (cfg?.authToken)
|
|
74
|
+
headers.Authorization = `Bearer ${cfg.authToken}`;
|
|
75
|
+
const relay = YaverFeedback_1.YaverFeedback.getRelayPassword();
|
|
76
|
+
if (relay)
|
|
77
|
+
headers['X-Relay-Password'] = relay;
|
|
78
|
+
return headers;
|
|
79
|
+
}, []);
|
|
80
|
+
const fetchOptions = (0, react_1.useCallback)(async () => {
|
|
81
|
+
setLoading(true);
|
|
82
|
+
setError(null);
|
|
83
|
+
const cfg = YaverFeedback_1.YaverFeedback.getConfig();
|
|
84
|
+
if (!cfg?.agentUrl) {
|
|
85
|
+
setError('Not connected to a Yaver agent yet.');
|
|
86
|
+
setLoading(false);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const app = resolveAppSlug();
|
|
90
|
+
const url = `${cfg.agentUrl.replace(/\/$/, '')}/fleet/deploy-options?app=${encodeURIComponent(app)}`;
|
|
91
|
+
try {
|
|
92
|
+
const resp = await fetch(url, { headers: baseAuthHeaders() });
|
|
93
|
+
if (!resp.ok) {
|
|
94
|
+
const text = await resp.text().catch(() => '');
|
|
95
|
+
throw new Error(`fetch failed (${resp.status}): ${text || resp.statusText}`);
|
|
96
|
+
}
|
|
97
|
+
const json = (await resp.json());
|
|
98
|
+
setOptions(json);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
setLoading(false);
|
|
105
|
+
}
|
|
106
|
+
}, [baseAuthHeaders, resolveAppSlug]);
|
|
107
|
+
(0, react_1.useEffect)(() => {
|
|
108
|
+
void fetchOptions();
|
|
109
|
+
}, [fetchOptions]);
|
|
110
|
+
const pickedTargets = () => {
|
|
111
|
+
switch (selected) {
|
|
112
|
+
case 'testflight':
|
|
113
|
+
return ['testflight'];
|
|
114
|
+
case 'playstore':
|
|
115
|
+
return ['playstore'];
|
|
116
|
+
default:
|
|
117
|
+
return ['testflight', 'playstore'];
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const machineRow = (d) => {
|
|
121
|
+
const targets = pickedTargets();
|
|
122
|
+
const blockers = [];
|
|
123
|
+
let allOK = true;
|
|
124
|
+
for (const t of targets) {
|
|
125
|
+
const cap = d.capabilities.find((c) => c.target === t);
|
|
126
|
+
if (!cap) {
|
|
127
|
+
allOK = false;
|
|
128
|
+
blockers.push(`${TARGET_LABELS[t] ?? t}: no capability data`);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (!cap.ok) {
|
|
132
|
+
allOK = false;
|
|
133
|
+
if (cap.reason)
|
|
134
|
+
blockers.push(`${TARGET_LABELS[t] ?? t}: ${cap.reason}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (!d.probed && allOK) {
|
|
138
|
+
allOK = false;
|
|
139
|
+
blockers.push(d.probeError || "couldn't reach this machine");
|
|
140
|
+
}
|
|
141
|
+
const label = (d.alias && d.alias.length > 0 ? d.alias : d.name) +
|
|
142
|
+
(d.isLocal ? ' (this phone’s primary)' : '');
|
|
143
|
+
return (<react_native_1.Pressable key={d.deviceId} disabled={!allOK || shipping} onPress={() => triggerDeploy(d.deviceId)} style={({ pressed }) => [
|
|
144
|
+
styles.row,
|
|
145
|
+
!allOK && styles.rowDisabled,
|
|
146
|
+
pressed && allOK && styles.rowPressed,
|
|
147
|
+
]}>
|
|
148
|
+
<react_native_1.Text style={styles.rowName}>{label}</react_native_1.Text>
|
|
149
|
+
<react_native_1.Text style={[styles.rowMeta, !allOK && styles.rowMetaWarning]}>
|
|
150
|
+
{d.platform} {'·'} {allOK ? 'ready' : blockers.join(' · ')}
|
|
151
|
+
</react_native_1.Text>
|
|
152
|
+
</react_native_1.Pressable>);
|
|
153
|
+
};
|
|
154
|
+
const triggerDeploy = async (machine) => {
|
|
155
|
+
if (!options)
|
|
156
|
+
return;
|
|
157
|
+
setShipping(true);
|
|
158
|
+
setStatus(`starting deploy on ${prettyMachineName(machine)}…`);
|
|
159
|
+
setStatusTone('progress');
|
|
160
|
+
const cfg = YaverFeedback_1.YaverFeedback.getConfig();
|
|
161
|
+
if (!cfg?.agentUrl) {
|
|
162
|
+
setStatus('Not connected to a Yaver agent yet.');
|
|
163
|
+
setStatusTone('error');
|
|
164
|
+
setShipping(false);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const targets = pickedTargets();
|
|
168
|
+
const body = {
|
|
169
|
+
app: options.app,
|
|
170
|
+
machine,
|
|
171
|
+
};
|
|
172
|
+
if (targets.length === 1) {
|
|
173
|
+
body.target = targets[0];
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
body.targets = targets;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
const resp = await fetch(`${cfg.agentUrl.replace(/\/$/, '')}/deploy/ship`, {
|
|
180
|
+
method: 'POST',
|
|
181
|
+
headers: { ...baseAuthHeaders(), 'Content-Type': 'application/json' },
|
|
182
|
+
body: JSON.stringify(body),
|
|
183
|
+
});
|
|
184
|
+
if (!resp.ok) {
|
|
185
|
+
const text = await resp.text().catch(() => '');
|
|
186
|
+
throw new Error(`ship failed (${resp.status}): ${text || resp.statusText}`);
|
|
187
|
+
}
|
|
188
|
+
setStatus('deploy started — track progress in the desktop / web tab');
|
|
189
|
+
setStatusTone('success');
|
|
190
|
+
// Auto-close shortly so the user can keep using their app. Keep
|
|
191
|
+
// this in sync with the iOS / Android pane delays.
|
|
192
|
+
setTimeout(() => onClose(), 1600);
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
setStatus(err instanceof Error ? err.message : String(err));
|
|
196
|
+
setStatusTone('error');
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
setShipping(false);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
const prettyMachineName = (id) => {
|
|
203
|
+
const d = options?.devices.find((x) => x.deviceId === id);
|
|
204
|
+
if (!d)
|
|
205
|
+
return id;
|
|
206
|
+
return d.alias && d.alias.length > 0 ? d.alias : d.name;
|
|
207
|
+
};
|
|
208
|
+
const targetButton = (value, label) => (<react_native_1.Pressable key={value} onPress={() => setSelected(value)} style={[styles.segBtn, selected === value && styles.segBtnSelected]}>
|
|
209
|
+
<react_native_1.Text style={[styles.segText, selected === value && styles.segTextSelected]}>{label}</react_native_1.Text>
|
|
210
|
+
</react_native_1.Pressable>);
|
|
211
|
+
return (<react_native_1.View style={styles.container}>
|
|
212
|
+
<react_native_1.View style={styles.headerRow}>
|
|
213
|
+
<react_native_1.Text style={styles.title}>Deploy</react_native_1.Text>
|
|
214
|
+
<react_native_1.Pressable onPress={onClose} hitSlop={10}>
|
|
215
|
+
<react_native_1.Text style={styles.closeIcon}>✕</react_native_1.Text>
|
|
216
|
+
</react_native_1.Pressable>
|
|
217
|
+
</react_native_1.View>
|
|
218
|
+
<react_native_1.Text style={styles.subtitle}>
|
|
219
|
+
{loading
|
|
220
|
+
? 'loading machines…'
|
|
221
|
+
: options
|
|
222
|
+
? `${options.devices.length} machine${options.devices.length === 1 ? '' : 's'} — pick a target, then tap to deploy`
|
|
223
|
+
: 'no data yet'}
|
|
224
|
+
</react_native_1.Text>
|
|
225
|
+
|
|
226
|
+
<react_native_1.View style={styles.segment}>
|
|
227
|
+
{targetButton('testflight', 'TestFlight')}
|
|
228
|
+
{targetButton('playstore', 'Play Store')}
|
|
229
|
+
{targetButton('both', 'Both')}
|
|
230
|
+
</react_native_1.View>
|
|
231
|
+
|
|
232
|
+
{loading ? (<react_native_1.View style={styles.loading}>
|
|
233
|
+
<react_native_1.ActivityIndicator color="rgba(255,255,255,0.6)"/>
|
|
234
|
+
</react_native_1.View>) : error ? (<react_native_1.Text style={styles.error}>{error}</react_native_1.Text>) : options ? (<react_native_1.ScrollView style={styles.list}>{options.devices.map(machineRow)}</react_native_1.ScrollView>) : null}
|
|
235
|
+
|
|
236
|
+
{status && (<react_native_1.Text style={[
|
|
237
|
+
styles.status,
|
|
238
|
+
statusTone === 'success' && styles.statusSuccess,
|
|
239
|
+
statusTone === 'error' && styles.statusError,
|
|
240
|
+
]}>
|
|
241
|
+
{status}
|
|
242
|
+
</react_native_1.Text>)}
|
|
243
|
+
</react_native_1.View>);
|
|
244
|
+
};
|
|
245
|
+
exports.DeployPanel = DeployPanel;
|
|
246
|
+
const styles = react_native_1.StyleSheet.create({
|
|
247
|
+
container: {
|
|
248
|
+
backgroundColor: 'rgba(14,12,28,0.92)',
|
|
249
|
+
borderRadius: 16,
|
|
250
|
+
paddingHorizontal: 16,
|
|
251
|
+
paddingTop: 14,
|
|
252
|
+
paddingBottom: 18,
|
|
253
|
+
marginVertical: 8,
|
|
254
|
+
borderWidth: 1,
|
|
255
|
+
borderColor: 'rgba(255,255,255,0.08)',
|
|
256
|
+
},
|
|
257
|
+
headerRow: {
|
|
258
|
+
flexDirection: 'row',
|
|
259
|
+
alignItems: 'center',
|
|
260
|
+
justifyContent: 'space-between',
|
|
261
|
+
},
|
|
262
|
+
title: {
|
|
263
|
+
color: '#fff',
|
|
264
|
+
fontSize: 16,
|
|
265
|
+
fontWeight: '600',
|
|
266
|
+
},
|
|
267
|
+
closeIcon: {
|
|
268
|
+
color: 'rgba(255,255,255,0.55)',
|
|
269
|
+
fontSize: 18,
|
|
270
|
+
paddingHorizontal: 4,
|
|
271
|
+
},
|
|
272
|
+
subtitle: {
|
|
273
|
+
color: 'rgba(255,255,255,0.55)',
|
|
274
|
+
fontSize: 12,
|
|
275
|
+
marginTop: 2,
|
|
276
|
+
},
|
|
277
|
+
segment: {
|
|
278
|
+
flexDirection: 'row',
|
|
279
|
+
backgroundColor: 'rgba(255,255,255,0.08)',
|
|
280
|
+
borderRadius: 10,
|
|
281
|
+
padding: 3,
|
|
282
|
+
marginTop: 14,
|
|
283
|
+
},
|
|
284
|
+
segBtn: {
|
|
285
|
+
flex: 1,
|
|
286
|
+
alignItems: 'center',
|
|
287
|
+
paddingVertical: 7,
|
|
288
|
+
borderRadius: 8,
|
|
289
|
+
},
|
|
290
|
+
segBtnSelected: {
|
|
291
|
+
backgroundColor: 'rgba(127,140,247,0.65)',
|
|
292
|
+
},
|
|
293
|
+
segText: {
|
|
294
|
+
color: 'rgba(255,255,255,0.65)',
|
|
295
|
+
fontSize: 13,
|
|
296
|
+
fontWeight: '500',
|
|
297
|
+
},
|
|
298
|
+
segTextSelected: {
|
|
299
|
+
color: '#fff',
|
|
300
|
+
fontWeight: '600',
|
|
301
|
+
},
|
|
302
|
+
loading: {
|
|
303
|
+
paddingVertical: 32,
|
|
304
|
+
alignItems: 'center',
|
|
305
|
+
},
|
|
306
|
+
error: {
|
|
307
|
+
color: 'rgb(255,115,115)',
|
|
308
|
+
fontSize: 12,
|
|
309
|
+
marginTop: 14,
|
|
310
|
+
},
|
|
311
|
+
list: {
|
|
312
|
+
marginTop: 14,
|
|
313
|
+
maxHeight: 260,
|
|
314
|
+
},
|
|
315
|
+
row: {
|
|
316
|
+
backgroundColor: 'rgba(255,255,255,0.06)',
|
|
317
|
+
borderRadius: 12,
|
|
318
|
+
paddingHorizontal: 14,
|
|
319
|
+
paddingVertical: 12,
|
|
320
|
+
marginBottom: 8,
|
|
321
|
+
},
|
|
322
|
+
rowPressed: {
|
|
323
|
+
backgroundColor: 'rgba(255,255,255,0.10)',
|
|
324
|
+
},
|
|
325
|
+
rowDisabled: {
|
|
326
|
+
opacity: 0.55,
|
|
327
|
+
backgroundColor: 'rgba(255,255,255,0.03)',
|
|
328
|
+
},
|
|
329
|
+
rowName: {
|
|
330
|
+
color: '#fff',
|
|
331
|
+
fontSize: 15,
|
|
332
|
+
fontWeight: '600',
|
|
333
|
+
},
|
|
334
|
+
rowMeta: {
|
|
335
|
+
color: 'rgba(255,255,255,0.55)',
|
|
336
|
+
fontSize: 12,
|
|
337
|
+
marginTop: 2,
|
|
338
|
+
},
|
|
339
|
+
rowMetaWarning: {
|
|
340
|
+
color: 'rgb(255,178,115)',
|
|
341
|
+
},
|
|
342
|
+
status: {
|
|
343
|
+
color: 'rgba(255,255,255,0.55)',
|
|
344
|
+
fontSize: 12,
|
|
345
|
+
marginTop: 12,
|
|
346
|
+
textAlign: 'center',
|
|
347
|
+
},
|
|
348
|
+
statusSuccess: {
|
|
349
|
+
color: 'rgb(34,197,94)',
|
|
350
|
+
},
|
|
351
|
+
statusError: {
|
|
352
|
+
color: 'rgb(255,115,115)',
|
|
353
|
+
},
|
|
354
|
+
});
|
package/dist/FeedbackModal.js
CHANGED
|
@@ -41,9 +41,16 @@ const capture_1 = require("./capture");
|
|
|
41
41
|
const upload_1 = require("./upload");
|
|
42
42
|
const AuthOverlay_1 = require("./AuthOverlay");
|
|
43
43
|
const QuickActionIcon_1 = require("./QuickActionIcon");
|
|
44
|
+
const VibeChatScreen_1 = require("./VibeChatScreen");
|
|
45
|
+
const DeployPanel_1 = require("./DeployPanel");
|
|
44
46
|
const auth_1 = require("./auth");
|
|
45
47
|
const preferences_1 = require("./preferences");
|
|
46
48
|
const FeedbackModal = () => {
|
|
49
|
+
const { width: winW, height: winH } = (0, react_native_1.useWindowDimensions)();
|
|
50
|
+
const isTablet = Math.min(winW, winH) >= 600;
|
|
51
|
+
// Tablet color/icon picker fans out to 5/6 cols — 31% (3-col)
|
|
52
|
+
// looks empty on a 1024pt iPad. Mobile keeps 3-col.
|
|
53
|
+
const iconOptionWidthOverride = isTablet ? '18%' : undefined;
|
|
47
54
|
const [visible, setVisible] = (0, react_1.useState)(false);
|
|
48
55
|
const [action, setAction] = (0, react_1.useState)('idle');
|
|
49
56
|
const [error, setError] = (0, react_1.useState)(null);
|
|
@@ -61,6 +68,7 @@ const FeedbackModal = () => {
|
|
|
61
68
|
// "pick something for me" prompt (which in 0.7.13 pointed Claude at
|
|
62
69
|
// the wrong project because the matcher grepped the prompt itself).
|
|
63
70
|
const [showVibeInput, setShowVibeInput] = (0, react_1.useState)(false);
|
|
71
|
+
const [showDeploy, setShowDeploy] = (0, react_1.useState)(false);
|
|
64
72
|
const [vibePrompt, setVibePrompt] = (0, react_1.useState)('');
|
|
65
73
|
const [lastVibeTaskId, setLastVibeTaskId] = (0, react_1.useState)(null);
|
|
66
74
|
const [quickIconColorPreset, setQuickIconColorPreset] = (0, react_1.useState)(null);
|
|
@@ -376,7 +384,7 @@ const FeedbackModal = () => {
|
|
|
376
384
|
return;
|
|
377
385
|
}
|
|
378
386
|
try {
|
|
379
|
-
const uploaded = await (0, upload_1.uploadFeedback)(config.agentUrl, config.authToken ?? '', bundle);
|
|
387
|
+
const uploaded = await (0, upload_1.uploadFeedback)(config.agentUrl, config.authToken ?? '', bundle, YaverFeedback_1.YaverFeedback.getRelayPassword());
|
|
380
388
|
// The agent returns the new report id as `id` (see
|
|
381
389
|
// feedback_http.go::ReceiveFeedback). Trigger the fix loop if we got
|
|
382
390
|
// one back; otherwise just ack the upload.
|
|
@@ -488,6 +496,13 @@ const FeedbackModal = () => {
|
|
|
488
496
|
setShowVibeInput(false);
|
|
489
497
|
}
|
|
490
498
|
}, [showVibeInput, vibePrompt]);
|
|
499
|
+
// Hold the active vibe-chat session — set when handleVibingSubmit
|
|
500
|
+
// returns a fresh taskId. Renders <VibeChatScreen> which streams the
|
|
501
|
+
// SSE transcript, supports multi-turn follow-ups via /tasks/{id}/
|
|
502
|
+
// resume, and exposes a Reload button. Mirrors the in-Yaver native
|
|
503
|
+
// pane's transcript-mode behaviour, just rendered in RN here.
|
|
504
|
+
const [activeVibe, setActiveVibe] = (0, react_1.useState)(null);
|
|
505
|
+
const [includeScreenshot, setIncludeScreenshot] = (0, react_1.useState)(true);
|
|
491
506
|
const handleVibingSubmit = (0, react_1.useCallback)(async () => {
|
|
492
507
|
const client = YaverFeedback_1.YaverFeedback.getP2PClient();
|
|
493
508
|
if (!client) {
|
|
@@ -506,13 +521,43 @@ const FeedbackModal = () => {
|
|
|
506
521
|
.join('\n')
|
|
507
522
|
: '';
|
|
508
523
|
const userPrompt = vibePrompt.trim();
|
|
509
|
-
const
|
|
524
|
+
const promptText = userPrompt
|
|
510
525
|
? userPrompt + errNote
|
|
511
526
|
: 'Pick the next small improvement or fix for this app based on recent activity and the current screen.' +
|
|
512
527
|
errNote;
|
|
513
|
-
|
|
528
|
+
// Optional screenshot — captured from the host app's window.
|
|
529
|
+
// captureScreenshotBase64 returns null when react-native-view-
|
|
530
|
+
// shot isn't installed; we skip the screenshot rather than
|
|
531
|
+
// abort the whole feedback in that case.
|
|
532
|
+
let screenshotBase64;
|
|
533
|
+
if (includeScreenshot) {
|
|
534
|
+
const cap = await Promise.resolve().then(() => __importStar(require('./capture')));
|
|
535
|
+
const captured = await cap.captureScreenshotBase64();
|
|
536
|
+
if (captured?.base64) {
|
|
537
|
+
screenshotBase64 = captured.base64;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
// Resolve project context the same way reloadApp / vibing did.
|
|
541
|
+
const { resolveAppIdentity } = await Promise.resolve().then(() => __importStar(require('./P2PClient')));
|
|
542
|
+
const identity = resolveAppIdentity();
|
|
543
|
+
// Pull the user's preferred runner / model from local prefs.
|
|
544
|
+
// Both are optional — the agent falls back to whatever runner
|
|
545
|
+
// is signed in if neither is provided.
|
|
546
|
+
const prefs = await Promise.resolve().then(() => __importStar(require('./preferences')));
|
|
547
|
+
const preferredRunner = (await prefs.getPreferredRunner?.()) ?? null;
|
|
548
|
+
const preferredModel = (await prefs.getPreferredModel?.()) ?? null;
|
|
549
|
+
const result = await client.createFeedbackTask({
|
|
550
|
+
userPrompt: promptText,
|
|
551
|
+
projectName: identity.projectName,
|
|
552
|
+
projectPath: identity.projectPath,
|
|
553
|
+
runner: preferredRunner ?? undefined,
|
|
554
|
+
model: preferredModel ?? undefined,
|
|
555
|
+
screenshotBase64,
|
|
556
|
+
});
|
|
514
557
|
setLastVibeTaskId(result.taskId);
|
|
515
|
-
|
|
558
|
+
// Hand off to VibeChatScreen — it streams the SSE transcript,
|
|
559
|
+
// accepts follow-ups, and surfaces a Reload button.
|
|
560
|
+
setActiveVibe({ taskId: result.taskId, initialPrompt: promptText });
|
|
516
561
|
setVibePrompt('');
|
|
517
562
|
setShowVibeInput(false);
|
|
518
563
|
}
|
|
@@ -523,20 +568,54 @@ const FeedbackModal = () => {
|
|
|
523
568
|
if (mountedRef.current)
|
|
524
569
|
setAction('idle');
|
|
525
570
|
}
|
|
526
|
-
}, [vibePrompt]);
|
|
571
|
+
}, [vibePrompt, includeScreenshot]);
|
|
527
572
|
/*
|
|
528
573
|
const handleScreenRecording = useCallback(async () => {
|
|
529
574
|
...
|
|
530
575
|
}, [closeSoon, isRecordingVideo, lastVideo]);
|
|
531
576
|
*/
|
|
532
577
|
const busy = action !== 'idle';
|
|
578
|
+
// Once the user fires off a vibe task, swap the entire modal body
|
|
579
|
+
// for the live chat screen. The chat manages its own SSE
|
|
580
|
+
// subscription, multi-turn follow-ups, and Reload button. Closing
|
|
581
|
+
// the chat returns to idle and clears the active vibe.
|
|
582
|
+
if (visible && activeVibe) {
|
|
583
|
+
const client = YaverFeedback_1.YaverFeedback.getP2PClient();
|
|
584
|
+
return (<>
|
|
585
|
+
<AuthOverlay_1.AuthOverlay />
|
|
586
|
+
<QuickActionIcon_1.QuickActionIcon />
|
|
587
|
+
<react_native_1.Modal visible={visible} animationType="slide" transparent onRequestClose={() => setActiveVibe(null)}>
|
|
588
|
+
{client ? (<VibeChatScreen_1.VibeChatScreen client={client} initialTaskId={activeVibe.taskId} initialUserPrompt={activeVibe.initialPrompt} onClose={() => setActiveVibe(null)} onReload={async () => {
|
|
589
|
+
const c = YaverFeedback_1.YaverFeedback.getP2PClient();
|
|
590
|
+
if (!c)
|
|
591
|
+
throw new Error('Not connected');
|
|
592
|
+
await c.reloadApp();
|
|
593
|
+
}}/>) : null}
|
|
594
|
+
</react_native_1.Modal>
|
|
595
|
+
</>);
|
|
596
|
+
}
|
|
533
597
|
return (<>
|
|
534
598
|
<AuthOverlay_1.AuthOverlay />
|
|
535
599
|
<QuickActionIcon_1.QuickActionIcon />
|
|
536
600
|
{visible && (<react_native_1.Modal visible={visible} animationType="slide" transparent onRequestClose={handleClose}>
|
|
537
601
|
<react_native_1.Pressable style={styles.overlay} onPress={handleClose}>
|
|
538
602
|
<react_native_1.KeyboardAvoidingView behavior={react_native_1.Platform.OS === 'ios' ? 'padding' : 'height'} keyboardVerticalOffset={react_native_1.Platform.OS === 'ios' ? 12 : 0} style={styles.kbAvoider} pointerEvents="box-none">
|
|
539
|
-
<react_native_1.Pressable
|
|
603
|
+
<react_native_1.Pressable
|
|
604
|
+
// Tablet: cap modal width and center as a card-style
|
|
605
|
+
// sheet rather than a phone bottom sheet that stretches
|
|
606
|
+
// across a 12.9" iPad. Phone behaviour unchanged.
|
|
607
|
+
style={[
|
|
608
|
+
styles.modal,
|
|
609
|
+
isTablet
|
|
610
|
+
? {
|
|
611
|
+
width: '100%',
|
|
612
|
+
maxWidth: 640,
|
|
613
|
+
alignSelf: 'center',
|
|
614
|
+
borderTopLeftRadius: 22,
|
|
615
|
+
borderTopRightRadius: 22,
|
|
616
|
+
}
|
|
617
|
+
: null,
|
|
618
|
+
]} onPress={(e) => {
|
|
540
619
|
e.stopPropagation();
|
|
541
620
|
react_native_1.Keyboard.dismiss();
|
|
542
621
|
}}>
|
|
@@ -613,6 +692,7 @@ const FeedbackModal = () => {
|
|
|
613
692
|
void YaverFeedback_1.YaverFeedback.setQuickIconColorPreset(preset);
|
|
614
693
|
}} style={[
|
|
615
694
|
styles.iconOption,
|
|
695
|
+
iconOptionWidthOverride ? { width: iconOptionWidthOverride } : null,
|
|
616
696
|
selected && styles.iconOptionSelected,
|
|
617
697
|
]}>
|
|
618
698
|
<react_native_1.View style={[
|
|
@@ -667,6 +747,14 @@ const FeedbackModal = () => {
|
|
|
667
747
|
{/* Screenshot & Fix */}
|
|
668
748
|
<ActionRow label={action === 'capturing' ? 'Working…' : 'Screenshot & Fix'} tint="#22c55e" onPress={handleScreenshotAndFix} disabled={busy} busy={action === 'capturing'}/>
|
|
669
749
|
|
|
750
|
+
{/* Deploy — opens an inline panel that talks to
|
|
751
|
+
/fleet/deploy-options on the agent and lets the user
|
|
752
|
+
pick TestFlight / Play / Both, then a machine to run
|
|
753
|
+
it on. Capabilities (e.g. "Linux can't TestFlight")
|
|
754
|
+
come from the agent's doctor probes — no client-side
|
|
755
|
+
platform smarts here. */}
|
|
756
|
+
{!showDeploy ? (<ActionRow label="Deploy" tint="#7f8cf7" onPress={() => setShowDeploy(true)} disabled={busy}/>) : (<DeployPanel_1.DeployPanel onClose={() => setShowDeploy(false)}/>)}
|
|
757
|
+
|
|
670
758
|
{/* Remote sign-in buttons — trigger codex/claude device-auth
|
|
671
759
|
on the selected agent without leaving the app. Opens a
|
|
672
760
|
small native modal showing the verification URL + 8-char
|