yaver-feedback-react-native 0.7.16 → 0.8.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.
@@ -7,6 +7,7 @@ const BlackBox_1 = require("./BlackBox");
7
7
  const P2PClient_1 = require("./P2PClient");
8
8
  const ShakeDetector_1 = require("./ShakeDetector");
9
9
  const auth_1 = require("./auth");
10
+ const preferences_1 = require("./preferences");
10
11
  // Is this JS runtime the Yaver mobile app's super-host bridge? The
11
12
  // YaverInfo native module is only registered inside Yaver's container
12
13
  // (mobile/ios/Yaver/YaverInfo.{swift,m} + Android counterpart); a
@@ -38,6 +39,13 @@ let errorBuffer = [];
38
39
  let maxErrors = 5;
39
40
  /** Track whether BlackBox was running before disable (to restart on enable). */
40
41
  let blackBoxWasStreaming = false;
42
+ /**
43
+ * Tracks whether the user has already shaken once in this process.
44
+ * Consumed by QuickActionIcon's `'after-shake'` mode so the icon
45
+ * appears the first time a user discovers shake and stays around
46
+ * thereafter.
47
+ */
48
+ let firstShakeFired = false;
41
49
  /**
42
50
  * Flag evaluation cache — 30s TTL per `userId|key`. Prevents a
43
51
  * tight render loop from hammering /flags/eval when the dev calls
@@ -118,6 +126,7 @@ class YaverFeedback {
118
126
  if (enabled && config.trigger === 'shake') {
119
127
  shakeDetector = new ShakeDetector_1.ShakeDetector();
120
128
  shakeDetector.start(() => {
129
+ YaverFeedback.notifyShake();
121
130
  if (config?.reportingOnly) {
122
131
  YaverFeedback.sendAutoReport();
123
132
  }
@@ -450,6 +459,7 @@ class YaverFeedback {
450
459
  if (config?.trigger === 'shake' && !shakeDetector) {
451
460
  shakeDetector = new ShakeDetector_1.ShakeDetector();
452
461
  shakeDetector.start(() => {
462
+ YaverFeedback.notifyShake();
453
463
  if (config?.reportingOnly) {
454
464
  YaverFeedback.sendAutoReport();
455
465
  }
@@ -752,6 +762,56 @@ class YaverFeedback {
752
762
  // Not in dev mode
753
763
  }
754
764
  }
765
+ /**
766
+ * Internal: fired from every shake path (dev-menu + accelerometer)
767
+ * before the feedback modal opens. Emits `yaverFeedback:firstShake`
768
+ * exactly once per process so QuickActionIcon's `'after-shake'` mode
769
+ * can surface itself on first shake and stay visible for the rest of
770
+ * the session.
771
+ */
772
+ static notifyShake() {
773
+ if (firstShakeFired)
774
+ return;
775
+ firstShakeFired = true;
776
+ try {
777
+ const { DeviceEventEmitter } = require('react-native');
778
+ DeviceEventEmitter.emit('yaverFeedback:firstShake');
779
+ }
780
+ catch {
781
+ // emitter unavailable (e.g. jsdom unit test) — safe to ignore
782
+ }
783
+ }
784
+ /**
785
+ * Show / hide the QuickActionIcon programmatically and persist the
786
+ * choice across launches. Host apps can call this from a settings
787
+ * screen so the user has a second way to re-enable the icon after
788
+ * hiding it via the icon's own long-press menu — shake is always the
789
+ * third back-door because it never depends on a visible control.
790
+ */
791
+ static async setQuickIconVisible(visible) {
792
+ await (0, preferences_1.setQuickIconDisabled)(!visible);
793
+ try {
794
+ const { DeviceEventEmitter } = require('react-native');
795
+ DeviceEventEmitter.emit(visible ? 'yaverFeedback:quickIconShow' : 'yaverFeedback:quickIconHide');
796
+ }
797
+ catch {
798
+ // emitter unavailable — preference is still persisted
799
+ }
800
+ }
801
+ /**
802
+ * Returns `true` when the user has chosen to hide the QuickActionIcon
803
+ * (via its long-press menu or `setQuickIconVisible(false)`).
804
+ * FeedbackModal uses this to surface a one-tap "Show quick icon"
805
+ * control so the user can bring the icon back without having to know
806
+ * about the programmatic API.
807
+ */
808
+ static async isQuickIconHidden() {
809
+ return (0, preferences_1.getQuickIconDisabled)();
810
+ }
811
+ /** Clear the persisted "user hid the icon" flag. */
812
+ static async resetQuickIconPreference() {
813
+ await YaverFeedback.setQuickIconVisible(true);
814
+ }
755
815
  /** Tear down the SDK (stop shake detector, clear state). */
