yaver-feedback-react-native 0.8.12 → 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 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) {
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ interface DeployPanelProps {
3
+ /** Called when the user taps Cancel or after a successful deploy starts. */
4
+ onClose: () => void;
5
+ }
6
+ export declare const DeployPanel: React.FC<DeployPanelProps>;
7
+ export {};
@@ -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
+ });
@@ -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);
@@ -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 prompt = userPrompt
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
- const result = await client.vibing(prompt);
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
- setToast(`Vibing task ${result.taskId.slice(0, 8)} created`);
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 style={styles.modal} onPress={(e) => {
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
@@ -41,6 +41,14 @@ const FixReport_1 = require("./FixReport");
41
41
  const BlackBox_1 = require("./BlackBox");
42
42
  const DEFAULT_SIZE = 40;
43
43
  const DEFAULT_COLOR = '#6366f1';
44
+ // Tablet detection — short-edge dp >= 600 means iPad / 7"+ Android
45
+ // tablet / Z Fold open. The SDK has no app-side responsive context
46
+ // to lean on (it's a guest in third-party apps), so we infer
47
+ // locally and bump the button + panel to tablet sizes.
48
+ const TABLET_SHORT_EDGE = 600;
49
+ function isTabletWindow(width, height) {
50
+ return Math.min(width, height) >= TABLET_SHORT_EDGE;
51
+ }
44
52
  const DEFAULT_PANEL_BG = '#2d2d2d';
45
53
  /**
46
54
  * Draggable debug console button for the Yaver Feedback SDK.
@@ -71,7 +79,16 @@ const DEFAULT_PANEL_BG = '#2d2d2d';
71
79
  * - **"quit"** → disable the SDK
72
80
  */
73
81
  const FloatingButton = ({ onPress, initialPosition, size = DEFAULT_SIZE, color = DEFAULT_COLOR, showStatusDot = true, style: stylePreset = 'terminal', icon, agentUrl: agentUrlProp, authToken: authTokenProp, healthCheckInterval = 5000, panelBackgroundColor, }) => {
74
- const { width: screenWidth } = react_native_1.Dimensions.get('window');
82
+ // Read window size live so the SDK overlay re-pins itself when
83
+ // the host app rotates or splits. The legacy snapshot via
84
+ // Dimensions.get only ran once and parked the button off-screen
85
+ // after orientation changes on iPad.
86
+ const { width: screenWidth, height: screenHeight } = (0, react_native_1.useWindowDimensions)();
87
+ const isTablet = isTabletWindow(screenWidth, screenHeight);
88
+ // Tablets get a larger touch target and a wider panel — phones
89
+ // keep the existing 40 / 280 defaults so guest apps aren't
90
+ // disrupted on small screens.
91
+ const effectiveSize = isTablet ? Math.max(size, 52) : size;
75
92
  const defaultX = initialPosition?.x ?? 10;
76
93
  const defaultY = initialPosition?.y ?? 90;
77
94
  const pan = (0, react_1.useRef)(new react_native_1.Animated.ValueXY({ x: defaultX, y: defaultY })).current;
@@ -516,7 +533,12 @@ const FloatingButton = ({ onPress, initialPosition, size = DEFAULT_SIZE, color =
516
533
  const isTerminal = stylePreset === 'terminal';
517
534
  const buttonIcon = icon ?? 'y';
518
535
  const btnBg = isConnected ? color : `${color}88`;
519
- const panelWidth = fullSize ? screenWidth - 24 : 280;
536
+ // Panel sizing tablets get a wider compact panel (420) and a
537
+ // capped full-size panel (max 720 instead of full window) so the
538
+ // overlay doesn't dwarf the host app on a 12.9" iPad.
539
+ const compactPanelWidth = isTablet ? 420 : 280;
540
+ const fullPanelWidth = isTablet ? Math.min(screenWidth - 24, 720) : screenWidth - 24;
541
+ const panelWidth = fullSize ? fullPanelWidth : compactPanelWidth;
520
542
  return (<react_native_1.Animated.View style={[s.root, { transform: [{ translateX: pan.x }, { translateY: pan.y }] }]} {...panResponder.panHandlers}>
521
543
  {/* Console panel */}
522
544
  {chatOpen && (<react_native_1.View style={[
@@ -642,7 +664,7 @@ const FloatingButton = ({ onPress, initialPosition, size = DEFAULT_SIZE, color =
642
664
  <react_native_1.TouchableOpacity style={[
643
665
  s.button,
644
666
  isTerminal ? s.buttonTerminal : s.buttonMinimal,
645
- { backgroundColor: btnBg, width: size, height: size },
667
+ { backgroundColor: btnBg, width: effectiveSize, height: effectiveSize },
646
668
  !isTerminal && { borderRadius: size / 2 },
647
669
  ]} activeOpacity={0.7} onPress={handleTap}>
648
670
  <react_native_1.Text style={[s.buttonIcon, isTerminal && s.mono, { fontSize: 22 }]}>