@mobileai/react-native 0.9.12 → 0.9.14

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.
Files changed (38) hide show
  1. package/README.md +480 -69
  2. package/lib/module/components/AIAgent.js +61 -3
  3. package/lib/module/components/AIAgent.js.map +1 -1
  4. package/lib/module/components/AgentChatBar.js +12 -2
  5. package/lib/module/components/AgentChatBar.js.map +1 -1
  6. package/lib/module/components/DiscoveryTooltip.js +128 -0
  7. package/lib/module/components/DiscoveryTooltip.js.map +1 -0
  8. package/lib/module/core/AgentRuntime.js +77 -1
  9. package/lib/module/core/AgentRuntime.js.map +1 -1
  10. package/lib/module/core/FiberTreeWalker.js +5 -1
  11. package/lib/module/core/FiberTreeWalker.js.map +1 -1
  12. package/lib/module/core/systemPrompt.js +41 -3
  13. package/lib/module/core/systemPrompt.js.map +1 -1
  14. package/lib/module/index.js.map +1 -1
  15. package/lib/typescript/src/components/AIAgent.d.ts +18 -2
  16. package/lib/typescript/src/components/AIAgent.d.ts.map +1 -1
  17. package/lib/typescript/src/components/AgentChatBar.d.ts +5 -1
  18. package/lib/typescript/src/components/AgentChatBar.d.ts.map +1 -1
  19. package/lib/typescript/src/components/DiscoveryTooltip.d.ts +15 -0
  20. package/lib/typescript/src/components/DiscoveryTooltip.d.ts.map +1 -0
  21. package/lib/typescript/src/core/AgentRuntime.d.ts +7 -0
  22. package/lib/typescript/src/core/AgentRuntime.d.ts.map +1 -1
  23. package/lib/typescript/src/core/FiberTreeWalker.d.ts.map +1 -1
  24. package/lib/typescript/src/core/systemPrompt.d.ts +1 -1
  25. package/lib/typescript/src/core/systemPrompt.d.ts.map +1 -1
  26. package/lib/typescript/src/core/types.d.ts +19 -0
  27. package/lib/typescript/src/core/types.d.ts.map +1 -1
  28. package/lib/typescript/src/index.d.ts +1 -1
  29. package/lib/typescript/src/index.d.ts.map +1 -1
  30. package/package.json +1 -1
  31. package/src/components/AIAgent.tsx +75 -1
  32. package/src/components/AgentChatBar.tsx +19 -1
  33. package/src/components/DiscoveryTooltip.tsx +148 -0
  34. package/src/core/AgentRuntime.ts +87 -1
  35. package/src/core/FiberTreeWalker.ts +5 -0
  36. package/src/core/systemPrompt.ts +41 -3
  37. package/src/core/types.ts +21 -0
  38. package/src/index.ts +1 -0
