@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.
- package/cli/dist/server/promptServer.d.ts.map +1 -1
- package/cli/dist/server/promptServer.js +2 -0
- package/cli/dist/server/promptServer.js.map +1 -1
- package/ios/widget.jsbundle +11 -5
- package/package.json +1 -1
- package/widget/BubbleContent.tsx +50 -755
- package/widget/components/FormattedText.tsx +320 -0
- package/widget/components/Header.tsx +225 -0
- package/widget/components/MessageItems.tsx +308 -0
- package/widget/components/ResponseArea.tsx +3 -754
- package/widget/components/TabBar.tsx +161 -0
|
@@ -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
|
+
});
|