@alia.onl/sdk 15.7.0 → 19.0.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/CHANGELOG.md +255 -12
- package/README.md +241 -155
- package/package.json +17 -5
- package/src/animations.ts +0 -1
- package/src/components/AliaAskProvider.tsx +127 -0
- package/src/components/AliaChatContent.tsx +604 -392
- package/src/components/AliaChatHeader.tsx +14 -38
- package/src/components/AliaChatScreen.tsx +56 -31
- package/src/components/AliaChatSheet.tsx +110 -36
- package/src/components/AliaComposer.tsx +1 -1
- package/src/components/AliaParticipants.tsx +202 -0
- package/src/components/AliaParticipantsHeader.tsx +60 -0
- package/src/components/AliaSharedConversation.tsx +86 -0
- package/src/components/AliaWelcomeMessage.tsx +18 -5
- package/src/components/Image.tsx +12 -0
- package/src/components/Markdown.tsx +498 -14
- package/src/components/PlanPreviewCard.tsx +7 -49
- package/src/components/ReplyMarkdown.tsx +359 -0
- package/src/components/RichBlocks.tsx +195 -0
- package/src/components/canvas/CanvasComponent.tsx +59 -0
- package/src/components/canvas/ChartRenderer.tsx +73 -0
- package/src/components/canvas/CodeRenderer.tsx +24 -0
- package/src/components/canvas/FormRenderer.tsx +101 -0
- package/src/components/canvas/MarkdownRenderer.tsx +14 -0
- package/src/components/canvas/TableRenderer.tsx +47 -0
- package/src/components/thought/FilesAndSources.tsx +120 -0
- package/src/components/thought/ThoughtPanel.tsx +417 -0
- package/src/components/thought/ToolStep.tsx +141 -0
- package/src/components/thread/AgentResultCard.tsx +125 -0
- package/src/components/thread/AgentTaskCard.tsx +127 -0
- package/src/components/thread/AliaThread.tsx +401 -0
- package/src/components/thread/MessageBlockBoundary.tsx +78 -0
- package/src/components/thread/NewConversationOffer.tsx +54 -0
- package/src/components/thread/ThreadRow.tsx +458 -0
- package/src/components/thread/ThreadSearch.tsx +129 -0
- package/src/components/thread/ToolResultCard.tsx +254 -0
- package/src/components/thread/TurnMenu.tsx +54 -0
- package/src/components/thread/TurnStatusLine.tsx +34 -0
- package/src/components/voice/AliaVoiceCall.tsx +21 -0
- package/src/components/voice/VoiceControls.tsx +2 -28
- package/src/components/voice/VoiceSession.tsx +39 -11
- package/src/csp.ts +50 -0
- package/src/hooks/useAliaActivity.ts +3 -3
- package/src/hooks/useAtBottom.ts +51 -0
- package/src/hooks/useAttachMenu.ts +94 -0
- package/src/hooks/useChatComposer.ts +299 -0
- package/src/hooks/useChatVoice.ts +129 -0
- package/src/hooks/useDictation.ts +2 -3
- package/src/hooks/useDocumentPicker.ts +3 -29
- package/src/hooks/useFailedTurnToast.ts +43 -0
- package/src/hooks/useMessageVote.ts +50 -0
- package/src/hooks/useOpenThought.ts +79 -0
- package/src/hooks/usePendingQuestions.ts +2 -2
- package/src/hooks/useReadAloud.ts +92 -0
- package/src/hooks/useShareHandler.ts +24 -0
- package/src/hooks/useSoundEffects.ts +1 -1
- package/src/hooks/useTTS.ts +2 -3
- package/src/hooks/useThoughtScope.ts +59 -0
- package/src/hooks/useThreadRows.ts +145 -0
- package/src/hooks/useThreadView.ts +68 -0
- package/src/hooks/useTimelineWindow.ts +256 -0
- package/src/hooks/useVoiceLauncher.ts +180 -0
- package/src/hooks/useVoiceRoom.ts +2 -6
- package/src/index.ts +43 -29
- package/src/lib/activity-store.ts +1 -1
- package/src/lib/add-menu.ts +7 -0
- package/src/lib/call-thread.ts +33 -0
- package/src/lib/character-runtime.ts +6 -4
- package/src/lib/citations.ts +79 -2
- package/src/lib/clipboard.ts +23 -0
- package/src/lib/content-utils.ts +10 -0
- package/src/lib/copy-en.ts +284 -0
- package/src/lib/copy.ts +32 -0
- package/src/lib/document-picker.ts +31 -0
- package/src/lib/flights.ts +22 -13
- package/src/lib/focus-return.ts +66 -0
- package/src/lib/keyboard.native.tsx +19 -0
- package/src/lib/keyboard.tsx +9 -0
- package/src/lib/labels.tsx +69 -17
- package/src/lib/origins.ts +15 -0
- package/src/lib/participants.ts +86 -0
- package/src/lib/power-level-options.ts +66 -0
- package/src/lib/questions.ts +2 -2
- package/src/lib/task-labels.ts +4 -4
- package/src/lib/tool-call-status.ts +1 -1
- package/src/lib/tool-icons.ts +36 -0
- package/src/lib/tool-renderers.tsx +48 -0
- package/src/lib/turn-state.ts +2 -2
- package/src/lib/voice-audio-engine.ts +14 -158
- package/src/lib/voice-call-request.ts +49 -0
- package/src/lib/voice-call.ts +76 -34
- package/src/lib/voice-controls-labels.ts +31 -0
- package/src/lib/voice-protocol.ts +32 -14
- package/src/lib/voice-worklets.ts +19 -0
- package/src/lib/work-log.ts +6 -6
- package/src/runtime/agents.ts +151 -0
- package/src/runtime/api.ts +123 -0
- package/src/runtime/attachment-utils.ts +96 -0
- package/src/runtime/composer-draft-store.ts +270 -0
- package/src/runtime/config.tsx +186 -0
- package/src/runtime/conversation-store.ts +49 -0
- package/src/runtime/conversations.ts +557 -0
- package/src/runtime/error-utils.ts +104 -0
- package/src/runtime/host-tools.ts +140 -0
- package/src/runtime/index.ts +113 -0
- package/src/runtime/message-history.ts +124 -0
- package/src/runtime/model.ts +39 -0
- package/src/runtime/oxy-files.ts +37 -0
- package/src/runtime/power-levels.ts +66 -0
- package/src/runtime/query-keys.ts +34 -0
- package/src/runtime/random-uuid.ts +25 -0
- package/src/runtime/routes.ts +51 -0
- package/src/runtime/sharing.ts +165 -0
- package/src/runtime/sse-frame-reader.ts +88 -0
- package/src/runtime/stream-events.ts +146 -0
- package/src/runtime/stream-outcome.ts +21 -0
- package/src/runtime/thought-store.ts +103 -0
- package/src/runtime/thread/agent-activity.ts +261 -0
- package/src/runtime/thread/automation-labels.ts +79 -0
- package/src/runtime/thread/conversation-export.ts +135 -0
- package/src/runtime/thread/message-days.ts +120 -0
- package/src/runtime/thread/message-processor.ts +93 -0
- package/src/runtime/thread/task-utils.ts +11 -0
- package/src/runtime/thread/thought-utils.ts +752 -0
- package/src/runtime/thread/thread-history.ts +139 -0
- package/src/runtime/thread/timeline.ts +202 -0
- package/src/runtime/thread/title-tags.ts +8 -0
- package/src/runtime/thread/tool-cards.ts +383 -0
- package/src/runtime/turn-failure.ts +108 -0
- package/src/runtime/types.ts +148 -0
- package/src/runtime/usage-limit-error.ts +44 -0
- package/src/runtime/use-agent-activity.ts +80 -0
- package/src/runtime/use-conversation-stream.ts +1427 -0
- package/src/runtime/use-conversation-thread.ts +556 -0
- package/src/runtime/use-start-conversation.ts +107 -0
- package/src/runtime/use-thread-history.ts +64 -0
- package/src/runtime/use-thread-search.ts +95 -0
- package/src/runtime/use-turn-edit.ts +99 -0
- package/src/tools.ts +2 -2
- package/src/types.ts +18 -47
- package/worklets/voice-processors.js +152 -0
- package/src/components/AliaChatMessageList.tsx +0 -532
- package/src/components/ThinkingBlock.tsx +0 -43
- package/src/hooks/useAliaChat.ts +0 -573
- package/src/lib/api-url.ts +0 -2
- package/src/lib/catalogue.ts +0 -236
- package/src/lib/chat-stream.ts +0 -655
- package/src/lib/chat-transport.ts +0 -98
- package/src/lib/delegation.ts +0 -34
- package/src/lib/work-log-copy.ts +0 -61
|
@@ -1,31 +1,515 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import React, { useMemo, type ReactNode } from 'react';
|
|
2
|
+
import { View, Text, Platform, Linking } from 'react-native';
|
|
3
|
+
import { useTheme } from '@oxy.so/bloom/theme';
|
|
4
|
+
import Markdown, { MarkdownIt, type ASTNode } from 'react-native-markdown-display';
|
|
5
|
+
import { CodeBlock } from '@oxy.so/bloom/code';
|
|
6
|
+
import { fontFamilies } from '@oxy.so/bloom/fonts';
|
|
7
|
+
import type { AliaColors } from '../theme';
|
|
3
8
|
|
|
4
|
-
|
|
5
|
-
|
|
9
|
+
// Hardcoded fallback colors for standalone SDK usage (when no color override is passed).
|
|
10
|
+
// The main app always passes resolved colors via the `colors` prop.
|
|
11
|
+
const BODY_TEXT = { fontSize: 16, lineHeight: 28 } as const;
|
|
12
|
+
const HEADING_TEXT = { fontSize: 16, lineHeight: 22 } as const;
|
|
13
|
+
const MONO_FONT = Platform.select({ ios: 'Courier', android: 'monospace', default: 'monospace' })!;
|
|
6
14
|
|
|
15
|
+
function createRules(colors: AliaColors, renderCodeBlock?: RenderCodeBlock) {
|
|
16
|
+
const { text: textColor, muted, border, primary, mutedForeground } = colors;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Fenced and indented code: Bloom's `CodeBlock` unless the host brings a
|
|
20
|
+
* code card of its own.
|
|
21
|
+
*/
|
|
22
|
+
const drawCodeBlock: RenderCodeBlock =
|
|
23
|
+
renderCodeBlock ?? ((code, language) => <CodeBlock code={code} language={language} />);
|
|
24
|
+
const codeRules = {
|
|
25
|
+
fence: (node: ASTNode) => (
|
|
26
|
+
<View key={node.key} style={{ marginVertical: 8 }}>
|
|
27
|
+
{drawCodeBlock(node.content.replace(/\n$/, ''), codeLanguage(node))}
|
|
28
|
+
</View>
|
|
29
|
+
),
|
|
30
|
+
code_block: (node: ASTNode) => (
|
|
31
|
+
<View key={node.key} style={{ marginVertical: 8 }}>
|
|
32
|
+
{drawCodeBlock(node.content.replace(/\n$/, ''), undefined)}
|
|
33
|
+
</View>
|
|
34
|
+
),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
...codeRules,
|
|
39
|
+
heading1: (node: ASTNode, children: ReactNode[]) => (
|
|
40
|
+
<Text
|
|
41
|
+
key={node.key}
|
|
42
|
+
style={{
|
|
43
|
+
marginBottom: 8,
|
|
44
|
+
marginTop: 12,
|
|
45
|
+
fontSize: 18,
|
|
46
|
+
fontWeight: '600',
|
|
47
|
+
lineHeight: 24,
|
|
48
|
+
color: textColor,
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
{children}
|
|
52
|
+
</Text>
|
|
53
|
+
),
|
|
54
|
+
heading2: (node: ASTNode, children: ReactNode[]) => (
|
|
55
|
+
<Text
|
|
56
|
+
key={node.key}
|
|
57
|
+
style={{
|
|
58
|
+
marginBottom: 8,
|
|
59
|
+
marginTop: 12,
|
|
60
|
+
...HEADING_TEXT,
|
|
61
|
+
fontWeight: '600',
|
|
62
|
+
color: textColor,
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
{children}
|
|
66
|
+
</Text>
|
|
67
|
+
),
|
|
68
|
+
heading3: (node: ASTNode, children: ReactNode[]) => (
|
|
69
|
+
<Text
|
|
70
|
+
key={node.key}
|
|
71
|
+
style={{
|
|
72
|
+
marginBottom: 6,
|
|
73
|
+
marginTop: 8,
|
|
74
|
+
...HEADING_TEXT,
|
|
75
|
+
fontWeight: '600',
|
|
76
|
+
color: textColor,
|
|
77
|
+
}}
|
|
78
|
+
>
|
|
79
|
+
{children}
|
|
80
|
+
</Text>
|
|
81
|
+
),
|
|
82
|
+
heading4: (node: ASTNode, children: ReactNode[]) => (
|
|
83
|
+
<Text
|
|
84
|
+
key={node.key}
|
|
85
|
+
style={{
|
|
86
|
+
marginBottom: 6,
|
|
87
|
+
marginTop: 8,
|
|
88
|
+
...HEADING_TEXT,
|
|
89
|
+
fontWeight: '500',
|
|
90
|
+
color: textColor,
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
93
|
+
{children}
|
|
94
|
+
</Text>
|
|
95
|
+
),
|
|
96
|
+
heading5: (node: ASTNode, children: ReactNode[]) => (
|
|
97
|
+
<Text
|
|
98
|
+
key={node.key}
|
|
99
|
+
style={{
|
|
100
|
+
marginBottom: 4,
|
|
101
|
+
marginTop: 8,
|
|
102
|
+
...HEADING_TEXT,
|
|
103
|
+
fontWeight: '500',
|
|
104
|
+
color: textColor,
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
{children}
|
|
108
|
+
</Text>
|
|
109
|
+
),
|
|
110
|
+
heading6: (node: ASTNode, children: ReactNode[]) => (
|
|
111
|
+
<Text
|
|
112
|
+
key={node.key}
|
|
113
|
+
style={{
|
|
114
|
+
marginBottom: 4,
|
|
115
|
+
marginTop: 8,
|
|
116
|
+
...HEADING_TEXT,
|
|
117
|
+
fontWeight: '500',
|
|
118
|
+
color: textColor,
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
{children}
|
|
122
|
+
</Text>
|
|
123
|
+
),
|
|
124
|
+
code: (node: ASTNode, children: ReactNode[], parent: ASTNode[]) => {
|
|
125
|
+
return parent.length > 1 ? (
|
|
126
|
+
<View
|
|
127
|
+
key={node.key}
|
|
128
|
+
style={{
|
|
129
|
+
marginVertical: 8,
|
|
130
|
+
overflow: 'hidden',
|
|
131
|
+
borderRadius: 6,
|
|
132
|
+
backgroundColor: muted,
|
|
133
|
+
borderWidth: 1,
|
|
134
|
+
borderColor: border,
|
|
135
|
+
padding: 12,
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
<Text style={{ fontSize: 13, fontFamily: MONO_FONT }}>{children}</Text>
|
|
139
|
+
</View>
|
|
140
|
+
) : (
|
|
141
|
+
<Text
|
|
142
|
+
key={node.key}
|
|
143
|
+
style={{
|
|
144
|
+
backgroundColor: muted,
|
|
145
|
+
paddingHorizontal: 4,
|
|
146
|
+
paddingVertical: 1,
|
|
147
|
+
fontSize: 13,
|
|
148
|
+
fontFamily: MONO_FONT,
|
|
149
|
+
borderRadius: 3,
|
|
150
|
+
}}
|
|
151
|
+
>
|
|
152
|
+
{children}
|
|
153
|
+
</Text>
|
|
154
|
+
);
|
|
155
|
+
},
|
|
156
|
+
list_item: (node: ASTNode, children: ReactNode[], parent: ASTNode[]) => {
|
|
157
|
+
const isOrdered = parent[parent.length - 1]?.type === 'ordered_list';
|
|
158
|
+
const bullet = isOrdered
|
|
159
|
+
? `${parent[parent.length - 1].children.indexOf(node) + 1}.`
|
|
160
|
+
: '\u2022';
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<View key={node.key} style={{ flexDirection: 'row', paddingVertical: 2, paddingLeft: 16 }}>
|
|
164
|
+
<Text style={{ marginRight: 8, minWidth: 14, ...BODY_TEXT, color: mutedForeground }}>
|
|
165
|
+
{bullet}
|
|
166
|
+
</Text>
|
|
167
|
+
{/* A View, not a Text: a nested list is a View, and inside a Text it runs inline. */}
|
|
168
|
+
<View style={{ flex: 1 }}>{children}</View>
|
|
169
|
+
</View>
|
|
170
|
+
);
|
|
171
|
+
},
|
|
172
|
+
ordered_list: (node: ASTNode, children: ReactNode[]) => (
|
|
173
|
+
<View key={node.key} style={{ marginVertical: 8 }}>
|
|
174
|
+
{children}
|
|
175
|
+
</View>
|
|
176
|
+
),
|
|
177
|
+
unordered_list: (node: ASTNode, children: ReactNode[]) => (
|
|
178
|
+
<View key={node.key} style={{ marginVertical: 8 }}>
|
|
179
|
+
{children}
|
|
180
|
+
</View>
|
|
181
|
+
),
|
|
182
|
+
strong: (node: ASTNode, children: ReactNode[]) => (
|
|
183
|
+
<Text key={node.key} style={{ fontWeight: '600', ...BODY_TEXT, color: textColor }}>
|
|
184
|
+
{children}
|
|
185
|
+
</Text>
|
|
186
|
+
),
|
|
187
|
+
link: (node: ASTNode, children: ReactNode[]) => (
|
|
188
|
+
<Text
|
|
189
|
+
key={node.key}
|
|
190
|
+
style={{ color: primary, textDecorationLine: 'underline', ...BODY_TEXT }}
|
|
191
|
+
onPress={() => {
|
|
192
|
+
if (node.attributes?.href) Linking.openURL(node.attributes.href);
|
|
193
|
+
}}
|
|
194
|
+
>
|
|
195
|
+
{children}
|
|
196
|
+
</Text>
|
|
197
|
+
),
|
|
198
|
+
paragraph: (node: ASTNode, children: ReactNode[]) => (
|
|
199
|
+
<Text key={node.key} style={{ marginBottom: 8, ...BODY_TEXT, color: textColor }}>
|
|
200
|
+
{children}
|
|
201
|
+
</Text>
|
|
202
|
+
),
|
|
203
|
+
blockquote: (node: ASTNode, children: ReactNode[]) => (
|
|
204
|
+
<View
|
|
205
|
+
key={node.key}
|
|
206
|
+
style={{
|
|
207
|
+
marginVertical: 8,
|
|
208
|
+
borderLeftWidth: 4,
|
|
209
|
+
borderLeftColor: border,
|
|
210
|
+
backgroundColor: muted,
|
|
211
|
+
paddingHorizontal: 12,
|
|
212
|
+
paddingVertical: 4,
|
|
213
|
+
borderTopRightRadius: 6,
|
|
214
|
+
borderBottomRightRadius: 6,
|
|
215
|
+
}}
|
|
216
|
+
>
|
|
217
|
+
{children}
|
|
218
|
+
</View>
|
|
219
|
+
),
|
|
220
|
+
hr: (node: ASTNode) => (
|
|
221
|
+
<View
|
|
222
|
+
key={node.key}
|
|
223
|
+
style={{
|
|
224
|
+
marginVertical: 16,
|
|
225
|
+
height: 1,
|
|
226
|
+
backgroundColor: border,
|
|
227
|
+
}}
|
|
228
|
+
/>
|
|
229
|
+
),
|
|
230
|
+
body: (node: ASTNode, children: ReactNode[]) => {
|
|
231
|
+
return (
|
|
232
|
+
<View key={node.key} style={{ marginBottom: -8 }}>
|
|
233
|
+
{children}
|
|
234
|
+
</View>
|
|
235
|
+
);
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function createStyles(colors: AliaColors, fontFamily: string | undefined) {
|
|
241
|
+
const { text: textColor, muted, border, primary, mutedForeground } = colors;
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
// On WEB the family arrives through the cascade down to `html`, so naming
|
|
245
|
+
// one here would override Bloom rather than complement it.
|
|
246
|
+
//
|
|
247
|
+
// On NATIVE nothing arrives. Bloom's only app-wide mechanism is the
|
|
248
|
+
// `Text.defaultProps.style` mutation in its `FontLoader.native.tsx`, and
|
|
249
|
+
// **React 19 drops `defaultProps` under the automatic JSX runtime** that
|
|
250
|
+
// Expo compiles to — measured on react@19.2.3: `createElement(Foo, {})`
|
|
251
|
+
// yields `{style:{fontFamily}}` while `jsx(Foo, {})` yields `{}`, for
|
|
252
|
+
// function AND class components alike. So that mutation reaches no `<Text>`
|
|
253
|
+
// in any Expo consumer, and this renderer cannot inherit a face.
|
|
254
|
+
//
|
|
255
|
+
// `fontFamily` therefore comes from the caller, which is the only party
|
|
256
|
+
// that both knows the family and is allowed to name it: react-native-markdown-display
|
|
257
|
+
// takes RN style objects, so there is no className path to NativeWind's token.
|
|
258
|
+
body: { ...BODY_TEXT, color: textColor, fontFamily },
|
|
259
|
+
text: { ...BODY_TEXT, color: textColor, fontFamily },
|
|
260
|
+
paragraph: { marginTop: 0, marginBottom: 8 },
|
|
261
|
+
strong: { fontWeight: '600' as const },
|
|
262
|
+
em: { fontStyle: 'italic' as const },
|
|
263
|
+
s: { textDecorationLine: 'line-through' as const },
|
|
264
|
+
bullet_list_icon: { marginLeft: 0, marginRight: 8, ...BODY_TEXT, color: mutedForeground },
|
|
265
|
+
ordered_list_icon: { marginLeft: 0, marginRight: 8, ...BODY_TEXT, color: mutedForeground },
|
|
266
|
+
bullet_list_content: { flex: 1 },
|
|
267
|
+
ordered_list_content: { flex: 1 },
|
|
268
|
+
code_inline: {
|
|
269
|
+
fontSize: 13,
|
|
270
|
+
fontFamily: MONO_FONT,
|
|
271
|
+
backgroundColor: muted,
|
|
272
|
+
borderWidth: 0,
|
|
273
|
+
borderRadius: 3,
|
|
274
|
+
paddingHorizontal: 4,
|
|
275
|
+
paddingVertical: 1,
|
|
276
|
+
},
|
|
277
|
+
code_block: {
|
|
278
|
+
fontSize: 13,
|
|
279
|
+
fontFamily: MONO_FONT,
|
|
280
|
+
backgroundColor: muted,
|
|
281
|
+
borderWidth: 1,
|
|
282
|
+
borderColor: border,
|
|
283
|
+
borderRadius: 6,
|
|
284
|
+
padding: 12,
|
|
285
|
+
},
|
|
286
|
+
fence: {
|
|
287
|
+
fontSize: 13,
|
|
288
|
+
fontFamily: MONO_FONT,
|
|
289
|
+
backgroundColor: muted,
|
|
290
|
+
borderWidth: 1,
|
|
291
|
+
borderColor: border,
|
|
292
|
+
borderRadius: 6,
|
|
293
|
+
padding: 12,
|
|
294
|
+
},
|
|
295
|
+
table: {
|
|
296
|
+
borderWidth: 1,
|
|
297
|
+
borderColor: border,
|
|
298
|
+
borderRadius: 12,
|
|
299
|
+
marginVertical: 8,
|
|
300
|
+
overflow: 'hidden' as const,
|
|
301
|
+
},
|
|
302
|
+
thead: { backgroundColor: muted },
|
|
303
|
+
th: { flex: 1, padding: 8, fontWeight: '600' as const, ...BODY_TEXT, color: textColor },
|
|
304
|
+
td: { flex: 1, padding: 8, ...BODY_TEXT, color: textColor },
|
|
305
|
+
tr: { borderBottomWidth: 1, borderColor: border, flexDirection: 'row' as const },
|
|
306
|
+
link: { textDecorationLine: 'underline' as const, color: primary, ...BODY_TEXT },
|
|
307
|
+
hr: { backgroundColor: border, height: 1, marginVertical: 16 },
|
|
308
|
+
heading1: { fontSize: 18, fontWeight: '600' as const },
|
|
309
|
+
heading2: { ...HEADING_TEXT, fontWeight: '600' as const },
|
|
310
|
+
heading3: { ...HEADING_TEXT, fontWeight: '600' as const },
|
|
311
|
+
heading4: { ...HEADING_TEXT, fontWeight: '500' as const },
|
|
312
|
+
heading5: { ...HEADING_TEXT, fontWeight: '500' as const },
|
|
313
|
+
heading6: { ...HEADING_TEXT, fontWeight: '500' as const },
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Draws one block of code: its source, and the fence's language when it named one. */
|
|
7
318
|
export type RenderCodeBlock = (code: string, language: string | undefined) => ReactNode;
|
|
8
319
|
|
|
320
|
+
/** The fence's info string up to the first space (```ts title=x → `ts`). */
|
|
321
|
+
function codeLanguage(node: ASTNode): string | undefined {
|
|
322
|
+
const info = (node as ASTNode & { sourceInfo?: string }).sourceInfo?.trim() ?? '';
|
|
323
|
+
const language = info.split(/\s+/)[0];
|
|
324
|
+
return language === '' ? undefined : language;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* The body face, which `react-native-markdown-display` cannot inherit on native.
|
|
329
|
+
*
|
|
330
|
+
* It is driven by RN STYLE OBJECTS and supports no `className`, so NativeWind's `--font-sans`
|
|
331
|
+
* cannot reach the `<Text>` elements it renders. And Bloom's app-wide native default — the
|
|
332
|
+
* `Text.defaultProps` mutation in its `FontLoader.native.tsx` — does not apply: React 19 drops
|
|
333
|
+
* `defaultProps` under the automatic JSX runtime Expo compiles to. So the family is handed over
|
|
334
|
+
* explicitly, taken from Bloom's own token so it has one owner. `fontFamilies.sans` is a CSS
|
|
335
|
+
* STACK and RN's `fontFamily` takes a single family, hence the leading entry. Web is left
|
|
336
|
+
* undefined on purpose: there the cascade from `html` already carries `--bloom-font-sans`, and
|
|
337
|
+
* naming a family would override Bloom instead of complementing it.
|
|
338
|
+
*/
|
|
339
|
+
const BODY_FONT = Platform.OS === 'web' ? undefined : fontFamilies.sans.split(',')[0].trim();
|
|
340
|
+
|
|
9
341
|
export interface AliaMarkdownProps {
|
|
342
|
+
/**
|
|
343
|
+
* The sans family for body text, as a SINGLE family name — RN's `fontFamily` takes one, not a
|
|
344
|
+
* CSS stack. Omitted, Bloom's own (see `BODY_FONT`).
|
|
345
|
+
*/
|
|
346
|
+
fontFamily?: string;
|
|
10
347
|
content: string;
|
|
11
|
-
|
|
12
|
-
streaming?: boolean;
|
|
348
|
+
colors?: Partial<AliaColors>;
|
|
13
349
|
/**
|
|
14
|
-
* Draw fenced code with the host's own code card. Keep the function's
|
|
15
|
-
* module-level function or a `useCallback`): it is
|
|
350
|
+
* Draw fenced code with the host's own code card. Keep the function's
|
|
351
|
+
* identity stable (a module-level function or a `useCallback`): it is part of
|
|
352
|
+
* the rules' memo, and a new one per render re-parses every block.
|
|
16
353
|
*/
|
|
17
354
|
renderCodeBlock?: RenderCodeBlock;
|
|
18
355
|
}
|
|
19
356
|
|
|
357
|
+
const FENCE_RE = /^\s*(?:```|~~~)/;
|
|
358
|
+
const LIST_OR_TABLE_RE = /^\s*(?:[-*+]\s|\d+[.)]\s|\|)/;
|
|
359
|
+
// Link reference definitions ([1]: https://…) resolve per-parse, so a document
|
|
360
|
+
// using them cannot be split into independently parsed blocks.
|
|
361
|
+
const LINK_REF_DEF_RE = /^\s*\[[^\]]+\]:\s/m;
|
|
362
|
+
|
|
20
363
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
364
|
+
* Split markdown into top-level blocks at blank-line boundaries, keeping
|
|
365
|
+
* fenced code, list runs, and tables intact. During streaming only the last
|
|
366
|
+
* block's text changes, so every completed block hits React.memo and skips
|
|
367
|
+
* its markdown-it re-parse — the per-flush cost drops from O(document) to
|
|
368
|
+
* O(trailing block).
|
|
24
369
|
*/
|
|
25
|
-
|
|
370
|
+
function splitMarkdownBlocks(content: string): string[] {
|
|
371
|
+
if (LINK_REF_DEF_RE.test(content)) return [content];
|
|
372
|
+
|
|
373
|
+
const lines = content.split('\n');
|
|
374
|
+
const blocks: string[] = [];
|
|
375
|
+
let current: string[] = [];
|
|
376
|
+
let lastNonBlank = '';
|
|
377
|
+
let inFence = false;
|
|
378
|
+
let i = 0;
|
|
379
|
+
|
|
380
|
+
while (i < lines.length) {
|
|
381
|
+
const line = lines[i];
|
|
382
|
+
if (FENCE_RE.test(line)) {
|
|
383
|
+
inFence = !inFence;
|
|
384
|
+
current.push(line);
|
|
385
|
+
lastNonBlank = line;
|
|
386
|
+
i += 1;
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (!inFence && line.trim() === '') {
|
|
390
|
+
let j = i;
|
|
391
|
+
while (j < lines.length && lines[j].trim() === '') j += 1;
|
|
392
|
+
const next = j < lines.length ? lines[j] : '';
|
|
393
|
+
// Splitting a list or table at a blank line would restart ordered-list
|
|
394
|
+
// numbering and break table parsing — keep those runs in one block.
|
|
395
|
+
const keepTogether = LIST_OR_TABLE_RE.test(lastNonBlank) || LIST_OR_TABLE_RE.test(next);
|
|
396
|
+
if (keepTogether || current.length === 0) {
|
|
397
|
+
for (; i < j; i += 1) current.push(lines[i]);
|
|
398
|
+
} else {
|
|
399
|
+
blocks.push(current.join('\n'));
|
|
400
|
+
current = [];
|
|
401
|
+
i = j;
|
|
402
|
+
}
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
current.push(line);
|
|
406
|
+
lastNonBlank = line;
|
|
407
|
+
i += 1;
|
|
408
|
+
}
|
|
409
|
+
if (current.length > 0) blocks.push(current.join('\n'));
|
|
410
|
+
return blocks.length > 0 ? blocks : [content];
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
type MarkdownBlockProps = {
|
|
414
|
+
content: string;
|
|
415
|
+
rules: ReturnType<typeof createRules>;
|
|
416
|
+
styles: ReturnType<typeof createStyles>;
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* `react-native-markdown-display` gives three of its props DEFAULT VALUES that
|
|
421
|
+
* are new objects on every render: `markdownit = MarkdownIt({ typographer:
|
|
422
|
+
* true })`, `allowedImageHandlers = [...]` and `topLevelMaxExceededItem =
|
|
423
|
+
* <Text>`. A default parameter is evaluated per call, so each render of each
|
|
424
|
+
* `<Markdown>` built a parser (compiling linkify-it's regexes), and the two
|
|
425
|
+
* others sit in the deps of its renderer's `useMemo`, so the renderer — and
|
|
426
|
+
* the `StyleSheet.create` of every style inside it — was rebuilt every render
|
|
427
|
+
* too. A streamed answer re-renders its last block per token, and opening a
|
|
428
|
+
* thread renders every block once: both paid for it. Passing the defaults'
|
|
429
|
+
* own values, created once, makes both memos hold. The parser is shared:
|
|
430
|
+
* `parse` keeps no state between calls.
|
|
431
|
+
*
|
|
432
|
+
* The typings omit the last two props, which the component does take.
|
|
433
|
+
*/
|
|
434
|
+
const PARSER = MarkdownIt({ typographer: true });
|
|
435
|
+
const IMAGE_HANDLERS = [
|
|
436
|
+
'data:image/png;base64',
|
|
437
|
+
'data:image/gif;base64',
|
|
438
|
+
'data:image/jpeg;base64',
|
|
439
|
+
'https://',
|
|
440
|
+
'http://',
|
|
441
|
+
];
|
|
442
|
+
const TOP_LEVEL_EXCEEDED = <Text key="dotdotdot">...</Text>;
|
|
443
|
+
const StableMarkdown = Markdown as React.ComponentType<
|
|
444
|
+
React.ComponentProps<typeof Markdown> & {
|
|
445
|
+
allowedImageHandlers?: string[];
|
|
446
|
+
topLevelMaxExceededItem?: ReactNode;
|
|
447
|
+
}
|
|
448
|
+
>;
|
|
449
|
+
|
|
450
|
+
function ParsedMarkdown({ content, rules, styles }: MarkdownBlockProps) {
|
|
26
451
|
return (
|
|
27
|
-
<
|
|
452
|
+
<StableMarkdown
|
|
453
|
+
rules={rules}
|
|
454
|
+
style={styles}
|
|
455
|
+
markdownit={PARSER}
|
|
456
|
+
allowedImageHandlers={IMAGE_HANDLERS}
|
|
457
|
+
topLevelMaxExceededItem={TOP_LEVEL_EXCEEDED}
|
|
458
|
+
>
|
|
28
459
|
{content}
|
|
29
|
-
</
|
|
460
|
+
</StableMarkdown>
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const MarkdownBlock = React.memo(ParsedMarkdown);
|
|
465
|
+
|
|
466
|
+
export function AliaMarkdown({
|
|
467
|
+
content,
|
|
468
|
+
colors: colorOverrides,
|
|
469
|
+
fontFamily = BODY_FONT,
|
|
470
|
+
renderCodeBlock,
|
|
471
|
+
}: AliaMarkdownProps) {
|
|
472
|
+
// Bloom's theme, which follows the app's chosen mode, not the system's scheme: a person who
|
|
473
|
+
// picked light on a dark OS gets dark text on a light surface, not the reverse.
|
|
474
|
+
const { colors: theme } = useTheme();
|
|
475
|
+
const colors = {
|
|
476
|
+
text: theme.text,
|
|
477
|
+
muted: theme.backgroundSecondary,
|
|
478
|
+
border: theme.border,
|
|
479
|
+
primary: theme.primary,
|
|
480
|
+
mutedForeground: theme.textSecondary,
|
|
481
|
+
...colorOverrides,
|
|
482
|
+
} as AliaColors;
|
|
483
|
+
|
|
484
|
+
const customRules = useMemo(
|
|
485
|
+
() => createRules(colors, renderCodeBlock),
|
|
486
|
+
[
|
|
487
|
+
colors.text,
|
|
488
|
+
colors.muted,
|
|
489
|
+
colors.border,
|
|
490
|
+
colors.primary,
|
|
491
|
+
colors.mutedForeground,
|
|
492
|
+
renderCodeBlock,
|
|
493
|
+
],
|
|
494
|
+
);
|
|
495
|
+
const markdownStyles = useMemo(
|
|
496
|
+
() => createStyles(colors, fontFamily),
|
|
497
|
+
[colors.text, colors.muted, colors.border, colors.primary, colors.mutedForeground, fontFamily],
|
|
498
|
+
);
|
|
499
|
+
const blocks = useMemo(() => splitMarkdownBlocks(content), [content]);
|
|
500
|
+
|
|
501
|
+
if (blocks.length === 1) {
|
|
502
|
+
return <ParsedMarkdown content={blocks[0]} rules={customRules} styles={markdownStyles} />;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Each block's `body` rule cancels its own trailing paragraph margin
|
|
506
|
+
// (marginBottom: -8), so rowGap restores the exact inter-paragraph spacing
|
|
507
|
+
// a single continuous parse would produce.
|
|
508
|
+
return (
|
|
509
|
+
<View style={{ rowGap: 8 }}>
|
|
510
|
+
{blocks.map((block, i) => (
|
|
511
|
+
<MarkdownBlock key={i} content={block} rules={customRules} styles={markdownStyles} />
|
|
512
|
+
))}
|
|
513
|
+
</View>
|
|
30
514
|
);
|
|
31
515
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { RiCheckLine, RiCloseLine, RiListCheck3 } from '@oxy.so/bloom/icons';
|
|
1
|
+
import { Card, CardBody, CardHeader, CardTitle } from '@oxy.so/bloom/card';
|
|
2
|
+
import { RiListCheck3 } from '@oxy.so/bloom/icons';
|
|
4
3
|
import { Item } from '@oxy.so/bloom/item';
|
|
5
4
|
import { useTheme } from '@oxy.so/bloom/theme';
|
|
6
5
|
import { Text } from '@oxy.so/bloom/typography';
|
|
@@ -12,35 +11,16 @@ import type { PlanStep } from '../types';
|
|
|
12
11
|
|
|
13
12
|
interface PlanPreviewCardProps {
|
|
14
13
|
steps: PlanStep[];
|
|
15
|
-
onApprove: () => void;
|
|
16
|
-
onReject: () => void;
|
|
17
|
-
approved?: boolean;
|
|
18
|
-
rejected?: boolean;
|
|
19
14
|
}
|
|
20
15
|
|
|
21
|
-
/**
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
approved,
|
|
27
|
-
rejected,
|
|
28
|
-
}: PlanPreviewCardProps) {
|
|
16
|
+
/**
|
|
17
|
+
* The plan the agent shows before it works. It is only shown: the agent does not wait on it, and
|
|
18
|
+
* nobody is asked to approve it.
|
|
19
|
+
*/
|
|
20
|
+
export function PlanPreviewCard({ steps }: PlanPreviewCardProps) {
|
|
29
21
|
const labels = useLabels();
|
|
30
22
|
const { colors } = useTheme();
|
|
31
23
|
|
|
32
|
-
if (approved || rejected) {
|
|
33
|
-
const Icon = approved ? RiCheckLine : RiCloseLine;
|
|
34
|
-
return (
|
|
35
|
-
<View className="flex-row items-center gap-1.5 py-1">
|
|
36
|
-
<Icon width={14} height={14} fill={approved ? colors.success : colors.error} />
|
|
37
|
-
<Text className="text-xs text-text-secondary">
|
|
38
|
-
{approved ? labels.planApproved : labels.planCancelled}
|
|
39
|
-
</Text>
|
|
40
|
-
</View>
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
24
|
return (
|
|
45
25
|
<Animated.View entering={planPreviewEnter} className="my-2">
|
|
46
26
|
<Card appearance="outline">
|
|
@@ -67,28 +47,6 @@ export function PlanPreviewCard({
|
|
|
67
47
|
/>
|
|
68
48
|
))}
|
|
69
49
|
</CardBody>
|
|
70
|
-
<CardFooter>
|
|
71
|
-
<View className="flex-1 flex-row gap-2">
|
|
72
|
-
<Button
|
|
73
|
-
tone="action"
|
|
74
|
-
size="sm"
|
|
75
|
-
className="flex-1"
|
|
76
|
-
leadingIcon={RiCheckLine}
|
|
77
|
-
onPress={onApprove}
|
|
78
|
-
>
|
|
79
|
-
{labels.planApprove}
|
|
80
|
-
</Button>
|
|
81
|
-
<Button
|
|
82
|
-
tone="neutral"
|
|
83
|
-
appearance="subtle"
|
|
84
|
-
size="sm"
|
|
85
|
-
className="flex-1"
|
|
86
|
-
onPress={onReject}
|
|
87
|
-
>
|
|
88
|
-
{labels.planCancel}
|
|
89
|
-
</Button>
|
|
90
|
-
</View>
|
|
91
|
-
</CardFooter>
|
|
92
50
|
</Card>
|
|
93
51
|
</Animated.View>
|
|
94
52
|
);
|