@10play/expo-air 0.12.1 → 0.12.2

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.
@@ -0,0 +1,308 @@
1
+ import React from "react";
2
+ import { View, Text, StyleSheet, Platform, Image } from "react-native";
3
+ import type {
4
+ ServerMessage,
5
+ ToolMessage,
6
+ ResultMessage,
7
+ UserPromptMessage,
8
+ HistoryResultMessage,
9
+ SystemDisplayMessage,
10
+ AssistantPart,
11
+ AssistantPartsMessage,
12
+ } from "../services/websocket";
13
+ import { FormattedText } from "./FormattedText";
14
+ import { SPACING, LAYOUT, COLORS, TYPOGRAPHY } from "../constants/design";
15
+
16
+ export function MessageItem({ message }: { key?: React.Key; message: ServerMessage }) {
17
+ switch (message.type) {
18
+ case "stream":
19
+ return null; // Handled by currentParts
20
+
21
+ case "tool":
22
+ // Legacy: individual tool messages from history
23
+ return <ToolItem tool={message} />;
24
+
25
+ case "result":
26
+ return <ResultItem result={message} />;
27
+
28
+ case "error":
29
+ return (
30
+ <View style={styles.errorContainer}>
31
+ <Text style={styles.errorText}>{message.message}</Text>
32
+ </View>
33
+ );
34
+
35
+ case "status":
36
+ return null; // Handled by header
37
+
38
+ case "user_prompt":
39
+ return <UserPromptItem message={message} />;
40
+
41
+ case "history_result":
42
+ return <HistoryResultItem message={message} />;
43
+
44
+ case "assistant_parts":
45
+ return <AssistantPartsItem message={message} />;
46
+
47
+ case "system_message":
48
+ return <SystemMessageItem message={message} />;
49
+
50
+ default:
51
+ return null;
52
+ }
53
+ }
54
+
55
+ function UserPromptItem({ message }: { message: UserPromptMessage }) {
56
+ return (
57
+ <View style={styles.userPromptContainer}>
58
+ {message.images && message.images.length > 0 && (
59
+ <View style={styles.userImages}>
60
+ {message.images.map((img, i) => (
61
+ <Image key={i} source={{ uri: img.uri }} style={styles.userImageThumb} />
62
+ ))}
63
+ </View>
64
+ )}
65
+ {message.content ? (
66
+ <Text style={styles.userPromptText} selectable>{message.content}</Text>
67
+ ) : null}
68
+ </View>
69
+ );
70
+ }
71
+
72
+ function HistoryResultItem({ message }: { message: HistoryResultMessage }) {
73
+ return (
74
+ <View style={styles.resultContainer}>
75
+ <FormattedText content={message.content} />
76
+ </View>
77
+ );
78
+ }
79
+
80
+ function SystemMessageItem({ message }: { message: SystemDisplayMessage }) {
81
+ // Use error styling for errors, muted styling for other system messages
82
+ if (message.messageType === "error") {
83
+ return (
84
+ <View style={styles.errorContainer}>
85
+ <Text style={styles.errorText}>{message.content}</Text>
86
+ </View>
87
+ );
88
+ }
89
+ // Stopped and info messages use muted styling
90
+ return (
91
+ <View style={styles.systemContainer}>
92
+ <Text style={styles.systemText}>{message.content}</Text>
93
+ </View>
94
+ );
95
+ }
96
+
97
+ // Renders interleaved text and tool parts in order
98
+ export function PartsRenderer({ parts, isStreaming }: { parts: AssistantPart[], isStreaming: boolean }) {
99
+ return (
100
+ <View style={styles.partsContainer}>
101
+ {parts.map((part, index) => {
102
+ if (part.type === "text") {
103
+ const isLastPart = index === parts.length - 1;
104
+ return (
105
+ <View key={part.id} style={styles.messageContainer}>
106
+ <FormattedText content={part.content} isStreaming={isStreaming && isLastPart} />
107
+ </View>
108
+ );
109
+ } else if (part.type === "tool") {
110
+ return <ToolPartItem key={part.id} part={part} />;
111
+ }
112
+ return null;
113
+ })}
114
+ </View>
115
+ );
116
+ }
117
+
118
+ // Renders a completed assistant response with parts
119
+ function AssistantPartsItem({ message }: { message: AssistantPartsMessage }) {
120
+ return (
121
+ <View style={styles.resultContainer}>
122
+ <PartsRenderer parts={message.parts} isStreaming={false} />
123
+ {!message.isComplete && (
124
+ <Text style={styles.interruptedText}>(interrupted)</Text>
125
+ )}
126
+ </View>
127
+ );
128
+ }
129
+
130
+ // Shared helper for tool display info
131
+ function getToolDisplayInfo(toolName: string, input: Record<string, unknown> | undefined): { label: string; value: string } {
132
+ const getFileName = (path: string): string => path.split('/').pop() || path;
133
+
134
+ switch (toolName) {
135
+ case "Read":
136
+ return { label: "read", value: getFileName(input?.file_path as string || "file") };
137
+ case "Edit":
138
+ return { label: "edit", value: getFileName(input?.file_path as string || "file") };
139
+ case "Write":
140
+ return { label: "write", value: getFileName(input?.file_path as string || "file") };
141
+ case "Bash": {
142
+ const cmd = input?.command as string || "";
143
+ return { label: "$", value: cmd.length > 45 ? cmd.slice(0, 45) + "…" : cmd };
144
+ }
145
+ case "Glob":
146
+ return { label: "glob", value: input?.pattern as string || "*" };
147
+ case "Grep":
148
+ return { label: "grep", value: input?.pattern as string || "search" };
149
+ case "Task":
150
+ return { label: "agent", value: input?.description as string || "task" };
151
+ default:
152
+ return { label: toolName.toLowerCase(), value: "" };
153
+ }
154
+ }
155
+
156
+ // Renders a tool display line (shared between ToolPartItem and ToolItem)
157
+ function ToolDisplay({ toolName, input, isFailed }: { toolName: string; input?: unknown; isFailed: boolean }) {
158
+ const { label, value } = getToolDisplayInfo(toolName, input as Record<string, unknown> | undefined);
159
+
160
+ return (
161
+ <View style={styles.toolLine}>
162
+ <Text style={isFailed ? styles.toolLabelFailed : styles.toolLabel}>{label}</Text>
163
+ <Text style={isFailed ? styles.toolValueFailed : styles.toolValue} numberOfLines={1}>{value}</Text>
164
+ {isFailed && <Text style={styles.toolLabelFailed}> ✕</Text>}
165
+ </View>
166
+ );
167
+ }
168
+
169
+ // Tool part renderer (for parts in AssistantPartsMessage)
170
+ function ToolPartItem({ part }: { key?: React.Key; part: AssistantPart & { type: "tool" } }) {
171
+ if (part.status === "started") return null;
172
+ return <ToolDisplay toolName={part.toolName} input={part.input} isFailed={part.status === "failed"} />;
173
+ }
174
+
175
+ // Tool item renderer (for legacy ToolMessage from history)
176
+ function ToolItem({ tool }: { tool: ToolMessage }) {
177
+ if (tool.status === "started") return null;
178
+ return <ToolDisplay toolName={tool.toolName} input={tool.input} isFailed={tool.status === "failed"} />;
179
+ }
180
+
181
+ function ResultItem({ result }: { result: ResultMessage }) {
182
+ if (!result.success && result.error) {
183
+ return (
184
+ <View style={styles.errorContainer}>
185
+ <Text style={styles.errorText}>{result.error}</Text>
186
+ </View>
187
+ );
188
+ }
189
+
190
+ return (
191
+ <View style={styles.resultContainer}>
192
+ {result.result && (
193
+ <Text style={styles.responseText} selectable>{result.result}</Text>
194
+ )}
195
+ {result.durationMs !== undefined && (
196
+ <Text style={styles.metaText}>
197
+ {`${result.durationMs}ms`}
198
+ </Text>
199
+ )}
200
+ </View>
201
+ );
202
+ }
203
+
204
+ const styles = StyleSheet.create({
205
+ messageContainer: {
206
+ flexDirection: "row",
207
+ flexWrap: "wrap",
208
+ },
209
+ responseText: {
210
+ color: "rgba(255,255,255,0.95)",
211
+ fontSize: TYPOGRAPHY.SIZE_LG,
212
+ lineHeight: 22,
213
+ },
214
+ resultContainer: {
215
+ marginTop: SPACING.SM,
216
+ },
217
+ partsContainer: {
218
+ // Container for interleaved parts
219
+ },
220
+ interruptedText: {
221
+ color: COLORS.TEXT_MUTED,
222
+ fontSize: TYPOGRAPHY.SIZE_XS + 1, // 12px
223
+ fontStyle: "italic",
224
+ marginTop: SPACING.XS,
225
+ },
226
+ metaText: {
227
+ color: COLORS.TEXT_MUTED,
228
+ fontSize: TYPOGRAPHY.SIZE_XS + 1, // 12px
229
+ marginTop: SPACING.SM + 2, // 10px
230
+ },
231
+ errorContainer: {
232
+ backgroundColor: "rgba(255,59,48,0.15)",
233
+ borderRadius: SPACING.MD,
234
+ padding: SPACING.MD,
235
+ marginVertical: SPACING.SM - 2, // 6px
236
+ },
237
+ errorText: {
238
+ color: "#FF6B6B",
239
+ fontSize: TYPOGRAPHY.SIZE_MD,
240
+ },
241
+ systemContainer: {
242
+ backgroundColor: "rgba(255,255,255,0.05)",
243
+ borderRadius: SPACING.MD,
244
+ padding: SPACING.MD,
245
+ marginVertical: SPACING.SM - 2, // 6px
246
+ },
247
+ systemText: {
248
+ color: COLORS.TEXT_TERTIARY,
249
+ fontSize: TYPOGRAPHY.SIZE_MD,
250
+ fontStyle: "italic",
251
+ },
252
+ toolLine: {
253
+ flexDirection: "row",
254
+ alignItems: "center",
255
+ marginVertical: SPACING.XS,
256
+ },
257
+ toolLabel: {
258
+ color: COLORS.TEXT_MUTED,
259
+ fontSize: TYPOGRAPHY.SIZE_XS + 1, // 12px
260
+ fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
261
+ marginRight: SPACING.SM,
262
+ minWidth: 36,
263
+ },
264
+ toolLabelFailed: {
265
+ color: "rgba(255,100,100,0.6)",
266
+ fontSize: TYPOGRAPHY.SIZE_XS + 1, // 12px
267
+ fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
268
+ marginRight: SPACING.SM,
269
+ minWidth: 36,
270
+ },
271
+ toolValue: {
272
+ color: "rgba(255,255,255,0.7)",
273
+ fontSize: TYPOGRAPHY.SIZE_SM,
274
+ fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
275
+ flexShrink: 1,
276
+ },
277
+ toolValueFailed: {
278
+ color: "rgba(255,100,100,0.7)",
279
+ fontSize: TYPOGRAPHY.SIZE_SM,
280
+ fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
281
+ flexShrink: 1,
282
+ },
283
+ userPromptContainer: {
284
+ backgroundColor: "rgba(0,122,255,0.15)",
285
+ borderRadius: LAYOUT.BORDER_RADIUS_SM + 2, // 16px
286
+ padding: SPACING.MD,
287
+ marginVertical: SPACING.SM,
288
+ alignSelf: "flex-end",
289
+ maxWidth: "85%",
290
+ },
291
+ userPromptText: {
292
+ color: COLORS.TEXT_PRIMARY,
293
+ fontSize: TYPOGRAPHY.SIZE_LG,
294
+ lineHeight: 20,
295
+ },
296
+ userImages: {
297
+ flexDirection: "row",
298
+ flexWrap: "wrap",
299
+ gap: SPACING.SM,
300
+ marginBottom: SPACING.SM,
301
+ },
302
+ userImageThumb: {
303
+ width: 80,
304
+ height: 80,
305
+ borderRadius: SPACING.SM,
306
+ backgroundColor: "rgba(255,255,255,0.1)",
307
+ },
308
+ });