clawdex-mobile 2.0.1 → 3.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.
Files changed (71) hide show
  1. package/.github/workflows/pages.yml +41 -0
  2. package/AGENTS.md +263 -110
  3. package/README.md +1 -1
  4. package/apps/mobile/.env.example +2 -2
  5. package/apps/mobile/App.tsx +175 -14
  6. package/apps/mobile/app.json +27 -9
  7. package/apps/mobile/eas.json +14 -4
  8. package/apps/mobile/package.json +13 -13
  9. package/apps/mobile/src/api/__tests__/chatMapping.test.ts +219 -0
  10. package/apps/mobile/src/api/__tests__/client.test.ts +579 -6
  11. package/apps/mobile/src/api/__tests__/ws.test.ts +27 -0
  12. package/apps/mobile/src/api/account.ts +47 -0
  13. package/apps/mobile/src/api/chatMapping.ts +435 -18
  14. package/apps/mobile/src/api/client.ts +296 -36
  15. package/apps/mobile/src/api/rateLimits.ts +143 -0
  16. package/apps/mobile/src/api/types.ts +106 -0
  17. package/apps/mobile/src/api/ws.ts +10 -1
  18. package/apps/mobile/src/components/ChatHeader.tsx +12 -12
  19. package/apps/mobile/src/components/ChatInput.tsx +154 -88
  20. package/apps/mobile/src/components/ChatMessage.tsx +548 -93
  21. package/apps/mobile/src/components/ComposerUsageLimits.tsx +167 -0
  22. package/apps/mobile/src/components/SelectionSheet.tsx +466 -0
  23. package/apps/mobile/src/components/ToolBlock.tsx +17 -15
  24. package/apps/mobile/src/components/VoiceRecordingWaveform.tsx +181 -0
  25. package/apps/mobile/src/components/WorkspacePickerModal.tsx +572 -0
  26. package/apps/mobile/src/components/__tests__/chat-input-layout.test.ts +35 -0
  27. package/apps/mobile/src/components/__tests__/chatImageSource.test.ts +44 -0
  28. package/apps/mobile/src/components/__tests__/composerUsageLimits.test.ts +138 -0
  29. package/apps/mobile/src/components/__tests__/voiceWaveform.test.ts +31 -0
  30. package/apps/mobile/src/components/chat-input-layout.ts +59 -0
  31. package/apps/mobile/src/components/chatImageSource.ts +86 -0
  32. package/apps/mobile/src/components/usageLimitBadges.ts +109 -0
  33. package/apps/mobile/src/components/voiceWaveform.ts +46 -0
  34. package/apps/mobile/src/config.ts +9 -2
  35. package/apps/mobile/src/hooks/useVoiceRecorder.ts +8 -1
  36. package/apps/mobile/src/navigation/DrawerContent.tsx +607 -457
  37. package/apps/mobile/src/navigation/__tests__/chatThreadTree.test.ts +89 -0
  38. package/apps/mobile/src/navigation/__tests__/drawerChats.test.ts +65 -0
  39. package/apps/mobile/src/navigation/chatThreadTree.ts +191 -0
  40. package/apps/mobile/src/navigation/drawerChats.ts +9 -0
  41. package/apps/mobile/src/screens/GitScreen.tsx +2 -0
  42. package/apps/mobile/src/screens/MainScreen.tsx +4244 -1237
  43. package/apps/mobile/src/screens/OnboardingScreen.tsx +2 -0
  44. package/apps/mobile/src/screens/SettingsScreen.tsx +256 -226
  45. package/apps/mobile/src/screens/TerminalScreen.tsx +2 -5
  46. package/apps/mobile/src/screens/__tests__/agentThreadDisplay.test.ts +80 -0
  47. package/apps/mobile/src/screens/__tests__/agentThreads.test.ts +170 -0
  48. package/apps/mobile/src/screens/__tests__/planCardState.test.ts +88 -0
  49. package/apps/mobile/src/screens/__tests__/subAgentTranscript.test.ts +102 -0
  50. package/apps/mobile/src/screens/__tests__/transcriptMessages.test.ts +97 -0
  51. package/apps/mobile/src/screens/agentThreadDisplay.ts +261 -0
  52. package/apps/mobile/src/screens/agentThreads.ts +167 -0
  53. package/apps/mobile/src/screens/planCardState.ts +40 -0
  54. package/apps/mobile/src/screens/subAgentTranscript.ts +149 -0
  55. package/apps/mobile/src/screens/transcriptMessages.ts +102 -0
  56. package/apps/mobile/src/theme.ts +6 -12
  57. package/docs/codex-app-server-cli-gap-tracker.md +14 -5
  58. package/docs/privacy-policy.md +54 -0
  59. package/docs/setup-and-operations.md +4 -3
  60. package/docs/terms-of-service.md +33 -0
  61. package/package.json +3 -3
  62. package/services/mac-bridge/package.json +6 -6
  63. package/services/rust-bridge/Cargo.lock +56 -47
  64. package/services/rust-bridge/Cargo.toml +1 -1
  65. package/services/rust-bridge/package.json +1 -1
  66. package/services/rust-bridge/src/main.rs +507 -9
  67. package/site/index.html +54 -0
  68. package/site/privacy/index.html +80 -0
  69. package/site/styles.css +135 -0
  70. package/site/support/index.html +51 -0
  71. package/site/terms/index.html +68 -0
