@10play/expo-air 0.12.0 → 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/gitOperations.d.ts +31 -0
- package/cli/dist/server/gitOperations.d.ts.map +1 -0
- package/cli/dist/server/gitOperations.js +232 -0
- package/cli/dist/server/gitOperations.js.map +1 -0
- package/cli/dist/server/promptServer.d.ts +1 -6
- package/cli/dist/server/promptServer.d.ts.map +1 -1
- package/cli/dist/server/promptServer.js +72 -264
- package/cli/dist/server/promptServer.js.map +1 -1
- package/cli/dist/tsconfig.tsbuildinfo +1 -1
- package/cli/dist/types/messages.d.ts +1 -0
- package/cli/dist/types/messages.d.ts.map +1 -1
- package/ios/FloatingBubbleManager.swift +73 -1
- package/ios/widget.jsbundle +12 -6
- package/package.json +1 -1
- package/widget/BubbleContent.tsx +85 -743
- package/widget/components/BranchSwitcher.tsx +205 -40
- 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 +32 -617
- package/widget/components/TabBar.tsx +161 -0
- package/widget/services/websocket.ts +1 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useRef, useEffect, useState, useCallback } from "react";
|
|
2
|
-
import { View, Text, ScrollView, StyleSheet, TouchableOpacity, NativeScrollEvent, Keyboard, Platform
|
|
3
|
-
import type { ServerMessage,
|
|
2
|
+
import { View, Text, ScrollView, StyleSheet, TouchableOpacity, NativeScrollEvent, Keyboard, Platform } from "react-native";
|
|
3
|
+
import type { ServerMessage, AssistantPart } from "../services/websocket";
|
|
4
|
+
import { MessageItem, PartsRenderer } from "./MessageItems";
|
|
4
5
|
import { SPACING, LAYOUT, COLORS, TYPOGRAPHY } from "../constants/design";
|
|
5
6
|
|
|
6
7
|
interface ResponseAreaProps {
|
|
@@ -15,21 +16,32 @@ export function ResponseArea({ messages, currentParts }: ResponseAreaProps) {
|
|
|
15
16
|
const scrollViewHeightRef = useRef(0);
|
|
16
17
|
|
|
17
18
|
// Track if user is at bottom (within 50px threshold)
|
|
18
|
-
const handleScroll = useCallback(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
const handleScroll = useCallback(
|
|
20
|
+
(event: { nativeEvent: NativeScrollEvent }) => {
|
|
21
|
+
const { contentOffset, contentSize, layoutMeasurement } =
|
|
22
|
+
event.nativeEvent;
|
|
23
|
+
const distanceFromBottom =
|
|
24
|
+
contentSize.height - layoutMeasurement.height - contentOffset.y;
|
|
25
|
+
setIsAtBottom(distanceFromBottom < 50);
|
|
26
|
+
},
|
|
27
|
+
[],
|
|
28
|
+
);
|
|
23
29
|
|
|
24
30
|
// Track content size changes
|
|
25
|
-
const handleContentSizeChange = useCallback(
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
const handleContentSizeChange = useCallback(
|
|
32
|
+
(width: number, height: number) => {
|
|
33
|
+
contentHeightRef.current = height;
|
|
34
|
+
},
|
|
35
|
+
[],
|
|
36
|
+
);
|
|
28
37
|
|
|
29
38
|
// Track scroll view layout
|
|
30
|
-
const handleLayout = useCallback(
|
|
31
|
-
|
|
32
|
-
|
|
39
|
+
const handleLayout = useCallback(
|
|
40
|
+
(event: { nativeEvent: { layout: { height: number } } }) => {
|
|
41
|
+
scrollViewHeightRef.current = event.nativeEvent.layout.height;
|
|
42
|
+
},
|
|
43
|
+
[],
|
|
44
|
+
);
|
|
33
45
|
|
|
34
46
|
// Auto-scroll only when at bottom
|
|
35
47
|
useEffect(() => {
|
|
@@ -53,7 +65,8 @@ export function ResponseArea({ messages, currentParts }: ResponseAreaProps) {
|
|
|
53
65
|
|
|
54
66
|
// Scroll to bottom when keyboard opens (if already at bottom)
|
|
55
67
|
useEffect(() => {
|
|
56
|
-
const keyboardEvent =
|
|
68
|
+
const keyboardEvent =
|
|
69
|
+
Platform.OS === "ios" ? "keyboardWillShow" : "keyboardDidShow";
|
|
57
70
|
const subscription = Keyboard.addListener(keyboardEvent, () => {
|
|
58
71
|
if (isAtBottom) {
|
|
59
72
|
// Delay to let the native resize happen first
|
|
@@ -102,7 +115,11 @@ export function ResponseArea({ messages, currentParts }: ResponseAreaProps) {
|
|
|
102
115
|
)}
|
|
103
116
|
</ScrollView>
|
|
104
117
|
{!isAtBottom && (
|
|
105
|
-
<TouchableOpacity
|
|
118
|
+
<TouchableOpacity
|
|
119
|
+
style={styles.scrollButton}
|
|
120
|
+
onPress={scrollToBottom}
|
|
121
|
+
activeOpacity={0.8}
|
|
122
|
+
>
|
|
106
123
|
<Text style={styles.scrollButtonText}>↓</Text>
|
|
107
124
|
</TouchableOpacity>
|
|
108
125
|
)}
|
|
@@ -110,384 +127,6 @@ export function ResponseArea({ messages, currentParts }: ResponseAreaProps) {
|
|
|
110
127
|
);
|
|
111
128
|
}
|
|
112
129
|
|
|
113
|
-
function MessageItem({ message }: { key?: React.Key; message: ServerMessage }) {
|
|
114
|
-
switch (message.type) {
|
|
115
|
-
case "stream":
|
|
116
|
-
return null; // Handled by currentParts
|
|
117
|
-
|
|
118
|
-
case "tool":
|
|
119
|
-
// Legacy: individual tool messages from history
|
|
120
|
-
return <ToolItem tool={message} />;
|
|
121
|
-
|
|
122
|
-
case "result":
|
|
123
|
-
return <ResultItem result={message} />;
|
|
124
|
-
|
|
125
|
-
case "error":
|
|
126
|
-
return (
|
|
127
|
-
<View style={styles.errorContainer}>
|
|
128
|
-
<Text style={styles.errorText}>{message.message}</Text>
|
|
129
|
-
</View>
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
case "status":
|
|
133
|
-
return null; // Handled by header
|
|
134
|
-
|
|
135
|
-
case "user_prompt":
|
|
136
|
-
return <UserPromptItem message={message} />;
|
|
137
|
-
|
|
138
|
-
case "history_result":
|
|
139
|
-
return <HistoryResultItem message={message} />;
|
|
140
|
-
|
|
141
|
-
case "assistant_parts":
|
|
142
|
-
return <AssistantPartsItem message={message} />;
|
|
143
|
-
|
|
144
|
-
case "system_message":
|
|
145
|
-
return <SystemMessageItem message={message} />;
|
|
146
|
-
|
|
147
|
-
default:
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function UserPromptItem({ message }: { message: UserPromptMessage }) {
|
|
153
|
-
return (
|
|
154
|
-
<View style={styles.userPromptContainer}>
|
|
155
|
-
{message.images && message.images.length > 0 && (
|
|
156
|
-
<View style={styles.userImages}>
|
|
157
|
-
{message.images.map((img, i) => (
|
|
158
|
-
<Image key={i} source={{ uri: img.uri }} style={styles.userImageThumb} />
|
|
159
|
-
))}
|
|
160
|
-
</View>
|
|
161
|
-
)}
|
|
162
|
-
{message.content ? (
|
|
163
|
-
<Text style={styles.userPromptText} selectable>{message.content}</Text>
|
|
164
|
-
) : null}
|
|
165
|
-
</View>
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function HistoryResultItem({ message }: { message: HistoryResultMessage }) {
|
|
170
|
-
return (
|
|
171
|
-
<View style={styles.resultContainer}>
|
|
172
|
-
<FormattedText content={message.content} />
|
|
173
|
-
</View>
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function SystemMessageItem({ message }: { message: SystemDisplayMessage }) {
|
|
178
|
-
// Use error styling for errors, muted styling for other system messages
|
|
179
|
-
if (message.messageType === "error") {
|
|
180
|
-
return (
|
|
181
|
-
<View style={styles.errorContainer}>
|
|
182
|
-
<Text style={styles.errorText}>{message.content}</Text>
|
|
183
|
-
</View>
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
// Stopped and info messages use muted styling
|
|
187
|
-
return (
|
|
188
|
-
<View style={styles.systemContainer}>
|
|
189
|
-
<Text style={styles.systemText}>{message.content}</Text>
|
|
190
|
-
</View>
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Renders formatted text with markdown-style lists, code, and emphasis
|
|
195
|
-
function FormattedText({ content, isStreaming }: { content: string; isStreaming?: boolean }) {
|
|
196
|
-
const lines = content.split('\n');
|
|
197
|
-
const elements: React.ReactNode[] = [];
|
|
198
|
-
let i = 0;
|
|
199
|
-
|
|
200
|
-
while (i < lines.length) {
|
|
201
|
-
const line = lines[i];
|
|
202
|
-
const trimmed = line.trim();
|
|
203
|
-
|
|
204
|
-
// Code block start (check first to avoid matching content inside code blocks)
|
|
205
|
-
if (trimmed.startsWith('```')) {
|
|
206
|
-
const codeLines: string[] = [];
|
|
207
|
-
const lang = trimmed.slice(3).trim();
|
|
208
|
-
i++;
|
|
209
|
-
while (i < lines.length && !lines[i].trim().startsWith('```')) {
|
|
210
|
-
codeLines.push(lines[i]);
|
|
211
|
-
i++;
|
|
212
|
-
}
|
|
213
|
-
elements.push(
|
|
214
|
-
<View key={`code-${i}`} style={styles.codeBlock}>
|
|
215
|
-
{lang && <Text style={styles.codeLang}>{lang}</Text>}
|
|
216
|
-
<Text style={styles.codeText} selectable>{codeLines.join('\n')}</Text>
|
|
217
|
-
</View>
|
|
218
|
-
);
|
|
219
|
-
i++; // skip closing ```
|
|
220
|
-
continue;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Heading (# ## ### etc.)
|
|
224
|
-
const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
|
|
225
|
-
if (headingMatch) {
|
|
226
|
-
const level = headingMatch[1].length;
|
|
227
|
-
const headingStyle = level <= 2 ? styles.heading1 : level <= 4 ? styles.heading2 : styles.heading3;
|
|
228
|
-
elements.push(
|
|
229
|
-
<Text key={i} style={headingStyle} selectable>{formatInlineText(headingMatch[2])}</Text>
|
|
230
|
-
);
|
|
231
|
-
i++;
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Blockquote (> text) - collect consecutive > lines
|
|
236
|
-
if (trimmed.startsWith('> ') || trimmed === '>') {
|
|
237
|
-
const quoteLines: string[] = [];
|
|
238
|
-
while (i < lines.length && (lines[i].trim().startsWith('> ') || lines[i].trim() === '>')) {
|
|
239
|
-
const qContent = lines[i].trim().startsWith('> ') ? lines[i].trim().slice(2) : '';
|
|
240
|
-
quoteLines.push(qContent);
|
|
241
|
-
i++;
|
|
242
|
-
}
|
|
243
|
-
elements.push(
|
|
244
|
-
<View key={`quote-${i}`} style={styles.blockquote}>
|
|
245
|
-
<Text style={styles.blockquoteText} selectable>{formatInlineText(quoteLines.join('\n'))}</Text>
|
|
246
|
-
</View>
|
|
247
|
-
);
|
|
248
|
-
continue;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Task list item (- [ ] or - [x])
|
|
252
|
-
const taskMatch = trimmed.match(/^[-*]\s+\[([ xX])\]\s+(.+)$/);
|
|
253
|
-
if (taskMatch) {
|
|
254
|
-
const checked = taskMatch[1] !== ' ';
|
|
255
|
-
elements.push(
|
|
256
|
-
<View key={i} style={styles.listItem}>
|
|
257
|
-
<Text style={styles.listBullet}>{checked ? '☑' : '☐'}</Text>
|
|
258
|
-
<Text style={styles.listText} selectable>{formatInlineText(taskMatch[2])}</Text>
|
|
259
|
-
</View>
|
|
260
|
-
);
|
|
261
|
-
i++;
|
|
262
|
-
continue;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Numbered list item (1. 2. 3.) - with nesting via indentation
|
|
266
|
-
const numberedMatch = line.match(/^(\s*)(\d+)\.\s+(.+)$/);
|
|
267
|
-
if (numberedMatch) {
|
|
268
|
-
const indent = Math.floor(numberedMatch[1].length / 2);
|
|
269
|
-
const [, , num, text] = numberedMatch;
|
|
270
|
-
elements.push(
|
|
271
|
-
<View key={i} style={[styles.listItem, indent > 0 && { paddingLeft: SPACING.SM + indent * SPACING.LG }]}>
|
|
272
|
-
<Text style={styles.listNumber}>{num}.</Text>
|
|
273
|
-
<Text style={styles.listText} selectable>{formatInlineText(text)}</Text>
|
|
274
|
-
</View>
|
|
275
|
-
);
|
|
276
|
-
i++;
|
|
277
|
-
continue;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Bullet list item (- or *) - with nesting via indentation
|
|
281
|
-
const bulletMatch = line.match(/^(\s*)[-*]\s+(.+)$/);
|
|
282
|
-
if (bulletMatch) {
|
|
283
|
-
const indent = Math.floor(bulletMatch[1].length / 2);
|
|
284
|
-
const bulletChar = indent === 0 ? '•' : indent === 1 ? '◦' : '▪';
|
|
285
|
-
elements.push(
|
|
286
|
-
<View key={i} style={[styles.listItem, indent > 0 && { paddingLeft: SPACING.SM + indent * SPACING.LG }]}>
|
|
287
|
-
<Text style={styles.listBullet}>{bulletChar}</Text>
|
|
288
|
-
<Text style={styles.listText} selectable>{formatInlineText(bulletMatch[2])}</Text>
|
|
289
|
-
</View>
|
|
290
|
-
);
|
|
291
|
-
i++;
|
|
292
|
-
continue;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Horizontal rule (---, ***, ___)
|
|
296
|
-
if (/^[-*_]{3,}$/.test(trimmed)) {
|
|
297
|
-
elements.push(<View key={i} style={styles.horizontalRule} />);
|
|
298
|
-
i++;
|
|
299
|
-
continue;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// Empty line = paragraph break
|
|
303
|
-
if (!trimmed) {
|
|
304
|
-
elements.push(<View key={i} style={styles.paragraphBreak} />);
|
|
305
|
-
i++;
|
|
306
|
-
continue;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Regular text
|
|
310
|
-
elements.push(
|
|
311
|
-
<Text key={i} style={styles.responseText} selectable>{formatInlineText(line)}</Text>
|
|
312
|
-
);
|
|
313
|
-
i++;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
return (
|
|
317
|
-
<View style={styles.formattedContainer}>
|
|
318
|
-
{elements}
|
|
319
|
-
{isStreaming && <View style={styles.cursor} />}
|
|
320
|
-
</View>
|
|
321
|
-
);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Format inline elements: code spans first, then links, then emphasis
|
|
325
|
-
function formatInlineText(text: string): React.ReactNode {
|
|
326
|
-
// Split on inline code first to avoid processing markdown inside code spans
|
|
327
|
-
const codeParts = text.split(/(`[^`]+`)/g);
|
|
328
|
-
if (codeParts.length === 1) {
|
|
329
|
-
return formatLinks(text);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
return codeParts.map((part, i) => {
|
|
333
|
-
if (part.startsWith('`') && part.endsWith('`')) {
|
|
334
|
-
return <Text key={i} style={styles.inlineCode}>{part.slice(1, -1)}</Text>;
|
|
335
|
-
}
|
|
336
|
-
return <React.Fragment key={i}>{formatLinks(part)}</React.Fragment>;
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Format markdown links [text](url)
|
|
341
|
-
function formatLinks(text: string): React.ReactNode {
|
|
342
|
-
const parts = text.split(/(\[[^\]]+\]\([^)]+\))/g);
|
|
343
|
-
if (parts.length === 1) return formatEmphasis(text);
|
|
344
|
-
|
|
345
|
-
return parts.map((part, i) => {
|
|
346
|
-
const linkMatch = part.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
|
|
347
|
-
if (linkMatch) {
|
|
348
|
-
return (
|
|
349
|
-
<Text
|
|
350
|
-
key={i}
|
|
351
|
-
style={styles.linkText}
|
|
352
|
-
onPress={() => Linking.openURL(linkMatch[2])}
|
|
353
|
-
>
|
|
354
|
-
{linkMatch[1]}
|
|
355
|
-
</Text>
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
return <React.Fragment key={i}>{formatEmphasis(part)}</React.Fragment>;
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// Format bold, italic, and strikethrough emphasis
|
|
363
|
-
function formatEmphasis(text: string): React.ReactNode {
|
|
364
|
-
// Match **bold**, ~~strikethrough~~, *italic*, and plain text segments
|
|
365
|
-
const parts = text.split(/(\*\*[^*]+\*\*|~~[^~]+~~|\*[^*]+\*)/g);
|
|
366
|
-
if (parts.length === 1) return text;
|
|
367
|
-
|
|
368
|
-
return parts.map((part, i) => {
|
|
369
|
-
if (part.startsWith('**') && part.endsWith('**')) {
|
|
370
|
-
return <Text key={i} style={styles.boldText}>{part.slice(2, -2)}</Text>;
|
|
371
|
-
}
|
|
372
|
-
if (part.startsWith('~~') && part.endsWith('~~')) {
|
|
373
|
-
return <Text key={i} style={styles.strikethroughText}>{part.slice(2, -2)}</Text>;
|
|
374
|
-
}
|
|
375
|
-
if (part.startsWith('*') && part.endsWith('*')) {
|
|
376
|
-
return <Text key={i} style={styles.italicText}>{part.slice(1, -1)}</Text>;
|
|
377
|
-
}
|
|
378
|
-
return part;
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// Renders interleaved text and tool parts in order
|
|
383
|
-
function PartsRenderer({ parts, isStreaming }: { parts: AssistantPart[], isStreaming: boolean }) {
|
|
384
|
-
return (
|
|
385
|
-
<View style={styles.partsContainer}>
|
|
386
|
-
{parts.map((part, index) => {
|
|
387
|
-
if (part.type === "text") {
|
|
388
|
-
const isLastPart = index === parts.length - 1;
|
|
389
|
-
return (
|
|
390
|
-
<View key={part.id} style={styles.messageContainer}>
|
|
391
|
-
<FormattedText content={part.content} isStreaming={isStreaming && isLastPart} />
|
|
392
|
-
</View>
|
|
393
|
-
);
|
|
394
|
-
} else if (part.type === "tool") {
|
|
395
|
-
return <ToolPartItem key={part.id} part={part} />;
|
|
396
|
-
}
|
|
397
|
-
return null;
|
|
398
|
-
})}
|
|
399
|
-
</View>
|
|
400
|
-
);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// Renders a completed assistant response with parts
|
|
404
|
-
function AssistantPartsItem({ message }: { message: AssistantPartsMessage }) {
|
|
405
|
-
return (
|
|
406
|
-
<View style={styles.resultContainer}>
|
|
407
|
-
<PartsRenderer parts={message.parts} isStreaming={false} />
|
|
408
|
-
{!message.isComplete && (
|
|
409
|
-
<Text style={styles.interruptedText}>(interrupted)</Text>
|
|
410
|
-
)}
|
|
411
|
-
</View>
|
|
412
|
-
);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// Shared helper for tool display info
|
|
416
|
-
function getToolDisplayInfo(toolName: string, input: Record<string, unknown> | undefined): { label: string; value: string } {
|
|
417
|
-
const getFileName = (path: string): string => path.split('/').pop() || path;
|
|
418
|
-
|
|
419
|
-
switch (toolName) {
|
|
420
|
-
case "Read":
|
|
421
|
-
return { label: "read", value: getFileName(input?.file_path as string || "file") };
|
|
422
|
-
case "Edit":
|
|
423
|
-
return { label: "edit", value: getFileName(input?.file_path as string || "file") };
|
|
424
|
-
case "Write":
|
|
425
|
-
return { label: "write", value: getFileName(input?.file_path as string || "file") };
|
|
426
|
-
case "Bash": {
|
|
427
|
-
const cmd = input?.command as string || "";
|
|
428
|
-
return { label: "$", value: cmd.length > 45 ? cmd.slice(0, 45) + "…" : cmd };
|
|
429
|
-
}
|
|
430
|
-
case "Glob":
|
|
431
|
-
return { label: "glob", value: input?.pattern as string || "*" };
|
|
432
|
-
case "Grep":
|
|
433
|
-
return { label: "grep", value: input?.pattern as string || "search" };
|
|
434
|
-
case "Task":
|
|
435
|
-
return { label: "agent", value: input?.description as string || "task" };
|
|
436
|
-
default:
|
|
437
|
-
return { label: toolName.toLowerCase(), value: "" };
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// Renders a tool display line (shared between ToolPartItem and ToolItem)
|
|
442
|
-
function ToolDisplay({ toolName, input, isFailed }: { toolName: string; input?: unknown; isFailed: boolean }) {
|
|
443
|
-
const { label, value } = getToolDisplayInfo(toolName, input as Record<string, unknown> | undefined);
|
|
444
|
-
|
|
445
|
-
return (
|
|
446
|
-
<View style={styles.toolLine}>
|
|
447
|
-
<Text style={isFailed ? styles.toolLabelFailed : styles.toolLabel}>{label}</Text>
|
|
448
|
-
<Text style={isFailed ? styles.toolValueFailed : styles.toolValue} numberOfLines={1}>{value}</Text>
|
|
449
|
-
{isFailed && <Text style={styles.toolLabelFailed}> ✕</Text>}
|
|
450
|
-
</View>
|
|
451
|
-
);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// Tool part renderer (for parts in AssistantPartsMessage)
|
|
455
|
-
function ToolPartItem({ part }: { key?: React.Key; part: AssistantPart & { type: "tool" } }) {
|
|
456
|
-
if (part.status === "started") return null;
|
|
457
|
-
return <ToolDisplay toolName={part.toolName} input={part.input} isFailed={part.status === "failed"} />;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// Tool item renderer (for legacy ToolMessage from history)
|
|
461
|
-
function ToolItem({ tool }: { tool: ToolMessage }) {
|
|
462
|
-
if (tool.status === "started") return null;
|
|
463
|
-
return <ToolDisplay toolName={tool.toolName} input={tool.input} isFailed={tool.status === "failed"} />;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
function ResultItem({ result }: { result: ResultMessage }) {
|
|
467
|
-
if (!result.success && result.error) {
|
|
468
|
-
return (
|
|
469
|
-
<View style={styles.errorContainer}>
|
|
470
|
-
<Text style={styles.errorText}>{result.error}</Text>
|
|
471
|
-
</View>
|
|
472
|
-
);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
return (
|
|
476
|
-
<View style={styles.resultContainer}>
|
|
477
|
-
{result.result && (
|
|
478
|
-
<Text style={styles.responseText} selectable>{result.result}</Text>
|
|
479
|
-
)}
|
|
480
|
-
{(result.costUsd !== undefined || result.durationMs !== undefined) && (
|
|
481
|
-
<Text style={styles.metaText}>
|
|
482
|
-
{result.durationMs !== undefined && `${result.durationMs}ms`}
|
|
483
|
-
{result.costUsd !== undefined && result.durationMs !== undefined && " • "}
|
|
484
|
-
{result.costUsd !== undefined && `$${result.costUsd.toFixed(4)}`}
|
|
485
|
-
</Text>
|
|
486
|
-
)}
|
|
487
|
-
</View>
|
|
488
|
-
);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
130
|
const styles = StyleSheet.create({
|
|
492
131
|
wrapper: {
|
|
493
132
|
flex: 1,
|
|
@@ -513,230 +152,6 @@ const styles = StyleSheet.create({
|
|
|
513
152
|
textAlign: "center",
|
|
514
153
|
lineHeight: 22,
|
|
515
154
|
},
|
|
516
|
-
messageContainer: {
|
|
517
|
-
flexDirection: "row",
|
|
518
|
-
flexWrap: "wrap",
|
|
519
|
-
},
|
|
520
|
-
responseText: {
|
|
521
|
-
color: "rgba(255,255,255,0.95)",
|
|
522
|
-
fontSize: TYPOGRAPHY.SIZE_LG,
|
|
523
|
-
lineHeight: 22,
|
|
524
|
-
},
|
|
525
|
-
cursor: {
|
|
526
|
-
width: 2,
|
|
527
|
-
height: 18,
|
|
528
|
-
backgroundColor: COLORS.TEXT_PRIMARY,
|
|
529
|
-
marginLeft: 2,
|
|
530
|
-
opacity: 0.7,
|
|
531
|
-
},
|
|
532
|
-
resultContainer: {
|
|
533
|
-
marginTop: SPACING.SM,
|
|
534
|
-
},
|
|
535
|
-
partsContainer: {
|
|
536
|
-
// Container for interleaved parts
|
|
537
|
-
},
|
|
538
|
-
formattedContainer: {
|
|
539
|
-
flexDirection: "column",
|
|
540
|
-
},
|
|
541
|
-
listItem: {
|
|
542
|
-
flexDirection: "row",
|
|
543
|
-
marginVertical: SPACING.XS / 2,
|
|
544
|
-
paddingLeft: SPACING.SM,
|
|
545
|
-
},
|
|
546
|
-
listNumber: {
|
|
547
|
-
color: COLORS.TEXT_MUTED,
|
|
548
|
-
fontSize: TYPOGRAPHY.SIZE_LG,
|
|
549
|
-
lineHeight: 22,
|
|
550
|
-
width: 24,
|
|
551
|
-
fontWeight: TYPOGRAPHY.WEIGHT_MEDIUM,
|
|
552
|
-
},
|
|
553
|
-
listBullet: {
|
|
554
|
-
color: COLORS.TEXT_MUTED,
|
|
555
|
-
fontSize: TYPOGRAPHY.SIZE_LG,
|
|
556
|
-
lineHeight: 22,
|
|
557
|
-
width: 18,
|
|
558
|
-
},
|
|
559
|
-
listText: {
|
|
560
|
-
flex: 1,
|
|
561
|
-
color: "rgba(255,255,255,0.95)",
|
|
562
|
-
fontSize: TYPOGRAPHY.SIZE_LG,
|
|
563
|
-
lineHeight: 22,
|
|
564
|
-
},
|
|
565
|
-
codeBlock: {
|
|
566
|
-
backgroundColor: "rgba(255,255,255,0.06)",
|
|
567
|
-
borderRadius: SPACING.SM,
|
|
568
|
-
padding: SPACING.MD,
|
|
569
|
-
marginVertical: SPACING.SM,
|
|
570
|
-
},
|
|
571
|
-
codeLang: {
|
|
572
|
-
color: COLORS.TEXT_MUTED,
|
|
573
|
-
fontSize: TYPOGRAPHY.SIZE_XS,
|
|
574
|
-
fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
|
|
575
|
-
marginBottom: SPACING.XS,
|
|
576
|
-
textTransform: "uppercase",
|
|
577
|
-
},
|
|
578
|
-
codeText: {
|
|
579
|
-
color: "rgba(255,255,255,0.85)",
|
|
580
|
-
fontSize: TYPOGRAPHY.SIZE_SM,
|
|
581
|
-
fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
|
|
582
|
-
lineHeight: 18,
|
|
583
|
-
},
|
|
584
|
-
inlineCode: {
|
|
585
|
-
backgroundColor: "rgba(255,255,255,0.1)",
|
|
586
|
-
color: "rgba(255,255,255,0.9)",
|
|
587
|
-
fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
|
|
588
|
-
fontSize: TYPOGRAPHY.SIZE_MD,
|
|
589
|
-
paddingHorizontal: 4,
|
|
590
|
-
borderRadius: 3,
|
|
591
|
-
},
|
|
592
|
-
heading1: {
|
|
593
|
-
color: COLORS.TEXT_PRIMARY,
|
|
594
|
-
fontSize: TYPOGRAPHY.SIZE_XL,
|
|
595
|
-
fontWeight: TYPOGRAPHY.WEIGHT_SEMIBOLD,
|
|
596
|
-
lineHeight: 24,
|
|
597
|
-
marginTop: SPACING.MD,
|
|
598
|
-
marginBottom: SPACING.XS,
|
|
599
|
-
},
|
|
600
|
-
heading2: {
|
|
601
|
-
color: "rgba(255,255,255,0.9)",
|
|
602
|
-
fontSize: TYPOGRAPHY.SIZE_LG,
|
|
603
|
-
fontWeight: TYPOGRAPHY.WEIGHT_SEMIBOLD,
|
|
604
|
-
lineHeight: 22,
|
|
605
|
-
marginTop: SPACING.SM,
|
|
606
|
-
marginBottom: SPACING.XS,
|
|
607
|
-
},
|
|
608
|
-
heading3: {
|
|
609
|
-
color: "rgba(255,255,255,0.85)",
|
|
610
|
-
fontSize: TYPOGRAPHY.SIZE_LG,
|
|
611
|
-
fontWeight: TYPOGRAPHY.WEIGHT_MEDIUM,
|
|
612
|
-
lineHeight: 22,
|
|
613
|
-
marginTop: SPACING.SM,
|
|
614
|
-
marginBottom: SPACING.XS,
|
|
615
|
-
},
|
|
616
|
-
boldText: {
|
|
617
|
-
fontWeight: TYPOGRAPHY.WEIGHT_SEMIBOLD,
|
|
618
|
-
color: COLORS.TEXT_PRIMARY,
|
|
619
|
-
},
|
|
620
|
-
italicText: {
|
|
621
|
-
fontStyle: "italic",
|
|
622
|
-
color: "rgba(255,255,255,0.85)",
|
|
623
|
-
},
|
|
624
|
-
strikethroughText: {
|
|
625
|
-
textDecorationLine: "line-through",
|
|
626
|
-
color: COLORS.TEXT_MUTED,
|
|
627
|
-
},
|
|
628
|
-
linkText: {
|
|
629
|
-
color: COLORS.STATUS_INFO,
|
|
630
|
-
textDecorationLine: "underline",
|
|
631
|
-
},
|
|
632
|
-
blockquote: {
|
|
633
|
-
borderLeftWidth: 3,
|
|
634
|
-
borderLeftColor: "rgba(255,255,255,0.15)",
|
|
635
|
-
paddingLeft: SPACING.MD,
|
|
636
|
-
marginVertical: SPACING.XS,
|
|
637
|
-
},
|
|
638
|
-
blockquoteText: {
|
|
639
|
-
color: COLORS.TEXT_SECONDARY,
|
|
640
|
-
fontSize: TYPOGRAPHY.SIZE_LG,
|
|
641
|
-
lineHeight: 22,
|
|
642
|
-
fontStyle: "italic",
|
|
643
|
-
},
|
|
644
|
-
horizontalRule: {
|
|
645
|
-
height: 1,
|
|
646
|
-
backgroundColor: COLORS.BORDER,
|
|
647
|
-
marginVertical: SPACING.MD,
|
|
648
|
-
},
|
|
649
|
-
paragraphBreak: {
|
|
650
|
-
height: SPACING.SM,
|
|
651
|
-
},
|
|
652
|
-
interruptedText: {
|
|
653
|
-
color: COLORS.TEXT_MUTED,
|
|
654
|
-
fontSize: TYPOGRAPHY.SIZE_XS + 1, // 12px
|
|
655
|
-
fontStyle: "italic",
|
|
656
|
-
marginTop: SPACING.XS,
|
|
657
|
-
},
|
|
658
|
-
metaText: {
|
|
659
|
-
color: COLORS.TEXT_MUTED,
|
|
660
|
-
fontSize: TYPOGRAPHY.SIZE_XS + 1, // 12px
|
|
661
|
-
marginTop: SPACING.SM + 2, // 10px
|
|
662
|
-
},
|
|
663
|
-
errorContainer: {
|
|
664
|
-
backgroundColor: "rgba(255,59,48,0.15)",
|
|
665
|
-
borderRadius: SPACING.MD,
|
|
666
|
-
padding: SPACING.MD,
|
|
667
|
-
marginVertical: SPACING.SM - 2, // 6px
|
|
668
|
-
},
|
|
669
|
-
errorText: {
|
|
670
|
-
color: "#FF6B6B",
|
|
671
|
-
fontSize: TYPOGRAPHY.SIZE_MD,
|
|
672
|
-
},
|
|
673
|
-
systemContainer: {
|
|
674
|
-
backgroundColor: "rgba(255,255,255,0.05)",
|
|
675
|
-
borderRadius: SPACING.MD,
|
|
676
|
-
padding: SPACING.MD,
|
|
677
|
-
marginVertical: SPACING.SM - 2, // 6px
|
|
678
|
-
},
|
|
679
|
-
systemText: {
|
|
680
|
-
color: COLORS.TEXT_TERTIARY,
|
|
681
|
-
fontSize: TYPOGRAPHY.SIZE_MD,
|
|
682
|
-
fontStyle: "italic",
|
|
683
|
-
},
|
|
684
|
-
toolLine: {
|
|
685
|
-
flexDirection: "row",
|
|
686
|
-
alignItems: "center",
|
|
687
|
-
marginVertical: SPACING.XS,
|
|
688
|
-
},
|
|
689
|
-
toolLabel: {
|
|
690
|
-
color: COLORS.TEXT_MUTED,
|
|
691
|
-
fontSize: TYPOGRAPHY.SIZE_XS + 1, // 12px
|
|
692
|
-
fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
|
|
693
|
-
marginRight: SPACING.SM,
|
|
694
|
-
minWidth: 36,
|
|
695
|
-
},
|
|
696
|
-
toolLabelFailed: {
|
|
697
|
-
color: "rgba(255,100,100,0.6)",
|
|
698
|
-
fontSize: TYPOGRAPHY.SIZE_XS + 1, // 12px
|
|
699
|
-
fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
|
|
700
|
-
marginRight: SPACING.SM,
|
|
701
|
-
minWidth: 36,
|
|
702
|
-
},
|
|
703
|
-
toolValue: {
|
|
704
|
-
color: "rgba(255,255,255,0.7)",
|
|
705
|
-
fontSize: TYPOGRAPHY.SIZE_SM,
|
|
706
|
-
fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
|
|
707
|
-
flexShrink: 1,
|
|
708
|
-
},
|
|
709
|
-
toolValueFailed: {
|
|
710
|
-
color: "rgba(255,100,100,0.7)",
|
|
711
|
-
fontSize: TYPOGRAPHY.SIZE_SM,
|
|
712
|
-
fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
|
|
713
|
-
flexShrink: 1,
|
|
714
|
-
},
|
|
715
|
-
userPromptContainer: {
|
|
716
|
-
backgroundColor: "rgba(0,122,255,0.15)",
|
|
717
|
-
borderRadius: LAYOUT.BORDER_RADIUS_SM + 2, // 16px
|
|
718
|
-
padding: SPACING.MD,
|
|
719
|
-
marginVertical: SPACING.SM,
|
|
720
|
-
alignSelf: "flex-end",
|
|
721
|
-
maxWidth: "85%",
|
|
722
|
-
},
|
|
723
|
-
userPromptText: {
|
|
724
|
-
color: COLORS.TEXT_PRIMARY,
|
|
725
|
-
fontSize: TYPOGRAPHY.SIZE_LG,
|
|
726
|
-
lineHeight: 20,
|
|
727
|
-
},
|
|
728
|
-
userImages: {
|
|
729
|
-
flexDirection: "row",
|
|
730
|
-
flexWrap: "wrap",
|
|
731
|
-
gap: SPACING.SM,
|
|
732
|
-
marginBottom: SPACING.SM,
|
|
733
|
-
},
|
|
734
|
-
userImageThumb: {
|
|
735
|
-
width: 80,
|
|
736
|
-
height: 80,
|
|
737
|
-
borderRadius: SPACING.SM,
|
|
738
|
-
backgroundColor: "rgba(255,255,255,0.1)",
|
|
739
|
-
},
|
|
740
155
|
scrollButton: {
|
|
741
156
|
position: "absolute",
|
|
742
157
|
bottom: LAYOUT.CONTENT_PADDING_H,
|