@ggui-ai/mcp-apps-react-native 0.12.0 → 0.14.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.12.0",
3
+ "version": "0.14.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/iframe-runtime": "0.12.0",
62
- "@ggui-ai/protocol": "0.12.0",
63
- "@ggui-ai/design": "0.12.0"
61
+ "@ggui-ai/design": "0.14.0",
62
+ "@ggui-ai/iframe-runtime": "0.14.0",
63
+ "@ggui-ai/protocol": "0.14.0"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@modelcontextprotocol/sdk": "^1.27.0",
@@ -76,7 +76,7 @@
76
76
  "url": "https://github.com/ggui-ai/ggui/issues"
77
77
  },
78
78
  "engines": {
79
- "node": ">=20.0.0"
79
+ "node": ">=22.0.0"
80
80
  },
81
81
  "author": "ggui contributors <hello@ggui.ai>",
82
82
  "scripts": {
@@ -202,10 +202,19 @@ export async function dispatchHostBridgeRequest(
202
202
  }
203
203
  case 'tools/call': {
204
204
  if (ctx.onToolCall === undefined) {
205
+ // In-band method refusal, spec shape (-32601 naming the
206
+ // method — the #600 catalog's H3 grade). Was -32000
207
+ // 'no-tool-handler'; retired (ggui#599 cycle-2) because
208
+ // -32000 collides with the MCP SDK's ErrorCode.ConnectionClosed
209
+ // and could never be classified as a confirmed refusal by the
210
+ // runtime's latch.
205
211
  return {
206
212
  jsonrpc: '2.0',
207
213
  id,
208
- error: { code: -32000, message: 'no-tool-handler' },
214
+ error: {
215
+ code: -32601,
216
+ message: 'tools/call not supported: no tool handler wired',
217
+ },
209
218
  };
210
219
  }
211
220
  const tool = paramString(req.params, 'name');
@@ -61,7 +61,7 @@ export interface McpAppIframePermissions {
61
61
  * - `ui/initialize` replies carry ONLY `{theme, containerDimensions,
62
62
  * locale}` — adapter-boundary rule.
63
63
  * - `tools/call` forwards to `onToolCall`; absent handler → reject
64
- * `no-tool-handler`.
64
+ * an in-band `-32601` naming `tools/call` (was `no-tool-handler`; ggui#599 cycle-2).
65
65
  * - `ui/open-link` with `https?://` → delegates to `Linking.openURL`;
66
66
  * other schemes → reject `unsupported-scheme`.
67
67
  * - Unknown method → `method_not_supported`.
@@ -166,7 +166,7 @@ export interface McpAppIframeProps {
166
166
  * iframe. The host forwards `(toolName, args)` and awaits; the
167
167
  * resolved value becomes the JSON-RPC `result`, rejections become
168
168
  * `-32000` errors. Absent handler = every `tools/call` is rejected
169
- * with `no-tool-handler`.
169
+ * with an in-band `-32601` naming `tools/call` (was `no-tool-handler`; ggui#599 cycle-2).
170
170
  */
171
171
  readonly onToolCall?: (
172
172
  toolName: string,
@@ -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
  });