@@ -0,0 +1,572 @@
1
+ import { Ionicons } from '@expo/vector-icons';
2
+ import {
3
+ ActivityIndicator,
4
+ Modal,
5
+ Pressable,
6
+ ScrollView,
7
+ StyleSheet,
8
+ Text,
9
+ TextInput,
10
+ useWindowDimensions,
11
+ View,
12
+ } from 'react-native';
13
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
14
+
15
+ import type {
16
+ FileSystemEntry,
17
+ WorkspaceSummary,
18
+ } from '../api/types';
19
+ import { colors, radius, spacing, typography } from '../theme';
20
+
21
+ interface WorkspacePickerModalProps {
22
+ visible: boolean;
23
+ selectedPath?: string | null;
24
+ bridgeRoot?: string | null;
25
+ recentWorkspaces: WorkspaceSummary[];
26
+ currentPath?: string | null;
27
+ parentPath?: string | null;
28
+ entries: FileSystemEntry[];
29
+ draftPath: string;
30
+ loadingRecent?: boolean;
31
+ loadingEntries?: boolean;
32
+ error?: string | null;
33
+ onDraftPathChange: (value: string) => void;
34
+ onBrowsePath: (path: string | null) => void;
35
+ onSelectPath: (path: string | null) => void;
36
+ onClose: () => void;
37
+ }
38
+
39
+ export function WorkspacePickerModal({
40
+ visible,
41
+ selectedPath = null,
42
+ bridgeRoot = null,
43
+ recentWorkspaces,
44
+ currentPath = null,
45
+ parentPath = null,
46
+ entries,
47
+ draftPath,
48
+ loadingRecent = false,
49
+ loadingEntries = false,
50
+ error = null,
51
+ onDraftPathChange,
52
+ onBrowsePath,
53
+ onSelectPath,
54
+ onClose,
55
+ }: WorkspacePickerModalProps) {
56
+ const insets = useSafeAreaInsets();
57
+ const { height: windowHeight } = useWindowDimensions();
58
+ const topInset = Math.max(insets.top + spacing.lg, 72);
59
+ const bottomInset = Math.max(insets.bottom + spacing.lg, 72);
60
+ const cardHeight = Math.min(
61
+ Math.max(520, Math.round(windowHeight * 0.76)),
62
+ windowHeight - topInset - bottomInset
63
+ );
64
+
65
+ return (
66
+ <Modal
67
+ visible={visible}
68
+ transparent
69
+ animationType="fade"
70
+ presentationStyle="overFullScreen"
71
+ onRequestClose={onClose}
72
+ >
73
+ <View style={styles.backdrop}>
74
+ <Pressable style={StyleSheet.absoluteFill} onPress={onClose} />
75
+ <View style={[styles.outer, { paddingTop: topInset, paddingBottom: bottomInset }]}>
76
+ <View style={[styles.card, { height: cardHeight }]}>
77
+ <View style={styles.handle} />
78
+ <View style={styles.header}>
79
+ <View style={styles.headerCopy}>
80
+ <Text style={styles.eyebrow}>Workspace</Text>
81
+ <Text style={styles.title}>Start directory</Text>
82
+ <Text style={styles.subtitle}>
83
+ Choose a known Codex workspace or browse any folder on the bridge host.
84
+ </Text>
85
+ </View>
86
+ <Pressable
87
+ onPress={onClose}
88
+ style={({ pressed }) => [styles.closeIconButton, pressed && styles.pressed]}
89
+ >
90
+ <Ionicons name="close" size={18} color={colors.textSecondary} />
91
+ </Pressable>
92
+ </View>
93
+
94
+ <View style={styles.body}>
95
+ <View style={styles.quickPicksSection}>
96
+ <Text style={styles.sectionTitle}>Quick picks</Text>
97
+ <ScrollView
98
+ horizontal
99
+ showsHorizontalScrollIndicator={false}
100
+ contentContainerStyle={styles.quickPicksContent}
101
+ >
102
+ <QuickPickChip
103
+ title="Bridge default"
104
+ meta="Auto"
105
+ selected={selectedPath === null}
106
+ onPress={() => onSelectPath(null)}
107
+ />
108
+ {!loadingRecent
109
+ ? recentWorkspaces.map((workspace) => (
110
+ <QuickPickChip
111
+ key={workspace.path}
112
+ title={toPathBasename(workspace.path)}
113
+ meta={workspace.chatCount === 1 ? '1 chat' : `${workspace.chatCount}`}
114
+ selected={workspace.path === selectedPath}
115
+ onPress={() => onSelectPath(workspace.path)}
116
+ />
117
+ ))
118
+ : null}
119
+ </ScrollView>
120
+ {loadingRecent ? (
121
+ <Text style={styles.inlineStatusText}>Refreshing known workspaces…</Text>
122
+ ) : null}
123
+ </View>
124
+
125
+ <View style={styles.browserPanel}>
126
+ <View style={styles.browserHeader}>
127
+ <View style={styles.browserHeaderCopy}>
128
+ <Text style={styles.sectionTitle}>Browse folders</Text>
129
+ <Text style={styles.pathValue} numberOfLines={2}>
130
+ {currentPath ?? bridgeRoot ?? 'Loading…'}
131
+ </Text>
132
+ </View>
133
+ <Pressable
134
+ onPress={() => parentPath && onBrowsePath(parentPath)}
135
+ disabled={!parentPath || loadingEntries}
136
+ style={({ pressed }) => [
137
+ styles.iconButton,
138
+ (!parentPath || loadingEntries) && styles.buttonDisabled,
139
+ pressed && parentPath && !loadingEntries && styles.pressed,
140
+ ]}
141
+ >
142
+ <Ionicons name="arrow-up-outline" size={18} color={colors.textPrimary} />
143
+ </Pressable>
144
+ </View>
145
+
146
+ <View style={styles.manualPathRow}>
147
+ <TextInput
148
+ value={draftPath}
149
+ onChangeText={onDraftPathChange}
150
+ keyboardAppearance="dark"
151
+ placeholder={bridgeRoot ?? '/path/to/workspace'}
152
+ placeholderTextColor={colors.textMuted}
153
+ style={styles.manualPathInput}
154
+ autoCapitalize="none"
155
+ autoCorrect={false}
156
+ returnKeyType="go"
157
+ onSubmitEditing={() => onBrowsePath(draftPath)}
158
+ />
159
+ <Pressable
160
+ onPress={() => onBrowsePath(draftPath)}
161
+ disabled={loadingEntries}
162
+ style={({ pressed }) => [
163
+ styles.secondaryButton,
164
+ loadingEntries && styles.buttonDisabled,
165
+ pressed && !loadingEntries && styles.pressed,
166
+ ]}
167
+ >
168
+ <Text style={styles.secondaryButtonText}>Open</Text>
169
+ </Pressable>
170
+ </View>
171
+
172
+ <View style={styles.browserActions}>
173
+ <Pressable
174
+ onPress={() => currentPath && onSelectPath(currentPath)}
175
+ disabled={!currentPath || loadingEntries}
176
+ style={({ pressed }) => [
177
+ styles.primaryButton,
178
+ (!currentPath || loadingEntries) && styles.buttonDisabled,
179
+ pressed && currentPath && !loadingEntries && styles.primaryButtonPressed,
180
+ ]}
181
+ >
182
+ <Ionicons name="checkmark-circle-outline" size={16} color={colors.bgMain} />
183
+ <Text style={styles.primaryButtonText}>Use this folder</Text>
184
+ </Pressable>
185
+ </View>
186
+
187
+ {error ? <Text style={styles.errorText}>{error}</Text> : null}
188
+
189
+ <View style={styles.entryListCard}>
190
+ {loadingEntries ? (
191
+ <LoadingRow label="Loading folders…" />
192
+ ) : entries.length > 0 ? (
193
+ <ScrollView
194
+ style={styles.entryListScroll}
195
+ contentContainerStyle={styles.entryListContent}
196
+ showsVerticalScrollIndicator={false}
197
+ keyboardShouldPersistTaps="handled"
198
+ >
199
+ {entries.map((entry) => (
200
+ <Pressable
201
+ key={entry.path}
202
+ onPress={() => onBrowsePath(entry.path)}
203
+ style={({ pressed }) => [
204
+ styles.entryRow,
205
+ entry.path === selectedPath && styles.entryRowSelected,
206
+ pressed && styles.pressed,
207
+ ]}
208
+ >
209
+ <View style={styles.entryIconWrap}>
210
+ <Ionicons
211
+ name={entry.isGitRepo ? 'git-branch-outline' : 'folder-outline'}
212
+ size={18}
213
+ color={colors.textPrimary}
214
+ />
215
+ </View>
216
+ <View style={styles.entryCopy}>
217
+ <Text style={styles.entryName} numberOfLines={1}>
218
+ {entry.name}
219
+ </Text>
220
+ <Text style={styles.entryPath} numberOfLines={1}>
221
+ {entry.path}
222
+ </Text>
223
+ </View>
224
+ <Ionicons
225
+ name="chevron-forward"
226
+ size={16}
227
+ color={colors.textMuted}
228
+ />
229
+ </Pressable>
230
+ ))}
231
+ </ScrollView>
232
+ ) : (
233
+ <EmptyRow label="No folders found here." />
234
+ )}
235
+ </View>
236
+ </View>
237
+ </View>
238
+ </View>
239
+ </View>
240
+ </View>
241
+ </Modal>
242
+ );
243
+ }
244
+
245
+ function QuickPickChip({
246
+ title,
247
+ meta,
248
+ selected,
249
+ onPress,
250
+ }: {
251
+ title: string;
252
+ meta?: string;
253
+ selected: boolean;
254
+ onPress: () => void;
255
+ }) {
256
+ return (
257
+ <Pressable
258
+ onPress={onPress}
259
+ style={({ pressed }) => [
260
+ styles.quickPickChip,
261
+ selected && styles.quickPickChipSelected,
262
+ pressed && styles.pressed,
263
+ ]}
264
+ >
265
+ <Text style={styles.quickPickTitle} numberOfLines={1}>
266
+ {title}
267
+ </Text>
268
+ {meta ? (
269
+ <Text style={styles.quickPickMeta} numberOfLines={1}>
270
+ {meta}
271
+ </Text>
272
+ ) : null}
273
+ {selected ? (
274
+ <Ionicons name="checkmark-circle" size={16} color={colors.textPrimary} />
275
+ ) : null}
276
+ </Pressable>
277
+ );
278
+ }
279
+
280
+ function LoadingRow({ label }: { label: string }) {
281
+ return (
282
+ <View style={styles.statusRow}>
283
+ <ActivityIndicator color={colors.textPrimary} />
284
+ <Text style={styles.statusText}>{label}</Text>
285
+ </View>
286
+ );
287
+ }
288
+
289
+ function EmptyRow({ label }: { label: string }) {
290
+ return (
291
+ <View style={styles.statusRow}>
292
+ <Text style={styles.statusText}>{label}</Text>
293
+ </View>
294
+ );
295
+ }
296
+
297
+ function toPathBasename(path: string): string {
298
+ const parts = path.split(/[\\/]/).filter(Boolean);
299
+ if (parts.length === 0) {
300
+ return path;
301
+ }
302
+ return parts[parts.length - 1] ?? path;
303
+ }
304
+
305
+ const styles = StyleSheet.create({
306
+ backdrop: {
307
+ flex: 1,
308
+ backgroundColor: 'rgba(0, 0, 0, 0.56)',
309
+ },
310
+ outer: {
311
+ flex: 1,
312
+ justifyContent: 'center',
313
+ paddingHorizontal: spacing.lg,
314
+ },
315
+ card: {
316
+ borderRadius: 26,
317
+ borderCurve: 'continuous',
318
+ backgroundColor: '#0F1218',
319
+ borderWidth: 1,
320
+ borderColor: colors.border,
321
+ overflow: 'hidden',
322
+ boxShadow: '0 24px 44px rgba(0, 0, 0, 0.34)',
323
+ },
324
+ handle: {
325
+ alignSelf: 'center',
326
+ width: 48,
327
+ height: 5,
328
+ borderRadius: radius.full,
329
+ backgroundColor: 'rgba(255,255,255,0.16)',
330
+ marginTop: spacing.sm,
331
+ },
332
+ header: {
333
+ flexDirection: 'row',
334
+ alignItems: 'flex-start',
335
+ justifyContent: 'space-between',
336
+ gap: spacing.md,
337
+ paddingHorizontal: spacing.xl,
338
+ paddingTop: spacing.lg,
339
+ paddingBottom: spacing.md,
340
+ borderBottomWidth: 1,
341
+ borderBottomColor: colors.borderLight,
342
+ },
343
+ headerCopy: {
344
+ flex: 1,
345
+ gap: spacing.xs,
346
+ },
347
+ eyebrow: {
348
+ ...typography.caption,
349
+ color: colors.textSecondary,
350
+ textTransform: 'uppercase',
351
+ letterSpacing: 1.2,
352
+ },
353
+ title: {
354
+ ...typography.largeTitle,
355
+ fontSize: 22,
356
+ },
357
+ subtitle: {
358
+ ...typography.body,
359
+ color: colors.textMuted,
360
+ },
361
+ closeIconButton: {
362
+ marginTop: 2,
363
+ width: 34,
364
+ height: 34,
365
+ borderRadius: radius.full,
366
+ borderWidth: 1,
367
+ borderColor: colors.borderLight,
368
+ alignItems: 'center',
369
+ justifyContent: 'center',
370
+ backgroundColor: 'rgba(255,255,255,0.04)',
371
+ },
372
+ body: {
373
+ flex: 1,
374
+ paddingHorizontal: spacing.xl,
375
+ paddingTop: spacing.lg,
376
+ paddingBottom: spacing.xl,
377
+ gap: spacing.lg,
378
+ },
379
+ quickPicksSection: {
380
+ gap: spacing.sm,
381
+ },
382
+ quickPicksContent: {
383
+ gap: spacing.sm,
384
+ paddingRight: spacing.lg,
385
+ },
386
+ quickPickChip: {
387
+ minWidth: 112,
388
+ maxWidth: 172,
389
+ minHeight: 54,
390
+ borderRadius: radius.lg,
391
+ borderWidth: 1,
392
+ borderColor: colors.borderLight,
393
+ backgroundColor: colors.bgItem,
394
+ paddingHorizontal: spacing.md,
395
+ paddingVertical: spacing.sm,
396
+ justifyContent: 'center',
397
+ gap: 2,
398
+ },
399
+ quickPickChipSelected: {
400
+ borderColor: colors.borderHighlight,
401
+ backgroundColor: 'rgba(255,255,255,0.10)',
402
+ },
403
+ quickPickTitle: {
404
+ ...typography.body,
405
+ fontWeight: '600',
406
+ },
407
+ quickPickMeta: {
408
+ ...typography.caption,
409
+ color: colors.textMuted,
410
+ },
411
+ inlineStatusText: {
412
+ ...typography.caption,
413
+ color: colors.textMuted,
414
+ },
415
+ sectionTitle: {
416
+ ...typography.headline,
417
+ },
418
+ browserPanel: {
419
+ flex: 1,
420
+ gap: spacing.md,
421
+ },
422
+ browserHeader: {
423
+ flexDirection: 'row',
424
+ gap: spacing.sm,
425
+ alignItems: 'center',
426
+ },
427
+ browserHeaderCopy: {
428
+ flex: 1,
429
+ gap: spacing.xs,
430
+ },
431
+ iconButton: {
432
+ width: 42,
433
+ height: 42,
434
+ borderRadius: radius.lg,
435
+ borderWidth: 1,
436
+ borderColor: colors.border,
437
+ backgroundColor: colors.bgItem,
438
+ alignItems: 'center',
439
+ justifyContent: 'center',
440
+ },
441
+ pathValue: {
442
+ ...typography.mono,
443
+ fontSize: 11,
444
+ },
445
+ browserActions: {
446
+ flexDirection: 'row',
447
+ gap: spacing.sm,
448
+ },
449
+ manualPathRow: {
450
+ flexDirection: 'row',
451
+ gap: spacing.sm,
452
+ alignItems: 'center',
453
+ },
454
+ manualPathInput: {
455
+ flex: 1,
456
+ minHeight: 46,
457
+ borderRadius: radius.lg,
458
+ borderWidth: 1,
459
+ borderColor: colors.borderLight,
460
+ backgroundColor: colors.bgInput,
461
+ paddingHorizontal: spacing.md,
462
+ ...typography.body,
463
+ },
464
+ primaryButton: {
465
+ minHeight: 46,
466
+ borderRadius: radius.lg,
467
+ backgroundColor: colors.textPrimary,
468
+ alignItems: 'center',
469
+ justifyContent: 'center',
470
+ flexDirection: 'row',
471
+ gap: spacing.xs,
472
+ paddingHorizontal: spacing.md,
473
+ flex: 1,
474
+ },
475
+ primaryButtonPressed: {
476
+ opacity: 0.82,
477
+ },
478
+ primaryButtonText: {
479
+ ...typography.body,
480
+ color: colors.bgMain,
481
+ fontWeight: '600',
482
+ },
483
+ secondaryButton: {
484
+ minHeight: 46,
485
+ borderRadius: radius.lg,
486
+ borderWidth: 1,
487
+ borderColor: colors.border,
488
+ backgroundColor: colors.bgItem,
489
+ alignItems: 'center',
490
+ justifyContent: 'center',
491
+ flexDirection: 'row',
492
+ gap: spacing.xs,
493
+ paddingHorizontal: spacing.md,
494
+ },
495
+ secondaryButtonText: {
496
+ ...typography.body,
497
+ fontWeight: '600',
498
+ },
499
+ buttonDisabled: {
500
+ opacity: 0.42,
501
+ },
502
+ entryListCard: {
503
+ flex: 1,
504
+ minHeight: 240,
505
+ borderRadius: radius.lg,
506
+ borderWidth: 1,
507
+ borderColor: colors.borderLight,
508
+ backgroundColor: colors.bgItem,
509
+ overflow: 'hidden',
510
+ },
511
+ entryListScroll: {
512
+ flex: 1,
513
+ },
514
+ entryListContent: {
515
+ paddingBottom: spacing.sm,
516
+ },
517
+ entryRow: {
518
+ flexDirection: 'row',
519
+ alignItems: 'center',
520
+ gap: spacing.sm,
521
+ paddingHorizontal: spacing.md,
522
+ paddingVertical: spacing.md,
523
+ backgroundColor: colors.bgItem,
524
+ borderBottomWidth: StyleSheet.hairlineWidth,
525
+ borderBottomColor: colors.borderLight,
526
+ },
527
+ entryRowSelected: {
528
+ backgroundColor: 'rgba(255,255,255,0.10)',
529
+ },
530
+ entryIconWrap: {
531
+ width: 34,
532
+ height: 34,
533
+ borderRadius: radius.md,
534
+ alignItems: 'center',
535
+ justifyContent: 'center',
536
+ backgroundColor: 'rgba(255,255,255,0.06)',
537
+ borderWidth: 1,
538
+ borderColor: colors.borderLight,
539
+ },
540
+ entryCopy: {
541
+ flex: 1,
542
+ gap: 2,
543
+ },
544
+ entryName: {
545
+ ...typography.body,
546
+ fontWeight: '600',
547
+ },
548
+ entryPath: {
549
+ ...typography.caption,
550
+ color: colors.textMuted,
551
+ },
552
+ errorText: {
553
+ ...typography.caption,
554
+ color: '#FF8A8A',
555
+ },
556
+ statusRow: {
557
+ flex: 1,
558
+ minHeight: 120,
559
+ alignItems: 'center',
560
+ justifyContent: 'center',
561
+ gap: spacing.sm,
562
+ paddingHorizontal: spacing.lg,
563
+ },
564
+ statusText: {
565
+ ...typography.body,
566
+ textAlign: 'center',
567
+ color: colors.textMuted,
568
+ },
569
+ pressed: {
570
+ opacity: 0.84,
571
+ },
572
+ });
@@ -0,0 +1,35 @@
1
+ import { resolveComposerBottomSpacing } from '../chat-input-layout';
2
+
3
+ describe('resolveComposerBottomSpacing', () => {
4
+ it('keeps a small fixed reserve for iPhones with a home indicator', () => {
5
+ expect(resolveComposerBottomSpacing('ios', 34, false)).toEqual({
6
+ baseBottomPadding: 6,
7
+ extraBottomInset: 8,
8
+ totalBottomPadding: 14,
9
+ });
10
+ });
11
+
12
+ it('collapses the bottom reserve while the keyboard is visible on iOS', () => {
13
+ expect(resolveComposerBottomSpacing('ios', 34, true)).toEqual({
14
+ baseBottomPadding: 2,
15
+ extraBottomInset: 0,
16
+ totalBottomPadding: 2,
17
+ });
18
+ });
19
+
20
+ it('treats Android gesture navigation as nearly flush', () => {
21
+ expect(resolveComposerBottomSpacing('android', 8, false)).toEqual({
22
+ baseBottomPadding: 8,
23
+ extraBottomInset: 2,
24
+ totalBottomPadding: 10,
25
+ });
26
+ });
27
+
28
+ it('keeps extra clearance for Android phones with visible nav buttons', () => {
29
+ expect(resolveComposerBottomSpacing('android', 24, false)).toEqual({
30
+ baseBottomPadding: 8,
31
+ extraBottomInset: 8,
32
+ totalBottomPadding: 16,
33
+ });
34
+ });
35
+ });
@@ -0,0 +1,44 @@
1
+ import { toMarkdownImageSource } from '../chatImageSource';
2
+
3
+ describe('chatImageSource', () => {
4
+ it('keeps remote https images direct', () => {
5
+ expect(
6
+ toMarkdownImageSource(
7
+ 'https://example.com/image.png',
8
+ 'http://192.168.1.26:8787',
9
+ 'secret-token'
10
+ )
11
+ ).toEqual({
12
+ uri: 'https://example.com/image.png',
13
+ });
14
+ });
15
+
16
+ it('proxies absolute local paths through the bridge', () => {
17
+ expect(
18
+ toMarkdownImageSource('/tmp/My QR.png', 'http://192.168.1.26:8787', 'secret-token')
19
+ ).toEqual({
20
+ uri: 'http://192.168.1.26:8787/local-image?path=%2Ftmp%2FMy%20QR.png',
21
+ headers: {
22
+ Authorization: 'Bearer secret-token',
23
+ },
24
+ });
25
+ });
26
+
27
+ it('proxies file scheme paths through the bridge', () => {
28
+ expect(
29
+ toMarkdownImageSource(
30
+ 'file:///Users/mohitpatil/Desktop/bridge.png',
31
+ 'http://192.168.1.26:8787',
32
+ null
33
+ )
34
+ ).toEqual({
35
+ uri: 'http://192.168.1.26:8787/local-image?path=%2FUsers%2Fmohitpatil%2FDesktop%2Fbridge.png',
36
+ });
37
+ });
38
+
39
+ it('returns null for unsupported relative paths', () => {
40
+ expect(
41
+ toMarkdownImageSource('./relative.png', 'http://192.168.1.26:8787', 'secret-token')
42
+ ).toBeNull();
43
+ });
44
+ });