@@ -0,0 +1,148 @@
1
+ /**
2
+ * DiscoveryTooltip — One-time tooltip shown above the FAB on first use.
3
+ *
4
+ * Tells users the AI can navigate the app and do things for them.
5
+ * Shows once, then persists dismissal via AsyncStorage.
6
+ * Bilingual: EN/AR.
7
+ */
8
+
9
+ import { useEffect, useRef } from 'react';
10
+ import {
11
+ View,
12
+ Text,
13
+ Pressable,
14
+ StyleSheet,
15
+ Animated,
16
+ } from 'react-native';
17
+
18
+ interface DiscoveryTooltipProps {
19
+ language: 'en' | 'ar';
20
+ primaryColor?: string;
21
+ onDismiss: () => void;
22
+ }
23
+
24
+ const LABELS = {
25
+ en: '✨ I can help you navigate the app and do things for you!',
26
+ ar: '✨ أقدر أساعدك تتنقل في التطبيق وأعمل حاجات بدالك!',
27
+ };
28
+
29
+ const AUTO_DISMISS_MS = 6000;
30
+
31
+ export function DiscoveryTooltip({
32
+ language,
33
+ primaryColor,
34
+ onDismiss,
35
+ }: DiscoveryTooltipProps) {
36
+ const scaleAnim = useRef(new Animated.Value(0)).current;
37
+ const opacityAnim = useRef(new Animated.Value(0)).current;
38
+
39
+ useEffect(() => {
40
+ // Spring-in entry
41
+ Animated.parallel([
42
+ Animated.spring(scaleAnim, {
43
+ toValue: 1,
44
+ friction: 6,
45
+ tension: 80,
46
+ useNativeDriver: true,
47
+ }),
48
+ Animated.timing(opacityAnim, {
49
+ toValue: 1,
50
+ duration: 200,
51
+ useNativeDriver: true,
52
+ }),
53
+ ]).start();
54
+
55
+ // Auto-dismiss after timeout
56
+ const timer = setTimeout(() => {
57
+ dismissWithAnimation();
58
+ }, AUTO_DISMISS_MS);
59
+
60
+ return () => clearTimeout(timer);
61
+ // eslint-disable-next-line react-hooks/exhaustive-deps
62
+ }, []);
63
+
64
+ const dismissWithAnimation = () => {
65
+ Animated.parallel([
66
+ Animated.timing(scaleAnim, {
67
+ toValue: 0,
68
+ duration: 200,
69
+ useNativeDriver: true,
70
+ }),
71
+ Animated.timing(opacityAnim, {
72
+ toValue: 0,
73
+ duration: 200,
74
+ useNativeDriver: true,
75
+ }),
76
+ ]).start(() => onDismiss());
77
+ };
78
+
79
+ const isArabic = language === 'ar';
80
+ const bgColor = primaryColor || '#1a1a2e';
81
+
82
+ return (
83
+ <Animated.View
84
+ style={[
85
+ styles.container,
86
+ {
87
+ backgroundColor: bgColor,
88
+ transform: [{ scale: scaleAnim }],
89
+ opacity: opacityAnim,
90
+ },
91
+ ]}
92
+ >
93
+ <Pressable onPress={dismissWithAnimation} style={styles.contentArea}>
94
+ <Text style={[styles.text, isArabic && styles.textRTL]}>
95
+ {LABELS[language]}
96
+ </Text>
97
+ </Pressable>
98
+
99
+ {/* Triangle pointer toward FAB */}
100
+ <View style={[styles.pointer, { borderTopColor: bgColor }]} />
101
+ </Animated.View>
102
+ );
103
+ }
104
+
105
+ const styles = StyleSheet.create({
106
+ container: {
107
+ position: 'absolute',
108
+ bottom: 70,
109
+ right: -4,
110
+ minWidth: 200,
111
+ maxWidth: 260,
112
+ borderRadius: 16,
113
+ paddingHorizontal: 14,
114
+ paddingVertical: 10,
115
+ shadowColor: '#000',
116
+ shadowOffset: { width: 0, height: 4 },
117
+ shadowOpacity: 0.3,
118
+ shadowRadius: 8,
119
+ elevation: 6,
120
+ },
121
+ contentArea: {
122
+ flexDirection: 'row',
123
+ alignItems: 'center',
124
+ },
125
+ text: {
126
+ color: '#ffffff',
127
+ fontSize: 13,
128
+ lineHeight: 19,
129
+ fontWeight: '500',
130
+ },
131
+ textRTL: {
132
+ textAlign: 'right',
133
+ writingDirection: 'rtl',
134
+ },
135
+ pointer: {
136
+ position: 'absolute',
137
+ bottom: -8,
138
+ right: 22,
139
+ width: 0,
140
+ height: 0,
141
+ borderLeftWidth: 8,
142
+ borderRightWidth: 8,
143
+ borderTopWidth: 8,
144
+ borderLeftColor: 'transparent',
145
+ borderRightColor: 'transparent',
146
+ borderTopColor: '#1a1a2e',
147
+ },
148
+ });
@@ -786,6 +786,15 @@ ${screen.elementsText}
786
786
  return validationError;
787
787
  }
788
788
 
789
+ // ── Copilot aiConfirm gate ──────────────────────────────────
790
+ // In copilot mode, elements marked with aiConfirm={true} require
791
+ // user confirmation before execution. This is the code-level safety net
792
+ // complementing the prompt-level copilot instructions.
793
+ if (this.config.interactionMode !== 'autopilot') {
794
+ const confirmResult = await this.checkCopilotConfirmation(toolName, args);
795
+ if (confirmResult) return confirmResult;
796
+ }
797
+
789
798
  const result = await tool.execute(args);
