@ggui-ai/mcp-apps-react-native 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ggui-ai/mcp-apps-react-native",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "React Native SDK for ggui — components and hooks for embedding agent-generated UIs in mobile apps.",
5
5
  "keywords": [
6
6
  "ggui",
@@ -58,9 +58,9 @@
58
58
  },
59
59
  "dependencies": {
60
60
  "@modelcontextprotocol/ext-apps": "^1.7.2",
61
- "@ggui-ai/design": "0.11.0",
62
- "@ggui-ai/protocol": "0.11.0",
63
- "@ggui-ai/iframe-runtime": "0.11.0"
61
+ "@ggui-ai/design": "0.13.0",
62
+ "@ggui-ai/protocol": "0.13.0",
63
+ "@ggui-ai/iframe-runtime": "0.13.0"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@modelcontextprotocol/sdk": "^1.27.0",
@@ -164,7 +164,12 @@ export async function dispatchHostBridgeRequest(
164
164
  // `define`), and a hand-maintained literal would silently drift
165
165
  // from the published version at every release.
166
166
  hostInfo: { name: 'ggui-react-native', version: 'unstamped' },
167
- hostCapabilities: {},
167
+ // Truthful positive advertisement (kit R2 grade, ggui#600 OPEN
168
+ // FINDING closed): a wired tools/call relay advertises
169
+ // `serverTools` so the runtime never spends a failed user
170
+ // gesture probing a capability the host actually has.
171
+ hostCapabilities:
172
+ ctx.onToolCall !== undefined ? { serverTools: {} } : {},
168
173
  hostContext: {
169
174
  locale: ctx.locale,
170
175
  containerDimensions: ctx.containerDimensions,
@@ -3,28 +3,32 @@
3
3
  * shell chrome ("did this generated UI work for you?").
4
4
  *
5
5
  * Platform delta (vs `ggui-react/src/components/UiFeedback.tsx`):
6
- * - DOM elements (`div` / `button` / `form` / `input`) become RN
7
- * primitives (`View` / `Pressable` / `Text` / `TextInput`).
6
+ * - DOM elements (`div` / `button`) become RN primitives
7
+ * (`View` / `Pressable` / `Text`).
8
8
  * - Inline styles with `--ggui-*` CSS-variable hooks become
9
9
  * `StyleSheet.create` constants carrying the same neutral palette
10
10
  * the web copy uses as its variable fallbacks (RN has no CSS
11
11
  * variable layer).
12
12
  * - `data-ggui-ui-feedback*` styling/test hooks become `testID`s
13
13
  * (`ggui-ui-feedback`, `ggui-ui-feedback-verdict-<verdict>`, …).
14
- * - The comment `<form>` submit becomes a Send `Pressable` plus the
15
- * `TextInput`'s `onSubmitEditing` (keyboard return key).
16
14
  * - `role` / `aria-label` become `accessibilityLabel` /
17
15
  * `accessibilityRole` props.
16
+ * - The web copy's monochrome stroked SVG thumbs become 👍 / 👎
17
+ * emoji glyphs in `Text` (#653 lean call: `react-native-svg` is
18
+ * not a dependency of this package and one chrome row does not
19
+ * justify adding it to the lockstep wave; the glyph renders in the
20
+ * platform's emoji face rather than the theme ink — revisit if a
21
+ * ggui icon dependency ever lands for other reasons).
18
22
  * The exported surface is identical to the web copy — pinned by the
19
23
  * twin-parity gate (`DOCUMENTED_DELTA_TWINS`).
20
24
  *
21
- * Three verdicts — Love / Dislike / Other (free-text comment) — plus a
22
- * dismiss control. Entirely host-driven: the component renders NOTHING
23
- * unless the host passes `onUiFeedback`, so deployments that don't
24
- * collect feedback never show a dead affordance. Where the payload
25
- * goes is the host's choice — a logger, an analytics client, a support
26
- * inbox; this component only builds the typed {@link UiFeedbackPayload}
27
- * and hands it over.
25
+ * Two verdicts — thumbs up / thumbs down (#653) — plus a dismiss
26
+ * control. Entirely host-driven: the component renders NOTHING unless
27
+ * the host passes `onUiFeedback`, so deployments that don't collect
28
+ * feedback never show a dead affordance. Where the payload goes is
29
+ * the host's choice — a logger, an analytics client, a support inbox;
30
+ * this component only builds the typed {@link UiFeedbackPayload} and
31
+ * hands it over.
28
32
  *
29
33
  * Zero wire surface: feedback never crosses the agent ↔ UI contract.
30
34
  * It is host-app chrome (Data vs Behavior — the agent cannot observe
@@ -50,20 +54,17 @@
50
54
  * choice safe: omitting `onUiFeedback` fully disables this surface.
51
55
  */
52
56
  import { useCallback, useState } from 'react';
53
- import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
57
+ import { Pressable, StyleSheet, Text, View } from 'react-native';
54
58
 
55
- /** The three feedback verdicts the affordance can emit. */
56
- export type UiFeedbackVerdict = 'love' | 'dislike' | 'other';
59
+ /** The two feedback verdicts the affordance can emit. */
60
+ export type UiFeedbackVerdict = 'up' | 'down';
57
61
 
58
62
  /**
59
63
  * Payload handed to {@link UiFeedbackProps.onUiFeedback}. Context
60
- * fields are present exactly when the host supplied them as props;
61
- * `comment` is present only for `verdict: 'other'` with a non-empty
62
- * trimmed comment.
64
+ * fields are present exactly when the host supplied them as props.
63
65
  */
64
66
  export interface UiFeedbackPayload {
65
67
  verdict: UiFeedbackVerdict;
66
- comment?: string;
67
68
  /** GguiSession id of the render the feedback is about. */
68
69
  sessionId?: string;
69
70
  /** Tool that produced the render (e.g. `ggui_render`). */
@@ -82,19 +83,26 @@ export interface UiFeedbackProps {
82
83
  toolName?: string;
83
84
  }
84
85
 
85
- type Phase = 'idle' | 'comment' | 'sent' | 'dismissed';
86
+ type Phase = 'idle' | 'sent' | 'dismissed';
87
+
88
+ const VERDICT_GLYPHS: Record<UiFeedbackVerdict, string> = {
89
+ up: '👍',
90
+ down: '👎',
91
+ };
92
+
93
+ const VERDICT_LABELS: Record<UiFeedbackVerdict, string> = {
94
+ up: 'Thumbs up',
95
+ down: 'Thumbs down',
96
+ };
86
97
 
87
98
  export function UiFeedback({ onUiFeedback, sessionId, toolName }: UiFeedbackProps) {
88
99
  const [phase, setPhase] = useState<Phase>('idle');
89
- const [comment, setComment] = useState('');
90
100
 
91
101
  const emit = useCallback(
92
- (verdict: UiFeedbackVerdict, commentText?: string) => {
102
+ (verdict: UiFeedbackVerdict) => {
93
103
  if (!onUiFeedback) return;
94
- const trimmed = commentText?.trim();
95
104
  onUiFeedback({
96
105
  verdict,
97
- ...(trimmed !== undefined && trimmed.length > 0 ? { comment: trimmed } : {}),
98
106
  ...(sessionId !== undefined ? { sessionId } : {}),
99
107
  ...(toolName !== undefined ? { toolName } : {}),
100
108
  });
@@ -103,10 +111,6 @@ export function UiFeedback({ onUiFeedback, sessionId, toolName }: UiFeedbackProp
103
111
  [onUiFeedback, sessionId, toolName],
104
112
  );
105
113
 
106
- const onCommentSubmit = useCallback(() => {
107
- emit('other', comment);
108
- }, [emit, comment]);
109
-
110
114
  // Hidden entirely: no sink wired, or the user dismissed it.
111
115
  if (!onUiFeedback || phase === 'dismissed') return null;
112
116
 
@@ -120,54 +124,19 @@ export function UiFeedback({ onUiFeedback, sessionId, toolName }: UiFeedbackProp
120
124
  <Text testID="ggui-ui-feedback-thanks" style={styles.thanksText}>
121
125
  Thanks for the feedback
122
126
  </Text>
123
- ) : phase === 'comment' ? (
124
- <View style={styles.commentForm} testID="ggui-ui-feedback-comment-form">
125
- <TextInput
126
- value={comment}
127
- onChangeText={setComment}
128
- onSubmitEditing={onCommentSubmit}
129
- placeholder="What happened?"
130
- accessibilityLabel="Feedback comment"
131
- testID="ggui-ui-feedback-comment"
132
- style={styles.commentInput}
133
- autoFocus
134
- />
135
- <Pressable
136
- accessibilityRole="button"
137
- style={styles.verdictButton}
138
- testID="ggui-ui-feedback-send"
139
- onPress={onCommentSubmit}
140
- >
141
- <Text style={styles.verdictText}>Send</Text>
142
- </Pressable>
143
- </View>
144
127
  ) : (
145
- <>
128
+ (['up', 'down'] as const).map((verdict) => (
146
129
  <Pressable
130
+ key={verdict}
147
131
  accessibilityRole="button"
132
+ accessibilityLabel={VERDICT_LABELS[verdict]}
148
133
  style={styles.verdictButton}
149
- testID="ggui-ui-feedback-verdict-love"
150
- onPress={() => emit('love')}
134
+ testID={`ggui-ui-feedback-verdict-${verdict}`}
135
+ onPress={() => emit(verdict)}
151
136
  >
152
- <Text style={styles.verdictText}>Love</Text>
137
+ <Text style={styles.verdictText}>{VERDICT_GLYPHS[verdict]}</Text>
153
138
  </Pressable>
154
- <Pressable
155
- accessibilityRole="button"
156
- style={styles.verdictButton}
157
- testID="ggui-ui-feedback-verdict-dislike"
158
- onPress={() => emit('dislike')}
159
- >
160
- <Text style={styles.verdictText}>Dislike</Text>
161
- </Pressable>
162
- <Pressable
163
- accessibilityRole="button"
164
- style={styles.verdictButton}
165
- testID="ggui-ui-feedback-verdict-other"
166
- onPress={() => setPhase('comment')}
167
- >
168
- <Text style={styles.verdictText}>Other…</Text>
169
- </Pressable>
170
- </>
139
+ ))
171
140
  )}
172
141
  <Pressable
173
142
  accessibilityRole="button"
@@ -199,12 +168,12 @@ const styles = StyleSheet.create({
199
168
  borderWidth: 1,
200
169
  borderColor: '#e5e7eb',
201
170
  borderRadius: 4,
202
- paddingVertical: 2,
203
- paddingHorizontal: 8,
171
+ paddingVertical: 3,
172
+ paddingHorizontal: 7,
204
173
  },
205
174
  verdictText: {
206
175
  fontSize: 12,
207
- color: '#4b5563',
176
+ lineHeight: 14,
208
177
  },
209
178
  dismissButton: {
210
179
  paddingVertical: 2,
@@ -214,20 +183,4 @@ const styles = StyleSheet.create({
214
183
  fontSize: 12,
215
184
  color: '#9ca3af',
216
185
  },
217
- commentForm: {
218
- flexDirection: 'row',
219
- alignItems: 'center',
220
- gap: 4,
221
- },
222
- commentInput: {
223
- fontSize: 12,
224
- paddingVertical: 2,
225
- paddingHorizontal: 6,
226
- minWidth: 140,
227
- borderWidth: 1,
228
- borderColor: '#e5e7eb',
229
- borderRadius: 4,
230
- backgroundColor: '#ffffff',
231
- color: '#1f2937',
232
- },
233
186
  });
@@ -93,9 +93,33 @@ export interface ThemeProviderProps {
93
93
  children: ReactNode;
94
94
  }
95
95
 
96
+ /**
97
+ * TERMINAL CONSUMER of the theme-mode ladder (adversarial-cycle
98
+ * ruling, ggui#598 leg 4): the RN provider paints the NATIVE chrome
99
+ * around the WebView — there is no downstream layer left to fill an
100
+ * absent opinion, and React Native must render with SOME concrete
101
+ * scheme. The `?? 'light'` here is therefore the documented
102
+ * terminal-consumer default (the same posture as the design package's
103
+ * css-tokens terminal default), NOT a mid-ladder defaulting: it fires
104
+ * only after the embedder's explicit prop AND the OS scheme are both
105
+ * absent (`useColorScheme()` → null, rare). Mid-ladder code — anything
106
+ * with a layer below it — must never default; it delegates to
107
+ * `@ggui-ai/protocol/integrations/theme-binding` and lets absence
108
+ * propagate (the composition law).
109
+ *
110
+ * @internal — exported for the precedence pin; production caller is
111
+ * `ThemeProvider`.
112
+ */
113
+ export function resolveNativeScheme(
114
+ prop: 'light' | 'dark' | undefined,
115
+ system: 'light' | 'dark' | null | undefined,
116
+ ): 'light' | 'dark' {
117
+ return prop ?? system ?? 'light';
118
+ }
119
+
96
120
  export function ThemeProvider({ colorScheme: colorSchemeProp, children }: ThemeProviderProps) {
97
121
  const systemScheme = useColorScheme();
98
- const effectiveScheme = colorSchemeProp ?? systemScheme ?? 'light';
122
+ const effectiveScheme = resolveNativeScheme(colorSchemeProp, systemScheme);
99
123
  const [reduceMotionEnabled, setReduceMotionEnabled] = useState(false);
100
124
 
101
125
  useEffect(() => {