@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,4 +1,4 @@
|
|
|
1
|
-
import React, { useState } from "react";
|
|
1
|
+
import React, { useState, useEffect, useRef } from "react";
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
Text,
|
|
@@ -6,25 +6,71 @@ import {
|
|
|
6
6
|
TouchableOpacity,
|
|
7
7
|
ScrollView,
|
|
8
8
|
TextInput,
|
|
9
|
+
Animated,
|
|
10
|
+
Easing,
|
|
9
11
|
} from "react-native";
|
|
10
12
|
import type { BranchInfo } from "../services/websocket";
|
|
11
13
|
import { SPACING, LAYOUT, COLORS, TYPOGRAPHY } from "../constants/design";
|
|
12
14
|
|
|
13
15
|
// Header height: paddingVertical (14) * 2 + content (~20) + border (1) ≈ 49
|
|
14
16
|
const HEADER_HEIGHT = 49;
|
|
17
|
+
const DEFAULT_BRANCHES = ["main", "master"];
|
|
18
|
+
const RECENT_COUNT = 5;
|
|
15
19
|
|
|
16
20
|
interface BranchSwitcherProps {
|
|
17
21
|
branches: BranchInfo[];
|
|
18
22
|
currentBranch: string;
|
|
23
|
+
loading?: boolean;
|
|
19
24
|
onSelect: (branchName: string) => void;
|
|
20
25
|
onCreate: (branchName: string) => void;
|
|
21
26
|
onClose: () => void;
|
|
22
27
|
error?: string | null;
|
|
23
28
|
}
|
|
24
29
|
|
|
30
|
+
interface BranchSection {
|
|
31
|
+
title: string;
|
|
32
|
+
branches: BranchInfo[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function groupBranches(
|
|
36
|
+
branches: BranchInfo[],
|
|
37
|
+
searchQuery: string
|
|
38
|
+
): BranchSection[] {
|
|
39
|
+
const query = searchQuery.toLowerCase().trim();
|
|
40
|
+
const filtered = query
|
|
41
|
+
? branches.filter((b) => b.name.toLowerCase().includes(query))
|
|
42
|
+
: branches;
|
|
43
|
+
|
|
44
|
+
const defaultBranch = filtered.filter((b) =>
|
|
45
|
+
DEFAULT_BRANCHES.includes(b.name)
|
|
46
|
+
);
|
|
47
|
+
const remaining = filtered.filter(
|
|
48
|
+
(b) => !DEFAULT_BRANCHES.includes(b.name)
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// Recent: local non-default branches, top N by commit date (already sorted)
|
|
52
|
+
const recentLocal = remaining.filter((b) => !b.isRemote);
|
|
53
|
+
const recent = recentLocal.slice(0, RECENT_COUNT);
|
|
54
|
+
const recentNames = new Set(recent.map((b) => b.name));
|
|
55
|
+
|
|
56
|
+
// Other: everything else
|
|
57
|
+
const other = remaining.filter((b) => !recentNames.has(b.name));
|
|
58
|
+
|
|
59
|
+
const sections: BranchSection[] = [];
|
|
60
|
+
if (defaultBranch.length > 0)
|
|
61
|
+
sections.push({ title: "Default", branches: defaultBranch });
|
|
62
|
+
if (recent.length > 0)
|
|
63
|
+
sections.push({ title: "Recent", branches: recent });
|
|
64
|
+
if (other.length > 0)
|
|
65
|
+
sections.push({ title: "Other", branches: other });
|
|
66
|
+
|
|
67
|
+
return sections;
|
|
68
|
+
}
|
|
69
|
+
|
|
25
70
|
export function BranchSwitcher({
|
|
26
71
|
branches,
|
|
27
72
|
currentBranch,
|
|
73
|
+
loading,
|
|
28
74
|
onSelect,
|
|
29
75
|
onCreate,
|
|
30
76
|
onClose,
|
|
@@ -33,6 +79,8 @@ export function BranchSwitcher({
|
|
|
33
79
|
const [showCreateInput, setShowCreateInput] = useState(false);
|
|
34
80
|
const [newBranchName, setNewBranchName] = useState("");
|
|
35
81
|
|
|
82
|
+
const sections = groupBranches(branches, "");
|
|
83
|
+
|
|
36
84
|
const handleCreate = () => {
|
|
37
85
|
const trimmed = newBranchName.trim();
|
|
38
86
|
if (trimmed) {
|
|
@@ -42,53 +90,84 @@ export function BranchSwitcher({
|
|
|
42
90
|
}
|
|
43
91
|
};
|
|
44
92
|
|
|
93
|
+
const renderBranchItem = (branch: BranchInfo, isFirst: boolean) => {
|
|
94
|
+
const isCurrent = branch.name === currentBranch;
|
|
95
|
+
return (
|
|
96
|
+
<TouchableOpacity
|
|
97
|
+
key={branch.name}
|
|
98
|
+
style={[
|
|
99
|
+
styles.branchItem,
|
|
100
|
+
isCurrent && styles.branchItemCurrent,
|
|
101
|
+
isFirst && styles.branchItemFirst,
|
|
102
|
+
]}
|
|
103
|
+
onPress={() => {
|
|
104
|
+
if (!isCurrent) {
|
|
105
|
+
onSelect(branch.name);
|
|
106
|
+
}
|
|
107
|
+
}}
|
|
108
|
+
activeOpacity={isCurrent ? 1 : 0.6}
|
|
109
|
+
>
|
|
110
|
+
<View style={styles.branchInfo}>
|
|
111
|
+
<Text
|
|
112
|
+
style={[
|
|
113
|
+
styles.branchName,
|
|
114
|
+
isCurrent && styles.branchNameCurrent,
|
|
115
|
+
]}
|
|
116
|
+
numberOfLines={1}
|
|
117
|
+
>
|
|
118
|
+
{branch.name}
|
|
119
|
+
</Text>
|
|
120
|
+
{branch.prNumber && (
|
|
121
|
+
<View style={styles.prBadge}>
|
|
122
|
+
<Text style={styles.prBadgeText}>#{branch.prNumber}</Text>
|
|
123
|
+
</View>
|
|
124
|
+
)}
|
|
125
|
+
{branch.isRemote && (
|
|
126
|
+
<View style={styles.remoteBadge}>
|
|
127
|
+
<Text style={styles.remoteBadgeText}>remote</Text>
|
|
128
|
+
</View>
|
|
129
|
+
)}
|
|
130
|
+
</View>
|
|
131
|
+
{isCurrent && <Text style={styles.currentIndicator}>✓</Text>}
|
|
132
|
+
</TouchableOpacity>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
|
|
45
136
|
return (
|
|
46
137
|
<View style={styles.overlay}>
|
|
47
|
-
<TouchableOpacity
|
|
138
|
+
<TouchableOpacity
|
|
139
|
+
style={styles.backdrop}
|
|
140
|
+
onPress={onClose}
|
|
141
|
+
activeOpacity={1}
|
|
142
|
+
/>
|
|
48
143
|
<View style={styles.dropdown}>
|
|
49
144
|
{error && (
|
|
50
145
|
<View style={styles.errorBanner}>
|
|
51
|
-
<Text style={styles.errorText} numberOfLines={2}>
|
|
146
|
+
<Text style={styles.errorText} numberOfLines={2}>
|
|
147
|
+
{error}
|
|
148
|
+
</Text>
|
|
52
149
|
</View>
|
|
53
150
|
)}
|
|
54
|
-
|
|
55
|
-
{
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
>
|
|
70
|
-
<View style={styles.branchInfo}>
|
|
71
|
-
<Text
|
|
72
|
-
style={[
|
|
73
|
-
styles.branchName,
|
|
74
|
-
branch.isCurrent && styles.branchNameCurrent,
|
|
75
|
-
]}
|
|
76
|
-
numberOfLines={1}
|
|
77
|
-
>
|
|
78
|
-
{branch.name}
|
|
79
|
-
</Text>
|
|
80
|
-
{branch.prNumber && (
|
|
81
|
-
<View style={styles.prBadge}>
|
|
82
|
-
<Text style={styles.prBadgeText}>#{branch.prNumber}</Text>
|
|
83
|
-
</View>
|
|
151
|
+
{loading && branches.length === 0 ? (
|
|
152
|
+
<View style={styles.loadingContainer}>
|
|
153
|
+
<LoadingDots />
|
|
154
|
+
</View>
|
|
155
|
+
) : (
|
|
156
|
+
<ScrollView style={styles.branchList} bounces={false}>
|
|
157
|
+
{sections.map((section) => (
|
|
158
|
+
<View key={section.title}>
|
|
159
|
+
<View style={styles.sectionHeader}>
|
|
160
|
+
<Text style={styles.sectionHeaderText}>
|
|
161
|
+
{section.title}
|
|
162
|
+
</Text>
|
|
163
|
+
</View>
|
|
164
|
+
{section.branches.map((branch, index) =>
|
|
165
|
+
renderBranchItem(branch, index === 0)
|
|
84
166
|
)}
|
|
85
167
|
</View>
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
</TouchableOpacity>
|
|
90
|
-
))}
|
|
91
|
-
</ScrollView>
|
|
168
|
+
))}
|
|
169
|
+
</ScrollView>
|
|
170
|
+
)}
|
|
92
171
|
|
|
93
172
|
<View style={styles.createSection}>
|
|
94
173
|
{showCreateInput ? (
|
|
@@ -121,7 +200,9 @@ export function BranchSwitcher({
|
|
|
121
200
|
style={styles.createButton}
|
|
122
201
|
onPress={() => setShowCreateInput(true)}
|
|
123
202
|
>
|
|
124
|
-
<Text style={styles.createButtonText}
|
|
203
|
+
<Text style={styles.createButtonText}>
|
|
204
|
+
+ New branch from main
|
|
205
|
+
</Text>
|
|
125
206
|
</TouchableOpacity>
|
|
126
207
|
)}
|
|
127
208
|
</View>
|
|
@@ -130,6 +211,52 @@ export function BranchSwitcher({
|
|
|
130
211
|
);
|
|
131
212
|
}
|
|
132
213
|
|
|
214
|
+
function LoadingDots() {
|
|
215
|
+
const dot1 = useRef(new Animated.Value(0.3)).current;
|
|
216
|
+
const dot2 = useRef(new Animated.Value(0.3)).current;
|
|
217
|
+
const dot3 = useRef(new Animated.Value(0.3)).current;
|
|
218
|
+
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
const animate = (dot: Animated.Value, delay: number) =>
|
|
221
|
+
Animated.loop(
|
|
222
|
+
Animated.sequence([
|
|
223
|
+
Animated.delay(delay),
|
|
224
|
+
Animated.timing(dot, {
|
|
225
|
+
toValue: 1,
|
|
226
|
+
duration: 300,
|
|
227
|
+
easing: Easing.inOut(Easing.ease),
|
|
228
|
+
useNativeDriver: true,
|
|
229
|
+
}),
|
|
230
|
+
Animated.timing(dot, {
|
|
231
|
+
toValue: 0.3,
|
|
232
|
+
duration: 300,
|
|
233
|
+
easing: Easing.inOut(Easing.ease),
|
|
234
|
+
useNativeDriver: true,
|
|
235
|
+
}),
|
|
236
|
+
]),
|
|
237
|
+
);
|
|
238
|
+
const a1 = animate(dot1, 0);
|
|
239
|
+
const a2 = animate(dot2, 150);
|
|
240
|
+
const a3 = animate(dot3, 300);
|
|
241
|
+
a1.start();
|
|
242
|
+
a2.start();
|
|
243
|
+
a3.start();
|
|
244
|
+
return () => {
|
|
245
|
+
a1.stop();
|
|
246
|
+
a2.stop();
|
|
247
|
+
a3.stop();
|
|
248
|
+
};
|
|
249
|
+
}, [dot1, dot2, dot3]);
|
|
250
|
+
|
|
251
|
+
return (
|
|
252
|
+
<View style={styles.loadingDots}>
|
|
253
|
+
{[dot1, dot2, dot3].map((dot, i) => (
|
|
254
|
+
<Animated.View key={i} style={[styles.loadingDot, { opacity: dot }]} />
|
|
255
|
+
))}
|
|
256
|
+
</View>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
133
260
|
const styles = StyleSheet.create({
|
|
134
261
|
overlay: {
|
|
135
262
|
...StyleSheet.absoluteFillObject,
|
|
@@ -151,9 +278,36 @@ const styles = StyleSheet.create({
|
|
|
151
278
|
borderColor: "rgba(255,255,255,0.12)",
|
|
152
279
|
overflow: "hidden",
|
|
153
280
|
},
|
|
281
|
+
loadingContainer: {
|
|
282
|
+
paddingVertical: SPACING.XL,
|
|
283
|
+
alignItems: "center",
|
|
284
|
+
justifyContent: "center",
|
|
285
|
+
},
|
|
286
|
+
loadingDots: {
|
|
287
|
+
flexDirection: "row",
|
|
288
|
+
gap: 6,
|
|
289
|
+
},
|
|
290
|
+
loadingDot: {
|
|
291
|
+
width: 6,
|
|
292
|
+
height: 6,
|
|
293
|
+
borderRadius: 3,
|
|
294
|
+
backgroundColor: "rgba(255,255,255,0.5)",
|
|
295
|
+
},
|
|
154
296
|
branchList: {
|
|
155
297
|
maxHeight: 300,
|
|
156
298
|
},
|
|
299
|
+
sectionHeader: {
|
|
300
|
+
paddingHorizontal: SPACING.LG,
|
|
301
|
+
paddingTop: SPACING.MD,
|
|
302
|
+
paddingBottom: SPACING.XS,
|
|
303
|
+
},
|
|
304
|
+
sectionHeaderText: {
|
|
305
|
+
color: COLORS.TEXT_MUTED,
|
|
306
|
+
fontSize: TYPOGRAPHY.SIZE_XS,
|
|
307
|
+
fontWeight: TYPOGRAPHY.WEIGHT_SEMIBOLD,
|
|
308
|
+
textTransform: "uppercase",
|
|
309
|
+
letterSpacing: 0.5,
|
|
310
|
+
},
|
|
157
311
|
branchItem: {
|
|
158
312
|
flexDirection: "row",
|
|
159
313
|
alignItems: "center",
|
|
@@ -196,6 +350,17 @@ const styles = StyleSheet.create({
|
|
|
196
350
|
fontSize: TYPOGRAPHY.SIZE_XS,
|
|
197
351
|
fontWeight: TYPOGRAPHY.WEIGHT_MEDIUM,
|
|
198
352
|
},
|
|
353
|
+
remoteBadge: {
|
|
354
|
+
backgroundColor: "rgba(255,255,255,0.06)",
|
|
355
|
+
paddingHorizontal: SPACING.SM,
|
|
356
|
+
paddingVertical: 2,
|
|
357
|
+
borderRadius: 8,
|
|
358
|
+
},
|
|
359
|
+
remoteBadgeText: {
|
|
360
|
+
color: COLORS.TEXT_MUTED,
|
|
361
|
+
fontSize: TYPOGRAPHY.SIZE_XS,
|
|
362
|
+
fontWeight: TYPOGRAPHY.WEIGHT_NORMAL,
|
|
363
|
+
},
|
|
199
364
|
currentIndicator: {
|
|
200
365
|
color: COLORS.STATUS_SUCCESS,
|
|
201
366
|
fontSize: TYPOGRAPHY.SIZE_SM,
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View, Text, StyleSheet, Platform, Linking } from "react-native";
|
|
3
|
+
import { SPACING, COLORS, TYPOGRAPHY } from "../constants/design";
|
|
4
|
+
|
|
5
|
+
// Renders formatted text with markdown-style lists, code, and emphasis
|
|
6
|
+
export function FormattedText({ content, isStreaming }: { content: string; isStreaming?: boolean }) {
|
|
7
|
+
const lines = content.split('\n');
|
|
8
|
+
const elements: React.ReactNode[] = [];
|
|
9
|
+
let i = 0;
|
|
10
|
+
|
|
11
|
+
while (i < lines.length) {
|
|
12
|
+
const line = lines[i];
|
|
13
|
+
const trimmed = line.trim();
|
|
14
|
+
|
|
15
|
+
// Code block start (check first to avoid matching content inside code blocks)
|
|
16
|
+
if (trimmed.startsWith('```')) {
|
|
17
|
+
const codeLines: string[] = [];
|
|
18
|
+
const lang = trimmed.slice(3).trim();
|
|
19
|
+
i++;
|
|
20
|
+
while (i < lines.length && !lines[i].trim().startsWith('```')) {
|
|
21
|
+
codeLines.push(lines[i]);
|
|
22
|
+
i++;
|
|
23
|
+
}
|
|
24
|
+
elements.push(
|
|
25
|
+
<View key={`code-${i}`} style={styles.codeBlock}>
|
|
26
|
+
{lang && <Text style={styles.codeLang}>{lang}</Text>}
|
|
27
|
+
<Text style={styles.codeText} selectable>{codeLines.join('\n')}</Text>
|
|
28
|
+
</View>
|
|
29
|
+
);
|
|
30
|
+
i++; // skip closing ```
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Heading (# ## ### etc.)
|
|
35
|
+
const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
|
|
36
|
+
if (headingMatch) {
|
|
37
|
+
const level = headingMatch[1].length;
|
|
38
|
+
const headingStyle = level <= 2 ? styles.heading1 : level <= 4 ? styles.heading2 : styles.heading3;
|
|
39
|
+
elements.push(
|
|
40
|
+
<Text key={i} style={headingStyle} selectable>{formatInlineText(headingMatch[2])}</Text>
|
|
41
|
+
);
|
|
42
|
+
i++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Blockquote (> text) - collect consecutive > lines
|
|
47
|
+
if (trimmed.startsWith('> ') || trimmed === '>') {
|
|
48
|
+
const quoteLines: string[] = [];
|
|
49
|
+
while (i < lines.length && (lines[i].trim().startsWith('> ') || lines[i].trim() === '>')) {
|
|
50
|
+
const qContent = lines[i].trim().startsWith('> ') ? lines[i].trim().slice(2) : '';
|
|
51
|
+
quoteLines.push(qContent);
|
|
52
|
+
i++;
|
|
53
|
+
}
|
|
54
|
+
elements.push(
|
|
55
|
+
<View key={`quote-${i}`} style={styles.blockquote}>
|
|
56
|
+
<Text style={styles.blockquoteText} selectable>{formatInlineText(quoteLines.join('\n'))}</Text>
|
|
57
|
+
</View>
|
|
58
|
+
);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Task list item (- [ ] or - [x])
|
|
63
|
+
const taskMatch = trimmed.match(/^[-*]\s+\[([ xX])\]\s+(.+)$/);
|
|
64
|
+
if (taskMatch) {
|
|
65
|
+
const checked = taskMatch[1] !== ' ';
|
|
66
|
+
elements.push(
|
|
67
|
+
<View key={i} style={styles.listItem}>
|
|
68
|
+
<Text style={styles.listBullet}>{checked ? '☑' : '☐'}</Text>
|
|
69
|
+
<Text style={styles.listText} selectable>{formatInlineText(taskMatch[2])}</Text>
|
|
70
|
+
</View>
|
|
71
|
+
);
|
|
72
|
+
i++;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Numbered list item (1. 2. 3.) - with nesting via indentation
|
|
77
|
+
const numberedMatch = line.match(/^(\s*)(\d+)\.\s+(.+)$/);
|
|
78
|
+
if (numberedMatch) {
|
|
79
|
+
const indent = Math.floor(numberedMatch[1].length / 2);
|
|
80
|
+
const [, , num, text] = numberedMatch;
|
|
81
|
+
elements.push(
|
|
82
|
+
<View key={i} style={[styles.listItem, indent > 0 && { paddingLeft: SPACING.SM + indent * SPACING.LG }]}>
|
|
83
|
+
<Text style={styles.listNumber}>{num}.</Text>
|
|
84
|
+
<Text style={styles.listText} selectable>{formatInlineText(text)}</Text>
|
|
85
|
+
</View>
|
|
86
|
+
);
|
|
87
|
+
i++;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Bullet list item (- or *) - with nesting via indentation
|
|
92
|
+
const bulletMatch = line.match(/^(\s*)[-*]\s+(.+)$/);
|
|
93
|
+
if (bulletMatch) {
|
|
94
|
+
const indent = Math.floor(bulletMatch[1].length / 2);
|
|
95
|
+
const bulletChar = indent === 0 ? '•' : indent === 1 ? '◦' : '▪';
|
|
96
|
+
elements.push(
|
|
97
|
+
<View key={i} style={[styles.listItem, indent > 0 && { paddingLeft: SPACING.SM + indent * SPACING.LG }]}>
|
|
98
|
+
<Text style={styles.listBullet}>{bulletChar}</Text>
|
|
99
|
+
<Text style={styles.listText} selectable>{formatInlineText(bulletMatch[2])}</Text>
|
|
100
|
+
</View>
|
|
101
|
+
);
|
|
102
|
+
i++;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Horizontal rule (---, ***, ___)
|
|
107
|
+
if (/^[-*_]{3,}$/.test(trimmed)) {
|
|
108
|
+
elements.push(<View key={i} style={styles.horizontalRule} />);
|
|
109
|
+
i++;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Empty line = paragraph break
|
|
114
|
+
if (!trimmed) {
|
|
115
|
+
elements.push(<View key={i} style={styles.paragraphBreak} />);
|
|
116
|
+
i++;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Regular text
|
|
121
|
+
elements.push(
|
|
122
|
+
<Text key={i} style={styles.responseText} selectable>{formatInlineText(line)}</Text>
|
|
123
|
+
);
|
|
124
|
+
i++;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<View style={styles.formattedContainer}>
|
|
129
|
+
{elements}
|
|
130
|
+
{isStreaming && <View style={styles.cursor} />}
|
|
131
|
+
</View>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Format inline elements: code spans first, then links, then emphasis
|
|
136
|
+
function formatInlineText(text: string): React.ReactNode {
|
|
137
|
+
// Split on inline code first to avoid processing markdown inside code spans
|
|
138
|
+
const codeParts = text.split(/(`[^`]+`)/g);
|
|
139
|
+
if (codeParts.length === 1) {
|
|
140
|
+
return formatLinks(text);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return codeParts.map((part, i) => {
|
|
144
|
+
if (part.startsWith('`') && part.endsWith('`')) {
|
|
145
|
+
return <Text key={i} style={styles.inlineCode}>{part.slice(1, -1)}</Text>;
|
|
146
|
+
}
|
|
147
|
+
return <React.Fragment key={i}>{formatLinks(part)}</React.Fragment>;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Format markdown links [text](url)
|
|
152
|
+
function formatLinks(text: string): React.ReactNode {
|
|
153
|
+
const parts = text.split(/(\[[^\]]+\]\([^)]+\))/g);
|
|
154
|
+
if (parts.length === 1) return formatEmphasis(text);
|
|
155
|
+
|
|
156
|
+
return parts.map((part, i) => {
|
|
157
|
+
const linkMatch = part.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
|
|
158
|
+
if (linkMatch) {
|
|
159
|
+
return (
|
|
160
|
+
<Text
|
|
161
|
+
key={i}
|
|
162
|
+
style={styles.linkText}
|
|
163
|
+
onPress={() => Linking.openURL(linkMatch[2])}
|
|
164
|
+
>
|
|
165
|
+
{linkMatch[1]}
|
|
166
|
+
</Text>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
return <React.Fragment key={i}>{formatEmphasis(part)}</React.Fragment>;
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Format bold, italic, and strikethrough emphasis
|
|
174
|
+
function formatEmphasis(text: string): React.ReactNode {
|
|
175
|
+
// Match **bold**, ~~strikethrough~~, *italic*, and plain text segments
|
|
176
|
+
const parts = text.split(/(\*\*[^*]+\*\*|~~[^~]+~~|\*[^*]+\*)/g);
|
|
177
|
+
if (parts.length === 1) return text;
|
|
178
|
+
|
|
179
|
+
return parts.map((part, i) => {
|
|
180
|
+
if (part.startsWith('**') && part.endsWith('**')) {
|
|
181
|
+
return <Text key={i} style={styles.boldText}>{part.slice(2, -2)}</Text>;
|
|
182
|
+
}
|
|
183
|
+
if (part.startsWith('~~') && part.endsWith('~~')) {
|
|
184
|
+
return <Text key={i} style={styles.strikethroughText}>{part.slice(2, -2)}</Text>;
|
|
185
|
+
}
|
|
186
|
+
if (part.startsWith('*') && part.endsWith('*')) {
|
|
187
|
+
return <Text key={i} style={styles.italicText}>{part.slice(1, -1)}</Text>;
|
|
188
|
+
}
|
|
189
|
+
return part;
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const styles = StyleSheet.create({
|
|
194
|
+
formattedContainer: {
|
|
195
|
+
flexDirection: "column",
|
|
196
|
+
},
|
|
197
|
+
responseText: {
|
|
198
|
+
color: "rgba(255,255,255,0.95)",
|
|
199
|
+
fontSize: TYPOGRAPHY.SIZE_LG,
|
|
200
|
+
lineHeight: 22,
|
|
201
|
+
},
|
|
202
|
+
cursor: {
|
|
203
|
+
width: 2,
|
|
204
|
+
height: 18,
|
|
205
|
+
backgroundColor: COLORS.TEXT_PRIMARY,
|
|
206
|
+
marginLeft: 2,
|
|
207
|
+
opacity: 0.7,
|
|
208
|
+
},
|
|
209
|
+
listItem: {
|
|
210
|
+
flexDirection: "row",
|
|
211
|
+
marginVertical: SPACING.XS / 2,
|
|
212
|
+
paddingLeft: SPACING.SM,
|
|
213
|
+
},
|
|
214
|
+
listNumber: {
|
|
215
|
+
color: COLORS.TEXT_MUTED,
|
|
216
|
+
fontSize: TYPOGRAPHY.SIZE_LG,
|
|
217
|
+
lineHeight: 22,
|
|
218
|
+
width: 24,
|
|
219
|
+
fontWeight: TYPOGRAPHY.WEIGHT_MEDIUM,
|
|
220
|
+
},
|
|
221
|
+
listBullet: {
|
|
222
|
+
color: COLORS.TEXT_MUTED,
|
|
223
|
+
fontSize: TYPOGRAPHY.SIZE_LG,
|
|
224
|
+
lineHeight: 22,
|
|
225
|
+
width: 18,
|
|
226
|
+
},
|
|
227
|
+
listText: {
|
|
228
|
+
flex: 1,
|
|
229
|
+
color: "rgba(255,255,255,0.95)",
|
|
230
|
+
fontSize: TYPOGRAPHY.SIZE_LG,
|
|
231
|
+
lineHeight: 22,
|
|
232
|
+
},
|
|
233
|
+
codeBlock: {
|
|
234
|
+
backgroundColor: "rgba(255,255,255,0.06)",
|
|
235
|
+
borderRadius: SPACING.SM,
|
|
236
|
+
padding: SPACING.MD,
|
|
237
|
+
marginVertical: SPACING.SM,
|
|
238
|
+
},
|
|
239
|
+
codeLang: {
|
|
240
|
+
color: COLORS.TEXT_MUTED,
|
|
241
|
+
fontSize: TYPOGRAPHY.SIZE_XS,
|
|
242
|
+
fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
|
|
243
|
+
marginBottom: SPACING.XS,
|
|
244
|
+
textTransform: "uppercase",
|
|
245
|
+
},
|
|
246
|
+
codeText: {
|
|
247
|
+
color: "rgba(255,255,255,0.85)",
|
|
248
|
+
fontSize: TYPOGRAPHY.SIZE_SM,
|
|
249
|
+
fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
|
|
250
|
+
lineHeight: 18,
|
|
251
|
+
},
|
|
252
|
+
inlineCode: {
|
|
253
|
+
backgroundColor: "rgba(255,255,255,0.1)",
|
|
254
|
+
color: "rgba(255,255,255,0.9)",
|
|
255
|
+
fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
|
|
256
|
+
fontSize: TYPOGRAPHY.SIZE_MD,
|
|
257
|
+
paddingHorizontal: 4,
|
|
258
|
+
borderRadius: 3,
|
|
259
|
+
},
|
|
260
|
+
heading1: {
|
|
261
|
+
color: COLORS.TEXT_PRIMARY,
|
|
262
|
+
fontSize: TYPOGRAPHY.SIZE_XL,
|
|
263
|
+
fontWeight: TYPOGRAPHY.WEIGHT_SEMIBOLD,
|
|
264
|
+
lineHeight: 24,
|
|
265
|
+
marginTop: SPACING.MD,
|
|
266
|
+
marginBottom: SPACING.XS,
|
|
267
|
+
},
|
|
268
|
+
heading2: {
|
|
269
|
+
color: "rgba(255,255,255,0.9)",
|
|
270
|
+
fontSize: TYPOGRAPHY.SIZE_LG,
|
|
271
|
+
fontWeight: TYPOGRAPHY.WEIGHT_SEMIBOLD,
|
|
272
|
+
lineHeight: 22,
|
|
273
|
+
marginTop: SPACING.SM,
|
|
274
|
+
marginBottom: SPACING.XS,
|
|
275
|
+
},
|
|
276
|
+
heading3: {
|
|
277
|
+
color: "rgba(255,255,255,0.85)",
|
|
278
|
+
fontSize: TYPOGRAPHY.SIZE_LG,
|
|
279
|
+
fontWeight: TYPOGRAPHY.WEIGHT_MEDIUM,
|
|
280
|
+
lineHeight: 22,
|
|
281
|
+
marginTop: SPACING.SM,
|
|
282
|
+
marginBottom: SPACING.XS,
|
|
283
|
+
},
|
|
284
|
+
boldText: {
|
|
285
|
+
fontWeight: TYPOGRAPHY.WEIGHT_SEMIBOLD,
|
|
286
|
+
color: COLORS.TEXT_PRIMARY,
|
|
287
|
+
},
|
|
288
|
+
italicText: {
|
|
289
|
+
fontStyle: "italic",
|
|
290
|
+
color: "rgba(255,255,255,0.85)",
|
|
291
|
+
},
|
|
292
|
+
strikethroughText: {
|
|
293
|
+
textDecorationLine: "line-through",
|
|
294
|
+
color: COLORS.TEXT_MUTED,
|
|
295
|
+
},
|
|
296
|
+
linkText: {
|
|
297
|
+
color: COLORS.STATUS_INFO,
|
|
298
|
+
textDecorationLine: "underline",
|
|
299
|
+
},
|
|
300
|
+
blockquote: {
|
|
301
|
+
borderLeftWidth: 3,
|
|
302
|
+
borderLeftColor: "rgba(255,255,255,0.15)",
|
|
303
|
+
paddingLeft: SPACING.MD,
|
|
304
|
+
marginVertical: SPACING.XS,
|
|
305
|
+
},
|
|
306
|
+
blockquoteText: {
|
|
307
|
+
color: COLORS.TEXT_SECONDARY,
|
|
308
|
+
fontSize: TYPOGRAPHY.SIZE_LG,
|
|
309
|
+
lineHeight: 22,
|
|
310
|
+
fontStyle: "italic",
|
|
311
|
+
},
|
|
312
|
+
horizontalRule: {
|
|
313
|
+
height: 1,
|
|
314
|
+
backgroundColor: COLORS.BORDER,
|
|
315
|
+
marginVertical: SPACING.MD,
|
|
316
|
+
},
|
|
317
|
+
paragraphBreak: {
|
|
318
|
+
height: SPACING.SM,
|
|
319
|
+
},
|
|
320
|
+
});
|