790
799
 
791
800
  // Settle window for async side-effects (useEffect, native callbacks)
@@ -838,6 +847,74 @@ ${screen.elementsText}
838
847
  return null;
839
848
  }
840
849
 
850
+ // ─── Copilot Confirmation ─────────────────────────────────────
851
+
852
+ /** Write tools that can mutate state — only these are checked for aiConfirm */
853
+ private static readonly WRITE_TOOLS = new Set([
854
+ 'tap', 'type', 'long_press', 'adjust_slider', 'select_picker', 'set_date',
855
+ ]);
856
+
857
+ /**
858
+ * Check if a tool call targets an aiConfirm element and request user confirmation.
859
+ * Returns null if the action should proceed, or an error string if rejected.
860
+ */
861
+ private async checkCopilotConfirmation(
862
+ toolName: string,
863
+ args: Record<string, any>,
864
+ ): Promise<string | null> {
865
+ // Only gate write tools
866
+ if (!AgentRuntime.WRITE_TOOLS.has(toolName)) return null;
867
+
868
+ // Look up the target element by index
869
+ const index = args.index;
870
+ if (typeof index !== 'number') return null;
871
+
872
+ const screen = this.lastDehydratedRoot as import('./types').DehydratedScreen | null;
873
+ if (!screen?.elements) return null;
874
+
875
+ const element = screen.elements.find(e => e.index === index);
876
+ if (!element?.requiresConfirmation) return null;
877
+
878
+ // Element has aiConfirm — request user confirmation
879
+ const label = element.label || `[${element.type}]`;
880
+ const description = this.getToolStatusLabel(toolName, args);
881
+ const question = `I'm about to ${description} on "${label}". Should I proceed?`;
882
+
883
+ logger.info('AgentRuntime', `🛡️ Copilot: aiConfirm gate triggered for "${toolName}" on "${label}"`);
884
+
885
+ // Use onAskUser if available (integrated into chat UI), otherwise Alert.alert
886
+ if (this.config.onAskUser) {
887
+ const response = await this.config.onAskUser(question);
888
+ const approved = /^(yes|ok|sure|go|proceed|confirm|y)/i.test(response.trim());
889
+ if (!approved) {
890
+ logger.info('AgentRuntime', `🛑 User rejected "${toolName}" on "${label}"`);
891
+ return `❌ User rejected "${toolName}" action on "${label}". Ask what they would like to do instead.`;
892
+ }
893
+ return null;
894
+ }
895
+
896
+ // Fallback: React Native Alert
897
+ const { Alert } = require('react-native');
898
+ const approved = await new Promise<boolean>(resolve => {
899
+ Alert.alert(
900
+ 'Confirm Action',
901
+ question,
902
+ [
903
+ { text: 'Cancel', style: 'cancel', onPress: () => resolve(false) },
904
+ { text: 'Continue', onPress: () => resolve(true) },
905
+ ],
906
+ { cancelable: false },
907
+ );
908
+ });
909
+
910
+ if (!approved) {
911
+ logger.info('AgentRuntime', `🛑 User rejected "${toolName}" on "${label}"`);
912
+ return `❌ User rejected "${toolName}" action on "${label}". Ask what they would like to do instead.`;
913
+ }
914
+
915
+ return null;
916
+ }
917
+
841
918
  // ─── Walk Config (passes security settings to FiberTreeWalker) ─
842
919
 
843
920
  private getWalkConfig(): WalkConfig {
@@ -1190,7 +1267,8 @@ ${screen.elementsText}
1190
1267
  // 5. Send to AI provider
1191
1268
  this.config.onStatusUpdate?.('Analyzing screen...');
1192
1269
  const hasKnowledge = !!this.knowledgeService;
1193
- const systemPrompt = buildSystemPrompt('en', hasKnowledge);
1270
+ const isCopilot = this.config.interactionMode !== 'autopilot';
1271
+ const systemPrompt = buildSystemPrompt('en', hasKnowledge, isCopilot);
1194
1272
  const tools = this.buildToolsForProvider();
1195
1273
 
1196
1274
  logger.info('AgentRuntime', `Sending to AI with ${tools.length} tools...`);
@@ -1339,6 +1417,14 @@ ${screen.elementsText}
1339
1417
  steps: this.history,
1340
1418
  tokenUsage: sessionUsage,
1341
1419
  };