756
816
  static destroy() {
757
817
  if (shakeDetector) {
package/dist/index.d.ts CHANGED
@@ -44,7 +44,10 @@ export { AuthOverlay } from './AuthOverlay';
44
44
  export { ShakeDetector } from './ShakeDetector';
45
45
  export { FloatingButton } from './FloatingButton';
46
46
  export { FeedbackModal } from './FeedbackModal';
47
+ export { QuickActionIcon } from './QuickActionIcon';
48
+ export type { QuickActionIconProps } from './QuickActionIcon';
47
49
  export { FixReport } from './FixReport';
50
+ export { getQuickIconDisabled, setQuickIconDisabled, clearQuickIconDisabled, } from './preferences';
48
51
  export { configureAuthEndpoints, getConvexSiteUrl, getWebBaseUrl, getToken, saveToken, clearToken, getUser, saveUser, getSelectedDeviceId, saveSelectedDeviceId, clearSelectedDeviceId, validateToken, signInWithApple, signInWithOAuth, signupWithEmail, loginWithEmail, listReachableDevices, DEFAULT_CONVEX_SITE_URL, DEFAULT_WEB_BASE_URL, DEFAULT_OAUTH_REDIRECT, } from './auth';
49
52
  export type { OAuthProvider, User, RemoteDevice, DeviceList, } from './auth';
50
53
  export { captureScreenshot, startVideoRecording, stopVideoRecording, isVideoRecording, } from './capture';
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@
28
28
  * ```
29
29
  */
30
30
  Object.defineProperty(exports, "__esModule", { value: true });
31
- exports.uploadFeedback = exports.isVideoRecording = exports.stopVideoRecording = exports.startVideoRecording = exports.captureScreenshot = exports.DEFAULT_OAUTH_REDIRECT = exports.DEFAULT_WEB_BASE_URL = exports.DEFAULT_CONVEX_SITE_URL = exports.listReachableDevices = exports.loginWithEmail = exports.signupWithEmail = exports.signInWithOAuth = exports.signInWithApple = exports.validateToken = exports.clearSelectedDeviceId = exports.saveSelectedDeviceId = exports.getSelectedDeviceId = exports.saveUser = exports.getUser = exports.clearToken = exports.saveToken = exports.getToken = exports.getWebBaseUrl = exports.getConvexSiteUrl = exports.configureAuthEndpoints = exports.FixReport = exports.FeedbackModal = exports.FloatingButton = exports.ShakeDetector = exports.AuthOverlay = exports.PairDeviceModal = exports.YaverMachinePickerScreen = exports.YaverLoginScreen = exports.YaverConnectionScreen = exports.P2PClient = exports.YaverDiscovery = exports.initExpo = exports.YaverUpdates = exports.BlackBox = exports.YaverFeedback = void 0;
31
+ exports.uploadFeedback = exports.isVideoRecording = exports.stopVideoRecording = exports.startVideoRecording = exports.captureScreenshot = exports.DEFAULT_OAUTH_REDIRECT = exports.DEFAULT_WEB_BASE_URL = exports.DEFAULT_CONVEX_SITE_URL = exports.listReachableDevices = exports.loginWithEmail = exports.signupWithEmail = exports.signInWithOAuth = exports.signInWithApple = exports.validateToken = exports.clearSelectedDeviceId = exports.saveSelectedDeviceId = exports.getSelectedDeviceId = exports.saveUser = exports.getUser = exports.clearToken = exports.saveToken = exports.getToken = exports.getWebBaseUrl = exports.getConvexSiteUrl = exports.configureAuthEndpoints = exports.clearQuickIconDisabled = exports.setQuickIconDisabled = exports.getQuickIconDisabled = exports.FixReport = exports.QuickActionIcon = exports.FeedbackModal = exports.FloatingButton = exports.ShakeDetector = exports.AuthOverlay = exports.PairDeviceModal = exports.YaverMachinePickerScreen = exports.YaverLoginScreen = exports.YaverConnectionScreen = exports.P2PClient = exports.YaverDiscovery = exports.initExpo = exports.YaverUpdates = exports.BlackBox = exports.YaverFeedback = void 0;
32
32
  var YaverFeedback_1 = require("./YaverFeedback");
33
33
  Object.defineProperty(exports, "YaverFeedback", { enumerable: true, get: function () { return YaverFeedback_1.YaverFeedback; } });
34
34
  var BlackBox_1 = require("./BlackBox");
@@ -57,8 +57,14 @@ var FloatingButton_1 = require("./FloatingButton");
57
57
  Object.defineProperty(exports, "FloatingButton", { enumerable: true, get: function () { return FloatingButton_1.FloatingButton; } });
58
58
  var FeedbackModal_1 = require("./FeedbackModal");
59
59
  Object.defineProperty(exports, "FeedbackModal", { enumerable: true, get: function () { return FeedbackModal_1.FeedbackModal; } });
60
+ var QuickActionIcon_1 = require("./QuickActionIcon");
61
+ Object.defineProperty(exports, "QuickActionIcon", { enumerable: true, get: function () { return QuickActionIcon_1.QuickActionIcon; } });
60
62
  var FixReport_1 = require("./FixReport");
61
63
  Object.defineProperty(exports, "FixReport", { enumerable: true, get: function () { return FixReport_1.FixReport; } });
64
+ var preferences_1 = require("./preferences");
65
+ Object.defineProperty(exports, "getQuickIconDisabled", { enumerable: true, get: function () { return preferences_1.getQuickIconDisabled; } });
66
+ Object.defineProperty(exports, "setQuickIconDisabled", { enumerable: true, get: function () { return preferences_1.setQuickIconDisabled; } });
67
+ Object.defineProperty(exports, "clearQuickIconDisabled", { enumerable: true, get: function () { return preferences_1.clearQuickIconDisabled; } });
62
68
  var auth_1 = require("./auth");
63
69
  Object.defineProperty(exports, "configureAuthEndpoints", { enumerable: true, get: function () { return auth_1.configureAuthEndpoints; } });
64
70
  Object.defineProperty(exports, "getConvexSiteUrl", { enumerable: true, get: function () { return auth_1.getConvexSiteUrl; } });
@@ -0,0 +1,18 @@
1
+ /**
2
+ * SDK user preferences persisted across launches.
3
+ *
4
+ * Currently only the quick-action icon's user-level dismiss flag
5
+ * lives here: the dev enables the icon via `FeedbackConfig.quickIcon`,
6
+ * but the *user* can long-press → Hide to opt out, and we remember
7
+ * that choice across launches so their next app session still
8
+ * respects it.
9
+ *
10
+ * AsyncStorage is an optional peer dep — if it's not installed the
11
+ * getters return `false` and the setters silently no-op, so the icon
12
+ * still works (it just can't remember the disable beyond the
13
+ * in-memory session).
14
+ */
15
+ /** True if the user has long-pressed the icon and chosen "Hide". */
16
+ export declare function getQuickIconDisabled(): Promise<boolean>;
17
+ export declare function setQuickIconDisabled(disabled: boolean): Promise<void>;
18
+ export declare function clearQuickIconDisabled(): Promise<void>;
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ /**
3
+ * SDK user preferences persisted across launches.
4
+ *
5
+ * Currently only the quick-action icon's user-level dismiss flag
6
+ * lives here: the dev enables the icon via `FeedbackConfig.quickIcon`,
7
+ * but the *user* can long-press → Hide to opt out, and we remember
8
+ * that choice across launches so their next app session still
9
+ * respects it.
10
+ *
11
+ * AsyncStorage is an optional peer dep — if it's not installed the
12
+ * getters return `false` and the setters silently no-op, so the icon
13
+ * still works (it just can't remember the disable beyond the
14
+ * in-memory session).
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.getQuickIconDisabled = getQuickIconDisabled;
18
+ exports.setQuickIconDisabled = setQuickIconDisabled;
19
+ exports.clearQuickIconDisabled = clearQuickIconDisabled;
20
+ let AsyncStorage = null;
21
+ try {
22
+ AsyncStorage = require('@react-native-async-storage/async-storage').default;
23
+ }
24
+ catch {
25
+ // not installed — degrade gracefully
26
+ }
27
+ const QUICK_ICON_DISABLED_KEY = 'yaver_feedback_quickicon_disabled';
28
+ /** True if the user has long-pressed the icon and chosen "Hide". */
29
+ async function getQuickIconDisabled() {
30
+ if (!AsyncStorage)
31
+ return false;
32
+ try {
33
+ const v = await AsyncStorage.getItem(QUICK_ICON_DISABLED_KEY);
34
+ return v === '1';
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ }
40
+ async function setQuickIconDisabled(disabled) {
41
+ if (!AsyncStorage)
42
+ return;
43
+ try {
44
+ if (disabled) {
45
+ await AsyncStorage.setItem(QUICK_ICON_DISABLED_KEY, '1');
46
+ }
47
+ else {
48
+ await AsyncStorage.removeItem(QUICK_ICON_DISABLED_KEY);
49
+ }
50
+ }
51
+ catch {
52
+ // best-effort
53
+ }
54
+ }
55
+ async function clearQuickIconDisabled() {
56
+ await setQuickIconDisabled(false);
57
+ }
package/dist/types.d.ts CHANGED
@@ -42,6 +42,37 @@ export interface FeedbackConfig {
42
42
  preferredDeviceId?: string;
43
43
  /** How feedback collection is triggered */
44
44
  trigger?: 'shake' | 'floating-button' | 'manual';
45
+ /**
46
+ * Small tap-to-open icon that floats above the app so the user
47
+ * doesn't have to shake every time they want to open feedback.
48
+ * Single tap → open the feedback modal; long-press → menu with
49
+ * "Hide icon" (persisted across launches via AsyncStorage).
50
+ *
51
+ * - `'auto'` (default) → `'always'` on iOS/Android, `'off'` on web.
52
+ * - `'always'` → visible from app launch.
53
+ * - `'after-shake'` → hidden until the first shake this session.
54
+ * - `'off'` → never rendered. Shake still works.
55
+ *
56
+ * The user can always override `'always'` / `'auto'` by long-pressing
57
+ * → Hide. Devs can clear that override via
58
+ * `YaverFeedback.resetQuickIconPreference()`.
59
+ */
60
+ quickIcon?: 'auto' | 'always' | 'after-shake' | 'off';
61
+ /**
62
+ * Background color for the quick-action icon. Default: '#6366f1'
63
+ * (indigo). Pick something distinctive so the icon never visually
64
+ * collides with your own FAB.
65
+ */
66
+ quickIconColor?: string;
67
+ /**
68
+ * Initial position of the quick-action icon, in pixels from the
69
+ * top-left. Default: near the top-right corner of the screen. The
70
+ * icon is draggable at runtime — this is only the first mount.
71
+ */
72
+ quickIconInitialPosition?: {
73
+ x: number;
74
+ y: number;
75
+ };
45
76
  /** Enable/disable the SDK. Defaults to __DEV__ */
46
77
  enabled?: boolean;
47
78
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yaver-feedback-react-native",
3
- "version": "0.7.16",
3
+ "version": "0.8.0",
4
4
  "description": "Visual feedback SDK for Yaver — bug reports, screen recording, voice annotations, and local-first developer workflows",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -7,6 +7,7 @@ import {
7
7
  Pressable,
8
8
  StyleSheet,
9
9
  Text,
10
+ TextInput,
10
11
  View,
11
12
  } from 'react-native';
12
13
  import { YaverFeedback } from './YaverFeedback';
@@ -21,6 +22,7 @@ import {
21
22
  import { uploadFeedback } from './upload';
22
23
  import { DeviceInfo, FeedbackBundle } from './types';
23
24
  import { AuthOverlay } from './AuthOverlay';
25
+ import { QuickActionIcon } from './QuickActionIcon';
24
26
 
25
27
  /**
26
28
  * Simplified feedback modal — 5 actions:
@@ -65,6 +67,19 @@ export const FeedbackModal: React.FC = () => {
65
67
  // hidden button instead of a runtime error.
66
68
  const voiceSupported = useRef<boolean>(isVoiceCaptureSupported()).current;
67
69
  const [lastVideo, setLastVideo] = useState<LastVideo | null>(null);
70
+ // Tracks whether the user has hidden the QuickActionIcon via its
71
+ // long-press menu. Shake is always available, so the feedback modal
72
+ // is our guaranteed UI for bringing the icon back — we surface a
73
+ // small "Show quick icon" row when this is true.
74
+ const [quickIconHidden, setQuickIconHidden] = useState(false);
75
+ // Vibing-input mode: same expand-on-tap pattern as email login.
76
+ // Tap "Vibing" once → the button reveals an input + Send; that lets
77
+ // the user say WHAT they want to vibe on instead of firing a canned
78
+ // "pick something for me" prompt (which in 0.7.13 pointed Claude at
79
+ // the wrong project because the matcher grepped the prompt itself).
80
+ const [showVibeInput, setShowVibeInput] = useState(false);
81
+ const [vibePrompt, setVibePrompt] = useState('');
82
+ const [lastVibeTaskId, setLastVibeTaskId] = useState<string | null>(null);
68
83
  const mountedRef = useRef(true);
69
84
 
70
85
  useEffect(() => {
@@ -75,6 +90,14 @@ export const FeedbackModal: React.FC = () => {
75
90
  setError(null);
76
91
  setToast(null);
77
92
  setAction('idle');
93
+ // Re-read the "user hid the quick icon" flag on every open so
94
+ // the re-enable row reflects the latest preference (the user
95
+ // might have hidden or shown it between opens).
96
+ YaverFeedback.isQuickIconHidden()
97
+ .then((v) => {
98
+ if (mountedRef.current) setQuickIconHidden(v);
99
+ })
100
+ .catch(() => {});
78
101
  }
79
102
  });
80
103
  // Agent streams build / compile progress through the BlackBox
@@ -285,7 +308,23 @@ export const FeedbackModal: React.FC = () => {
285
308
  }, [closeSoon]);
286
309
 
287
310
  // ─── 3. Vibing ─────────────────────────────────────────────────────
288
- const handleVibing = useCallback(async () => {
311
+ // First tap expands the input; second submit fires the actual
312
+ // /vibing/execute. Mirrors the Yaver mobile app's Vibing tab —
313
+ // user types what they want, hits Send, sees the task id back. If
314
+ // left blank, we default to "pick the next small improvement"
315
+ // so a one-tap workflow still works for lazy days.
316
+ const handleVibingButton = useCallback(() => {
317
+ if (!showVibeInput) {
318
+ setShowVibeInput(true);
319
+ return;
320
+ }
321
+ // collapse if tapped again with empty input
322
+ if (!vibePrompt.trim()) {
323
+ setShowVibeInput(false);
324
+ }
325
+ }, [showVibeInput, vibePrompt]);
326
+
327
+ const handleVibingSubmit = useCallback(async () => {
289
328
  const client = YaverFeedback.getP2PClient();
290
329
  if (!client) {
291
330
  setError('Not connected to the agent yet.');
@@ -303,21 +342,22 @@ export const FeedbackModal: React.FC = () => {
303
342
  .map((e) => `- ${e.message}`)
304
343
  .join('\n')
305
344
  : '';
306
- const prompt =
307
- 'The user opened the feedback modal on their phone and tapped Vibing. ' +
308
- 'Investigate whatever they are likely to be asking about — pick the ' +
309
- 'next small improvement or fix based on recent activity and the ' +
310
- 'current screen.' +
311
- errNote;
312
- await client.vibing(prompt);
313
- setToast('Vibing task created');
314
- closeSoon(1200);
345
+ const userPrompt = vibePrompt.trim();
346
+ const prompt = userPrompt
347
+ ? userPrompt + errNote
348
+ : 'Pick the next small improvement or fix for this app based on recent activity and the current screen.' +
349
+ errNote;
350
+ const result = await client.vibing(prompt);
351
+ setLastVibeTaskId(result.taskId);
352
+ setToast(`Vibing task ${result.taskId.slice(0, 8)} created`);
353
+ setVibePrompt('');
354
+ setShowVibeInput(false);
315
355
  } catch (err: unknown) {
316
356
  setError(err instanceof Error ? err.message : String(err));
317
357
  } finally {
318
358
  if (mountedRef.current) setAction('idle');
319
359
  }
320
- }, [closeSoon]);
360
+ }, [vibePrompt]);
321
361
 
322
362
  // ─── 4. Toggle screen recording ────────────────────────────────────
323
363
  const handleToggleRecording = useCallback(async () => {
@@ -491,6 +531,7 @@ export const FeedbackModal: React.FC = () => {
491
531
  return (
492
532
  <>
493
533
  <AuthOverlay />
534
+ <QuickActionIcon />
494
535
  {visible && (
495
536
  <Modal
496
537
  visible={visible}
@@ -537,14 +578,64 @@ export const FeedbackModal: React.FC = () => {
537
578
  busy={action === 'capturing'}
538
579
  />
539
580
 
540
- {/* 3. Vibing */}
541
- <ActionRow
542
- label={action === 'vibing' ? 'Starting…' : 'Vibing'}
543
- tint="#818cf8"
544
- onPress={handleVibing}
545
- disabled={busy}
546
- busy={action === 'vibing'}
547
- />
581
+ {/* 3. Vibing — expands to an input box on first tap
582
+ so the user says WHAT they want to vibe on, just
583
+ like the Yaver mobile app's Vibing tab. Second
584
+ tap (Send) fires /vibing/execute with the typed
585
+ prompt + resolved bundle id so the agent routes
586
+ to the right repo. */}
587
+ {!showVibeInput ? (
588
+ <ActionRow
589
+ label={action === 'vibing' ? 'Starting…' : 'Vibing'}
590
+ tint="#818cf8"
591
+ onPress={handleVibingButton}
592
+ disabled={busy}
593
+ busy={action === 'vibing'}
594
+ />
595
+ ) : (
596
+ <View style={styles.vibeInputRow}>
597
+ <TextInput
598
+ style={styles.vibeInput}
599
+ placeholder="What do you want to vibe on?"
600
+ placeholderTextColor="#666"
601
+ value={vibePrompt}
602
+ onChangeText={setVibePrompt}
603
+ multiline
604
+ autoFocus
605
+ editable={action !== 'vibing'}
606
+ blurOnSubmit={false}
607
+ />
608
+ <View style={styles.vibeInputButtons}>
609
+ <Pressable
610
+ onPress={() => { setShowVibeInput(false); setVibePrompt(''); }}
611
+ style={({ pressed }) => [styles.vibeCancelBtn, pressed && styles.buttonPressed]}
612
+ disabled={action === 'vibing'}
613
+ >
614
+ <Text style={styles.vibeCancelBtnText}>Cancel</Text>
615
+ </Pressable>
616
+ <Pressable
617
+ onPress={handleVibingSubmit}
618
+ style={({ pressed }) => [
619
+ styles.vibeSendBtn,
620
+ pressed && styles.buttonPressed,
621
+ action === 'vibing' && { opacity: 0.6 },
622
+ ]}
623
+ disabled={action === 'vibing'}
624
+ >
625
+ {action === 'vibing' ? (
626
+ <ActivityIndicator color="#fff" />
627
+ ) : (
628
+ <Text style={styles.vibeSendBtnText}>Send</Text>
629
+ )}
630
+ </Pressable>
631
+ </View>
632
+ </View>
633
+ )}
634
+ {lastVibeTaskId && action !== 'vibing' && (
635
+ <Text style={styles.vibeTaskLine} numberOfLines={1}>
636
+ Last vibing task: {lastVibeTaskId.slice(0, 12)}…
637
+ </Text>
638
+ )}
548
639
 
549
640
  {/* Voice note — only rendered when expo-av is installed.
550
641
  Tap to start, tap again to stop → transcribes via
@@ -600,6 +691,34 @@ export const FeedbackModal: React.FC = () => {
600
691
  )}
601
692
  {toast && <Text style={styles.toast}>{toast}</Text>}
602
693
  {error && <Text style={styles.error}>{error}</Text>}
694
+
695
+ {/* Quick-icon toggle. The user's three ways to control
696
+ the floating icon are: (1) long-press the icon →
697
+ Hide, (2) tap this row to toggle it on/off, (3) shake
698
+ → this modal → tap this row. Shake is the unkillable
699
+ back-door when the icon is hidden and the dev hasn't
700
+ exposed their own settings UI. */}
701
+ <Pressable
702
+ onPress={async () => {
703
+ const next = !quickIconHidden;
704
+ setQuickIconHidden(next);
705
+ await YaverFeedback.setQuickIconVisible(!next);
706
+ }}
707
+ style={({ pressed }) => [
708
+ styles.quickIconToggle,
709
+ pressed && { opacity: 0.7 },
710
+ ]}
711
+ accessibilityRole="button"
712
+ accessibilityLabel={
713
+ quickIconHidden ? 'Show quick icon' : 'Hide quick icon'
714
+ }
715
+ >
716
+ <Text style={styles.quickIconToggleText}>
717
+ {quickIconHidden
718
+ ? '◯ Show quick-access icon'
719
+ : '● Hide quick-access icon'}
720
+ </Text>
721
+ </Pressable>
603
722
  </Pressable>
604
723
  </Pressable>
605
724
  </Modal>
@@ -647,6 +766,56 @@ const ActionRow: React.FC<ActionRowProps> = ({
647
766
  );
648
767
 
649
768
  const styles = StyleSheet.create({
769
+ vibeInputRow: {
770
+ backgroundColor: 'rgba(129,140,248,0.08)',
771
+ borderColor: 'rgba(129,140,248,0.4)',
772
+ borderWidth: 1,
773
+ borderRadius: 12,
774
+ padding: 12,
775
+ gap: 10,
776
+ },
777
+ vibeInput: {
778
+ color: '#fff',
779
+ fontSize: 15,
780
+ minHeight: 64,
781
+ textAlignVertical: 'top',
782
+ padding: 0,
783
+ },
784
+ vibeInputButtons: {
785
+ flexDirection: 'row',
786
+ justifyContent: 'flex-end',
787
+ gap: 10,
788
+ },
789
+ vibeCancelBtn: {
790
+ paddingHorizontal: 14,
791
+ paddingVertical: 8,
792
+ borderRadius: 8,
793
+ backgroundColor: 'transparent',
794
+ },
795
+ vibeCancelBtnText: {
796
+ color: '#999',
797
+ fontSize: 14,
798
+ fontWeight: '600',
799
+ },
800
+ vibeSendBtn: {
801
+ paddingHorizontal: 16,
802
+ paddingVertical: 8,
803
+ borderRadius: 8,
804
+ backgroundColor: '#818cf8',
805
+ minWidth: 72,
806
+ alignItems: 'center',
807
+ },
808
+ vibeSendBtnText: {
809
+ color: '#fff',
810
+ fontSize: 14,
811
+ fontWeight: '700',
812
+ },
813
+ vibeTaskLine: {
814
+ color: '#818cf8',
815
+ fontSize: 12,
816
+ marginTop: -4,
817
+ fontFamily: Platform.select({ ios: 'Menlo', android: 'monospace', default: 'monospace' }),
818
+ },
650
819
  overlay: {
651
820
  flex: 1,
652
821
  backgroundColor: 'rgba(0,0,0,0.55)',
@@ -723,4 +892,15 @@ const styles = StyleSheet.create({
723
892
  textAlign: 'center',
724
893
  marginTop: 4,
725
894
  },
895
+ quickIconToggle: {
896
+ marginTop: 4,
897
+ alignSelf: 'center',
898
+ paddingVertical: 6,
899
+ paddingHorizontal: 12,
900
+ },
901
+ quickIconToggleText: {
902
+ color: '#9ca3af',
903
+ fontSize: 12,
904
+ fontWeight: '500',
905
+ },
726
906
  });
package/src/P2PClient.ts CHANGED
@@ -376,14 +376,30 @@ export class P2PClient {
376
376
  * vibing from Claude Code / the Yaver mobile app; this method is a
377
377
  * convenience for the SDK's one-tap bug-report-to-vibing path.
378
378
  */
379
- async vibing(prompt: string, projectPath?: string): Promise<{ taskId: string }> {
379
+ async vibing(
380
+ prompt: string,
381
+ opts?: { projectName?: string; bundleId?: string; projectPath?: string },
382
+ ): Promise<{ taskId: string }> {
383
+ // Resolve app identity exactly the same way we do for
384
+ // reloadApp — bundle ID from expo-constants or native config.
385
+ // Without this, the agent falls back to "grep the prompt for a
386
+ // word that looks like a project name," which is catastrophically
387
+ // wrong: the prompt 'tapped Vibing' matched 'in' → picked mprint
388
+ // → Claude vibed on the wrong repo. Passing the bundle/name lets
389
+ // the agent go straight to findMobileProjectByName / bundleId.
390
+ const identity = resolveAppIdentity(opts);
380
391
  const response = await fetch(`${this.baseUrl}/vibing/execute`, {
381
392
  method: 'POST',
382
393
  headers: {
383
394
  Authorization: `Bearer ${this.authToken}`,
384
395
  'Content-Type': 'application/json',
385
396
  },
386
- body: JSON.stringify({ prompt, projectPath: projectPath ?? '' }),
397
+ body: JSON.stringify({
398
+ prompt,
399
+ projectPath: identity.projectPath ?? opts?.projectPath ?? '',
400
+ projectName: identity.projectName,
401
+ bundleId: identity.bundleId,
402
+ }),
387
403
  });
388
404
  if (!response.ok) {
389
405
  const text = await response.text().catch(() => '');