@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.
@@ -1,26 +1,7 @@
1
1
  import React, { useRef, useEffect, useState, useCallback } from "react";
2
- import {
3
- View,
4
- Text,
5
- ScrollView,
6
- StyleSheet,
7
- TouchableOpacity,
8
- NativeScrollEvent,
9
- Keyboard,
10
- Platform,
11
- Image,
12
- Linking,
13
- } from "react-native";
14
- import type {
15
- ServerMessage,
16
- ToolMessage,
17
- ResultMessage,
18
- UserPromptMessage,
19
- HistoryResultMessage,
20
- SystemDisplayMessage,
21
- AssistantPart,
22
- AssistantPartsMessage,
23
- } from "../services/websocket";
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";
24
5
  import { SPACING, LAYOUT, COLORS, TYPOGRAPHY } from "../constants/design";
25
6
 
26
7
  interface ResponseAreaProps {
@@ -146,514 +127,6 @@ export function ResponseArea({ messages, currentParts }: ResponseAreaProps) {
146
127
  );
147
128
  }
148
129
 
149
- function MessageItem({ message }: { key?: React.Key; message: ServerMessage }) {
150
- switch (message.type) {
151
- case "stream":
152
- return null; // Handled by currentParts
153
-
154
- case "tool":
155
- // Legacy: individual tool messages from history
156
- return <ToolItem tool={message} />;
157
-
158
- case "result":
159
- return <ResultItem result={message} />;
160
-
161
- case "error":
162
- return (
163
- <View style={styles.errorContainer}>
164
- <Text style={styles.errorText}>{message.message}</Text>
165
- </View>
166
- );
167
-
168
- case "status":
169
- return null; // Handled by header
170
-
171
- case "user_prompt":
172
- return <UserPromptItem message={message} />;
173
-
174
- case "history_result":
175
- return <HistoryResultItem message={message} />;
176
-
177
- case "assistant_parts":
178
- return <AssistantPartsItem message={message} />;
179
-
180
- case "system_message":
181
- return <SystemMessageItem message={message} />;
182
-
183
- default:
184
- return null;
185
- }
186
- }
187
-
188
- function UserPromptItem({ message }: { message: UserPromptMessage }) {
189
- return (
190
- <View style={styles.userPromptContainer}>
191
- {message.images && message.images.length > 0 && (
192
- <View style={styles.userImages}>
193
- {message.images.map((img, i) => (
194
- <Image
195
- key={i}
196
- source={{ uri: img.uri }}
197
- style={styles.userImageThumb}
198
- />
199
- ))}
200
- </View>
201
- )}
202
- {message.content ? (
203
- <Text style={styles.userPromptText} selectable>
204
- {message.content}
205
- </Text>
206
- ) : null}
207
- </View>
208
- );
209
- }
210
-
211
- function HistoryResultItem({ message }: { message: HistoryResultMessage }) {
212
- return (
213
- <View style={styles.resultContainer}>
214
- <FormattedText content={message.content} />
215
- </View>
216
- );
217
- }
218
-
219
- function SystemMessageItem({ message }: { message: SystemDisplayMessage }) {
220
- // Use error styling for errors, muted styling for other system messages
221
- if (message.messageType === "error") {
222
- return (
223
- <View style={styles.errorContainer}>
224
- <Text style={styles.errorText}>{message.content}</Text>
225
- </View>
226
- );
227
- }
228
- // Stopped and info messages use muted styling
229
- return (
230
- <View style={styles.systemContainer}>
231
- <Text style={styles.systemText}>{message.content}</Text>
232
- </View>
233
- );
234
- }
235
-
236
- // Renders formatted text with markdown-style lists, code, and emphasis
237
- function FormattedText({
238
- content,
239
- isStreaming,
240
- }: {
241
- content: string;
242
- isStreaming?: boolean;
243
- }) {
244
- const lines = content.split("\n");
245
- const elements: React.ReactNode[] = [];
246
- let i = 0;
247
-
248
- while (i < lines.length) {
249
- const line = lines[i];
250
- const trimmed = line.trim();
251
-
252
- // Code block start (check first to avoid matching content inside code blocks)
253
- if (trimmed.startsWith("```")) {
254
- const codeLines: string[] = [];
255
- const lang = trimmed.slice(3).trim();
256
- i++;
257
- while (i < lines.length && !lines[i].trim().startsWith("```")) {
258
- codeLines.push(lines[i]);
259
- i++;
260
- }
261
- elements.push(
262
- <View key={`code-${i}`} style={styles.codeBlock}>
263
- {lang && <Text style={styles.codeLang}>{lang}</Text>}
264
- <Text style={styles.codeText} selectable>
265
- {codeLines.join("\n")}
266
- </Text>
267
- </View>,
268
- );
269
- i++; // skip closing ```
270
- continue;
271
- }
272
-
273
- // Heading (# ## ### etc.)
274
- const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
275
- if (headingMatch) {
276
- const level = headingMatch[1].length;
277
- const headingStyle =
278
- level <= 2
279
- ? styles.heading1
280
- : level <= 4
281
- ? styles.heading2
282
- : styles.heading3;
283
- elements.push(
284
- <Text key={i} style={headingStyle} selectable>
285
- {formatInlineText(headingMatch[2])}
286
- </Text>,
287
- );
288
- i++;
289
- continue;
290
- }
291
-
292
- // Blockquote (> text) - collect consecutive > lines
293
- if (trimmed.startsWith("> ") || trimmed === ">") {
294
- const quoteLines: string[] = [];
295
- while (
296
- i < lines.length &&
297
- (lines[i].trim().startsWith("> ") || lines[i].trim() === ">")
298
- ) {
299
- const qContent = lines[i].trim().startsWith("> ")
300
- ? lines[i].trim().slice(2)
301
- : "";
302
- quoteLines.push(qContent);
303
- i++;
304
- }
305
- elements.push(
306
- <View key={`quote-${i}`} style={styles.blockquote}>
307
- <Text style={styles.blockquoteText} selectable>
308
- {formatInlineText(quoteLines.join("\n"))}
309
- </Text>
310
- </View>,
311
- );
312
- continue;
313
- }
314
-
315
- // Task list item (- [ ] or - [x])
316
- const taskMatch = trimmed.match(/^[-*]\s+\[([ xX])\]\s+(.+)$/);
317
- if (taskMatch) {
318
- const checked = taskMatch[1] !== " ";
319
- elements.push(
320
- <View key={i} style={styles.listItem}>
321
- <Text style={styles.listBullet}>{checked ? "☑" : "☐"}</Text>
322
- <Text style={styles.listText} selectable>
323
- {formatInlineText(taskMatch[2])}
324
- </Text>
325
- </View>,
326
- );
327
- i++;
328
- continue;
329
- }
330
-
331
- // Numbered list item (1. 2. 3.) - with nesting via indentation
332
- const numberedMatch = line.match(/^(\s*)(\d+)\.\s+(.+)$/);
333
- if (numberedMatch) {
334
- const indent = Math.floor(numberedMatch[1].length / 2);
335
- const [, , num, text] = numberedMatch;
336
- elements.push(
337
- <View
338
- key={i}
339
- style={[
340
- styles.listItem,
341
- indent > 0 && { paddingLeft: SPACING.SM + indent * SPACING.LG },
342
- ]}
343
- >
344
- <Text style={styles.listNumber}>{num}.</Text>
345
- <Text style={styles.listText} selectable>
346
- {formatInlineText(text)}
347
- </Text>
348
- </View>,
349
- );
350
- i++;
351
- continue;
352
- }
353
-
354
- // Bullet list item (- or *) - with nesting via indentation
355
- const bulletMatch = line.match(/^(\s*)[-*]\s+(.+)$/);
356
- if (bulletMatch) {
357
- const indent = Math.floor(bulletMatch[1].length / 2);
358
- const bulletChar = indent === 0 ? "•" : indent === 1 ? "◦" : "▪";
359
- elements.push(
360
- <View
361
- key={i}
362
- style={[
363
- styles.listItem,
364
- indent > 0 && { paddingLeft: SPACING.SM + indent * SPACING.LG },
365
- ]}
366
- >
367
- <Text style={styles.listBullet}>{bulletChar}</Text>
368
- <Text style={styles.listText} selectable>
369
- {formatInlineText(bulletMatch[2])}
370
- </Text>
371
- </View>,
372
- );
373
- i++;
374
- continue;
375
- }
376
-
377
- // Horizontal rule (---, ***, ___)
378
- if (/^[-*_]{3,}$/.test(trimmed)) {
379
- elements.push(<View key={i} style={styles.horizontalRule} />);
380
- i++;
381
- continue;
382
- }
383
-
384
- // Empty line = paragraph break
385
- if (!trimmed) {
386
- elements.push(<View key={i} style={styles.paragraphBreak} />);
387
- i++;
388
- continue;
389
- }
390
-
391
- // Regular text
392
- elements.push(
393
- <Text key={i} style={styles.responseText} selectable>
394
- {formatInlineText(line)}
395
- </Text>,
396
- );
397
- i++;
398
- }
399
-
400
- return (
401
- <View style={styles.formattedContainer}>
402
- {elements}
403
- {isStreaming && <View style={styles.cursor} />}
404
- </View>
405
- );
406
- }
407
-
408
- // Format inline elements: code spans first, then links, then emphasis
409
- function formatInlineText(text: string): React.ReactNode {
410
- // Split on inline code first to avoid processing markdown inside code spans
411
- const codeParts = text.split(/(`[^`]+`)/g);
412
- if (codeParts.length === 1) {
413
- return formatLinks(text);
414
- }
415
-
416
- return codeParts.map((part, i) => {
417
- if (part.startsWith("`") && part.endsWith("`")) {
418
- return (
419
- <Text key={i} style={styles.inlineCode}>
420
- {part.slice(1, -1)}
421
- </Text>
422
- );
423
- }
424
- return <React.Fragment key={i}>{formatLinks(part)}</React.Fragment>;
425
- });
426
- }
427
-
428
- // Format markdown links [text](url)
429
- function formatLinks(text: string): React.ReactNode {
430
- const parts = text.split(/(\[[^\]]+\]\([^)]+\))/g);
431
- if (parts.length === 1) return formatEmphasis(text);
432
-
433
- return parts.map((part, i) => {
434
- const linkMatch = part.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
435
- if (linkMatch) {
436
- return (
437
- <Text
438
- key={i}
439
- style={styles.linkText}
440
- onPress={() => Linking.openURL(linkMatch[2])}
441
- >
442
- {linkMatch[1]}
443
- </Text>
444
- );
445
- }
446
- return <React.Fragment key={i}>{formatEmphasis(part)}</React.Fragment>;
447
- });
448
- }
449
-
450
- // Format bold, italic, and strikethrough emphasis
451
- function formatEmphasis(text: string): React.ReactNode {
452
- // Match **bold**, ~~strikethrough~~, *italic*, and plain text segments
453
- const parts = text.split(/(\*\*[^*]+\*\*|~~[^~]+~~|\*[^*]+\*)/g);
454
- if (parts.length === 1) return text;
455
-
456
- return parts.map((part, i) => {
457
- if (part.startsWith("**") && part.endsWith("**")) {
458
- return (
459
- <Text key={i} style={styles.boldText}>
460
- {part.slice(2, -2)}
461
- </Text>
462
- );
463
- }
464
- if (part.startsWith("~~") && part.endsWith("~~")) {
465
- return (
466
- <Text key={i} style={styles.strikethroughText}>
467
- {part.slice(2, -2)}
468
- </Text>
469
- );
470
- }
471
- if (part.startsWith("*") && part.endsWith("*")) {
472
- return (
473
- <Text key={i} style={styles.italicText}>
474
- {part.slice(1, -1)}
475
- </Text>
476
- );
477
- }
478
- return part;
479
- });
480
- }
481
-
482
- // Renders interleaved text and tool parts in order
483
- function PartsRenderer({
484
- parts,
485
- isStreaming,
486
- }: {
487
- parts: AssistantPart[];
488
- isStreaming: boolean;
489
- }) {
490
- return (
491
- <View style={styles.partsContainer}>
492
- {parts.map((part, index) => {
493
- if (part.type === "text") {
494
- const isLastPart = index === parts.length - 1;
495
- return (
496
- <View key={part.id} style={styles.messageContainer}>
497
- <FormattedText
498
- content={part.content}
499
- isStreaming={isStreaming && isLastPart}
500
- />
501
- </View>
502
- );
503
- } else if (part.type === "tool") {
504
- return <ToolPartItem key={part.id} part={part} />;
505
- }
506
- return null;
507
- })}
508
- </View>
509
- );
510
- }
511
-
512
- // Renders a completed assistant response with parts
513
- function AssistantPartsItem({ message }: { message: AssistantPartsMessage }) {
514
- return (
515
- <View style={styles.resultContainer}>
516
- <PartsRenderer parts={message.parts} isStreaming={false} />
517
- {!message.isComplete && (
518
- <Text style={styles.interruptedText}>(interrupted)</Text>
519
- )}
520
- </View>
521
- );
522
- }
523
-
524
- // Shared helper for tool display info
525
- function getToolDisplayInfo(
526
- toolName: string,
527
- input: Record<string, unknown> | undefined,
528
- ): { label: string; value: string } {
529
- const getFileName = (path: string): string => path.split("/").pop() || path;
530
-
531
- switch (toolName) {
532
- case "Read":
533
- return {
534
- label: "read",
535
- value: getFileName((input?.file_path as string) || "file"),
536
- };
537
- case "Edit":
538
- return {
539
- label: "edit",
540
- value: getFileName((input?.file_path as string) || "file"),
541
- };
542
- case "Write":
543
- return {
544
- label: "write",
545
- value: getFileName((input?.file_path as string) || "file"),
546
- };
547
- case "Bash": {
548
- const cmd = (input?.command as string) || "";
549
- return {
550
- label: "$",
551
- value: cmd.length > 45 ? cmd.slice(0, 45) + "…" : cmd,
552
- };
553
- }
554
- case "Glob":
555
- return { label: "glob", value: (input?.pattern as string) || "*" };
556
- case "Grep":
557
- return { label: "grep", value: (input?.pattern as string) || "search" };
558
- case "Task":
559
- return {
560
- label: "agent",
561
- value: (input?.description as string) || "task",
562
- };
563
- default:
564
- return { label: toolName.toLowerCase(), value: "" };
565
- }
566
- }
567
-
568
- // Renders a tool display line (shared between ToolPartItem and ToolItem)
569
- function ToolDisplay({
570
- toolName,
571
- input,
572
- isFailed,
573
- }: {
574
- toolName: string;
575
- input?: unknown;
576
- isFailed: boolean;
577
- }) {
578
- const { label, value } = getToolDisplayInfo(
579
- toolName,
580
- input as Record<string, unknown> | undefined,
581
- );
582
-
583
- return (
584
- <View style={styles.toolLine}>
585
- <Text style={isFailed ? styles.toolLabelFailed : styles.toolLabel}>
586
- {label}
587
- </Text>
588
- <Text
589
- style={isFailed ? styles.toolValueFailed : styles.toolValue}
590
- numberOfLines={1}
591
- >
592
- {value}
593
- </Text>
594
- {isFailed && <Text style={styles.toolLabelFailed}> ✕</Text>}
595
- </View>
596
- );
597
- }
598
-
599
- // Tool part renderer (for parts in AssistantPartsMessage)
600
- function ToolPartItem({
601
- part,
602
- }: {
603
- key?: React.Key;
604
- part: AssistantPart & { type: "tool" };
605
- }) {
606
- if (part.status === "started") return null;
607
- return (
608
- <ToolDisplay
609
- toolName={part.toolName}
610
- input={part.input}
611
- isFailed={part.status === "failed"}
612
- />
613
- );
614
- }
615
-
616
- // Tool item renderer (for legacy ToolMessage from history)
617
- function ToolItem({ tool }: { tool: ToolMessage }) {
618
- if (tool.status === "started") return null;
619
- return (
620
- <ToolDisplay
621
- toolName={tool.toolName}
622
- input={tool.input}
623
- isFailed={tool.status === "failed"}
624
- />
625
- );
626
- }
627
-
628
- function ResultItem({ result }: { result: ResultMessage }) {
629
- if (!result.success && result.error) {
630
- return (
631
- <View style={styles.errorContainer}>
632
- <Text style={styles.errorText}>{result.error}</Text>
633
- </View>
634
- );
635
- }
636
-
637
- return (
638
- <View style={styles.resultContainer}>
639
- {result.result && (
640
- <Text style={styles.responseText} selectable>
641
- {result.result}
642
- </Text>
643
- )}
644
- {(result.costUsd !== undefined || result.durationMs !== undefined) && (
645
- <Text style={styles.metaText}>
646
- {result.durationMs !== undefined && `${result.durationMs}ms`}
647
- {result.costUsd !== undefined &&
648
- result.durationMs !== undefined &&
649
- " • "}
650
- {result.costUsd !== undefined && `$${result.costUsd.toFixed(4)}`}
651
- </Text>
652
- )}
653
- </View>
654
- );
655
- }
656
-
657
130
  const styles = StyleSheet.create({
658
131
  wrapper: {
659
132
  flex: 1,
@@ -679,230 +152,6 @@ const styles = StyleSheet.create({
679
152
  textAlign: "center",
680
153
  lineHeight: 22,
681
154
  },
682
- messageContainer: {
683
- flexDirection: "row",
684
- flexWrap: "wrap",
685
- },
686
- responseText: {
687
- color: "rgba(255,255,255,0.95)",
688
- fontSize: TYPOGRAPHY.SIZE_LG,
689
- lineHeight: 22,
690
- },
691
- cursor: {
692
- width: 2,
693
- height: 18,
694
- backgroundColor: COLORS.TEXT_PRIMARY,
695
- marginLeft: 2,
696
- opacity: 0.7,
697
- },
698
- resultContainer: {
699
- marginTop: SPACING.SM,
700
- },
701
- partsContainer: {
702
- // Container for interleaved parts
703
- },
704
- formattedContainer: {
705
- flexDirection: "column",
706
- },
707
- listItem: {
708
- flexDirection: "row",
709
- marginVertical: SPACING.XS / 2,
710
- paddingLeft: SPACING.SM,
711
- },
712
- listNumber: {
713
- color: COLORS.TEXT_MUTED,
714
- fontSize: TYPOGRAPHY.SIZE_LG,
715
- lineHeight: 22,
716
- width: 24,
717
- fontWeight: TYPOGRAPHY.WEIGHT_MEDIUM,
718
- },
719
- listBullet: {
720
- color: COLORS.TEXT_MUTED,
721
- fontSize: TYPOGRAPHY.SIZE_LG,
722
- lineHeight: 22,
723
- width: 18,
724
- },
725
- listText: {
726
- flex: 1,
727
- color: "rgba(255,255,255,0.95)",
728
- fontSize: TYPOGRAPHY.SIZE_LG,
729
- lineHeight: 22,
730
- },
731
- codeBlock: {
732
- backgroundColor: "rgba(255,255,255,0.06)",
733
- borderRadius: SPACING.SM,
734
- padding: SPACING.MD,
735
- marginVertical: SPACING.SM,
736
- },
737
- codeLang: {
738
- color: COLORS.TEXT_MUTED,
739
- fontSize: TYPOGRAPHY.SIZE_XS,
740
- fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
741
- marginBottom: SPACING.XS,
742
- textTransform: "uppercase",
743
- },
744
- codeText: {
745
- color: "rgba(255,255,255,0.85)",
746
- fontSize: TYPOGRAPHY.SIZE_SM,
747
- fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
748
- lineHeight: 18,
749
- },
750
- inlineCode: {
751
- backgroundColor: "rgba(255,255,255,0.1)",
752
- color: "rgba(255,255,255,0.9)",
753
- fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
754
- fontSize: TYPOGRAPHY.SIZE_MD,
755
- paddingHorizontal: 4,
756
- borderRadius: 3,
757
- },
758
- heading1: {
759
- color: COLORS.TEXT_PRIMARY,
760
- fontSize: TYPOGRAPHY.SIZE_XL,
761
- fontWeight: TYPOGRAPHY.WEIGHT_SEMIBOLD,
762
- lineHeight: 24,
763
- marginTop: SPACING.MD,
764
- marginBottom: SPACING.XS,
765
- },
766
- heading2: {
767
- color: "rgba(255,255,255,0.9)",
768
- fontSize: TYPOGRAPHY.SIZE_LG,
769
- fontWeight: TYPOGRAPHY.WEIGHT_SEMIBOLD,
770
- lineHeight: 22,
771
- marginTop: SPACING.SM,
772
- marginBottom: SPACING.XS,
773
- },
774
- heading3: {
775
- color: "rgba(255,255,255,0.85)",
776
- fontSize: TYPOGRAPHY.SIZE_LG,
777
- fontWeight: TYPOGRAPHY.WEIGHT_MEDIUM,
778
- lineHeight: 22,
779
- marginTop: SPACING.SM,
780
- marginBottom: SPACING.XS,
781
- },
782
- boldText: {
783
- fontWeight: TYPOGRAPHY.WEIGHT_SEMIBOLD,
784
- color: COLORS.TEXT_PRIMARY,
785
- },
786
- italicText: {
787
- fontStyle: "italic",
788
- color: "rgba(255,255,255,0.85)",
789
- },
790
- strikethroughText: {
791
- textDecorationLine: "line-through",
792
- color: COLORS.TEXT_MUTED,
793
- },
794
- linkText: {
795
- color: COLORS.STATUS_INFO,
796
- textDecorationLine: "underline",
797
- },
798
- blockquote: {
799
- borderLeftWidth: 3,
800
- borderLeftColor: "rgba(255,255,255,0.15)",
801
- paddingLeft: SPACING.MD,
802
- marginVertical: SPACING.XS,
803
- },
804
- blockquoteText: {
805
- color: COLORS.TEXT_SECONDARY,
806
- fontSize: TYPOGRAPHY.SIZE_LG,
807
- lineHeight: 22,
808
- fontStyle: "italic",
809
- },
810
- horizontalRule: {
811
- height: 1,
812
- backgroundColor: COLORS.BORDER,
813
- marginVertical: SPACING.MD,
814
- },
815
- paragraphBreak: {
816
- height: SPACING.SM,
817
- },
818
- interruptedText: {
819
- color: COLORS.TEXT_MUTED,
820
- fontSize: TYPOGRAPHY.SIZE_XS + 1, // 12px
821
- fontStyle: "italic",
822
- marginTop: SPACING.XS,
823
- },
824
- metaText: {
825
- color: COLORS.TEXT_MUTED,
826
- fontSize: TYPOGRAPHY.SIZE_XS + 1, // 12px
827
- marginTop: SPACING.SM + 2, // 10px
828
- },
829
- errorContainer: {
830
- backgroundColor: "rgba(255,59,48,0.15)",
831
- borderRadius: SPACING.MD,
832
- padding: SPACING.MD,
833
- marginVertical: SPACING.SM - 2, // 6px
834
- },
835
- errorText: {
836
- color: "#FF6B6B",
837
- fontSize: TYPOGRAPHY.SIZE_MD,
838
- },
839
- systemContainer: {
840
- backgroundColor: "rgba(255,255,255,0.05)",
841
- borderRadius: SPACING.MD,
842
- padding: SPACING.MD,
843
- marginVertical: SPACING.SM - 2, // 6px
844
- },
845
- systemText: {
846
- color: COLORS.TEXT_TERTIARY,
847
- fontSize: TYPOGRAPHY.SIZE_MD,
848
- fontStyle: "italic",
849
- },
850
- toolLine: {
851
- flexDirection: "row",
852
- alignItems: "center",
853
- marginVertical: SPACING.XS,
854
- },
855
- toolLabel: {
856
- color: COLORS.TEXT_MUTED,
857
- fontSize: TYPOGRAPHY.SIZE_XS + 1, // 12px
858
- fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
859
- marginRight: SPACING.SM,
860
- minWidth: 36,
861
- },
862
- toolLabelFailed: {
863
- color: "rgba(255,100,100,0.6)",
864
- fontSize: TYPOGRAPHY.SIZE_XS + 1, // 12px
865
- fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
866
- marginRight: SPACING.SM,
867
- minWidth: 36,
868
- },
869
- toolValue: {
870
- color: "rgba(255,255,255,0.7)",
871
- fontSize: TYPOGRAPHY.SIZE_SM,
872
- fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
873
- flexShrink: 1,
874
- },
875
- toolValueFailed: {
876
- color: "rgba(255,100,100,0.7)",
877
- fontSize: TYPOGRAPHY.SIZE_SM,
878
- fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
879
- flexShrink: 1,
880
- },
881
- userPromptContainer: {
882
- backgroundColor: "rgba(0,122,255,0.15)",
883
- borderRadius: LAYOUT.BORDER_RADIUS_SM + 2, // 16px
884
- padding: SPACING.MD,
885
- marginVertical: SPACING.SM,
886
- alignSelf: "flex-end",
887
- maxWidth: "85%",
888
- },
889
- userPromptText: {
890
- color: COLORS.TEXT_PRIMARY,
891
- fontSize: TYPOGRAPHY.SIZE_LG,
892
- lineHeight: 20,
893
- },
894
- userImages: {
895
- flexDirection: "row",
896
- flexWrap: "wrap",
897
- gap: SPACING.SM,
898
- marginBottom: SPACING.SM,
899
- },
900
- userImageThumb: {
901
- width: 80,
902
- height: 80,
903
- borderRadius: SPACING.SM,
904
- backgroundColor: "rgba(255,255,255,0.1)",
905
- },
906
155
  scrollButton: {
907
156
  position: "absolute",
908
157
  bottom: LAYOUT.CONTENT_PADDING_H,