1420
+
1421
+ // Dev warning: remind developers to add aiConfirm for extra safety
1422
+ if (__DEV__ && this.config.interactionMode !== 'autopilot') {
1423
+ logger.info('AgentRuntime',
1424
+ 'ℹ️ Copilot mode active. Tip: Add aiConfirm={true} to critical buttons (e.g. "Place Order", "Delete") for extra safety.'
1425
+ );
1426
+ }
1427
+
1342
1428
  await this.config.onAfterTask?.(result);
1343
1429
  return result;
1344
1430
  } catch (error: any) {
@@ -617,6 +617,7 @@ export function walkFiberTree(rootRef: any, config?: WalkConfig): WalkResult {
617
617
  zoneId: currentZoneId,
618
618
  fiberNode: node,
619
619
  props: { ...props },
620
+ requiresConfirmation: props.aiConfirm === true,
620
621
  });
621
622
 
622
623
  // Build output tag with state attributes
@@ -627,6 +628,10 @@ export function walkFiberTree(rootRef: any, config?: WalkConfig): WalkResult {
627
628
  if (currentZoneId) attrStr += ` zoneId="${currentZoneId}"`;
628
629
  }
629
630
 
631
+ if (props.aiConfirm === true) {
632
+ attrStr += ' aiConfirm';
633
+ }
634
+
630
635
  const textContent = label || '';
631
636
  const elementOutput = `${indent}[${currentIndex}]<${resolvedType}${attrStr}>${textContent} />${childrenText.trim() ? '\n' + childrenText : ''}\n`;
632
637
  currentIndex++;
@@ -89,9 +89,43 @@ const SHARED_CAPABILITY = `- It is ok to fail the task. User would rather you re
89
89
  - The app can have bugs. If something is not working as expected, report it to the user.
90
90
  - Trying too hard can be harmful. If stuck, report partial progress rather than repeating failed actions.`;
91
91
 
92
+ /**
93
+ * Copilot mode rules — AI pauses once before final irreversible commit.
94
+ * Injected when interactionMode is 'copilot' (the default).
95
+ */
96
+ const COPILOT_RULES = `<copilot_mode>
97
+ You are in COPILOT mode. This means:
98
+
99
+ Execute ALL intermediate actions SILENTLY — no confirmation needed:
100
+ - Navigating between screens, tabs, menus
101
+ - Scrolling to find content
102
+ - Typing into form fields
103
+ - Selecting options, filters, categories
104
+ - Adding items to cart (user can remove later)
105
+ - Opening/closing dialogs or details
106
+ - Toggling form controls while filling a form
107
+
108
+ PAUSE only when you reach the FINAL action that IRREVERSIBLY commits
109
+ the user's change. Use ask_user to summarize what you have done so far
110
+ and ask permission BEFORE tapping the commit button. Examples of commit actions:
111
+ - Placing an order / completing a purchase
112
+ - Submitting a form that sends data
113
+ - Deleting something (account, item, message)
114
+ - Confirming a payment or transaction
115
+ - Sending a message or email
116
+ - Saving account/profile changes
117
+
118
+ Elements marked with aiConfirm in the element tree are developer-flagged
119
+ as requiring confirmation. Treat them as commit actions regardless of context.
120
+
121
+ Call ask_user EXACTLY ONCE per task — at the final commit moment, not at
122
+ every step. If the task has no irreversible commit (e.g., "show me my orders",
123
+ "find the cheapest item"), complete the task without pausing.
124
+ </copilot_mode>`;
125
+
92
126
  // ─── Text Agent Prompt ──────────────────────────────────────────────────────
93
127
 
94
- export function buildSystemPrompt(language: string, hasKnowledge = false): string {
128
+ export function buildSystemPrompt(language: string, hasKnowledge = false, isCopilot = true): string {
95
129
  const isArabic = language === 'ar';
96
130
 
97
131
  return `${CONFIDENTIALITY("I'm your app assistant — I can help you navigate and use this app. What would you like to do?")}
@@ -170,6 +204,8 @@ ${NAVIGATION_RULE}
170
204
  ${UI_SIMPLIFICATION_RULE}
171
205
  </rules>
172
206
 
207
+ ${isCopilot ? COPILOT_RULES : ''}
208
+
173
209
  <task_completion_rules>
174
210
  You must call the done action in one of these cases:
175
211
  - When you have fully completed the USER REQUEST.
@@ -188,8 +224,10 @@ The done action is your opportunity to communicate findings and provide a cohere
188
224
  - Use the text field to answer questions, summarize what you found, or explain what you did.
189
225
  - You are ONLY ALLOWED to call done as a single action. Do not call it together with other actions.
190
226
 
191
- The ask_user action should ONLY be used when the user gave an action request but you lack specific information to execute it (e.g., user says "order a pizza" but there are multiple options and you don't know which one).
192
- - Do NOT use ask_user to confirm actions the user explicitly requested. If they said "place my order", just do it.
227
+ The ask_user action should ONLY be used when:
228
+ - The user gave an action request but you lack specific information to execute it (e.g., user says "order a pizza" but there are multiple options and you don't know which one).
229
+ - You are in copilot mode and about to perform an irreversible commit action (see copilot_mode rules above).
230
+ - Do NOT use ask_user for routine confirmations the user already gave. If they said "place my order", proceed to the commit step and confirm there.
193
231
  - NEVER ask for the same confirmation twice. If the user already answered, proceed with their answer.
194
232
  - For destructive/purchase actions (place order, delete, pay), tap the button exactly ONCE. Do not repeat the same action — the user could be charged multiple times.
195
233
  </task_completion_rules>
package/src/core/types.ts CHANGED
@@ -6,6 +6,15 @@
6
6
 
7
7
  export type AgentMode = 'text' | 'voice' | 'human';
8
8
 
9
+ /**
10
+ * Controls how the agent handles irreversible actions.
11
+ * 'copilot' (default): AI pauses before final commit actions (place order, delete, submit).
12
+ * The prompt instructs the AI to ask_user before the final irreversible step.
13
+ * Elements with aiConfirm={true} also trigger a code-level confirmation gate.
14
+ * 'autopilot': Full autonomy — all actions execute without confirmation.
15
+ */
16
+ export type InteractionMode = 'copilot' | 'autopilot';
17
+
9
18
  // ─── Provider Names ──────────────────────────────────────────
10
19
 
11
20
  export type AIProviderName = 'gemini' | 'openai';
@@ -38,6 +47,11 @@ export interface InteractiveElement {
38
47
  * - Switch: onValueChange, value
39
48
  */
40
49
  props: Record<string, any>;
50
+ /**
51
+ * If true, AI interaction with this element requires user confirmation (copilot safety net).
52
+ * Set automatically by the FiberTreeWalker when the element has aiConfirm={true} prop.
53
+ */
54
+ requiresConfirmation?: boolean;
41
55
  }
42
56
 
43
57
  // ─── Dehydrated Screen State ──────────────────────────────────
@@ -119,6 +133,13 @@ export interface AgentConfig {
119
133
  /** Maximum steps per task */
120
134
  maxSteps?: number;
121
135
 
136
+ /**
137
+ * Controls how the agent handles irreversible actions.
138
+ * 'copilot' (default): AI pauses before final commit actions.
139
+ * 'autopilot': Full autonomy, no pauses.
140
+ */
141
+ interactionMode?: InteractionMode;
142
+
122
143
  /**
123
144
  * MCP server mode — controls whether external agents can discover and invoke actions.
124
145
  * 'auto' (default): enabled in __DEV__, disabled in production
package/src/index.ts CHANGED
@@ -51,6 +51,7 @@ export type {
51
51
  AIProviderName,
52
52
  ScreenMap,
53
53
  ScreenMapEntry,
54
+ InteractionMode,
54
55
  } from './core/types';
55
56
 
56
57
  export type {