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
@@ -419,6 +419,7 @@ export function OnboardingScreen({
419
419
  setFormError(null);
420
420
  setConnectionCheck({ kind: 'idle' });
421
421
  }}
422
+ keyboardAppearance="dark"
422
423
  autoCapitalize="none"
423
424
  autoCorrect={false}
424
425
  keyboardType="url"
@@ -441,6 +442,7 @@ export function OnboardingScreen({
441
442
  setTokenInput(value);
442
443
  setConnectionCheck({ kind: 'idle' });
443
444
  }}
445
+ keyboardAppearance="dark"
444
446
  autoCapitalize="none"
445
447
  autoCorrect={false}
446
448
  keyboardType="default"
@@ -4,18 +4,25 @@ import { LinearGradient } from 'expo-linear-gradient';
4
4
  import { useCallback, useEffect, useMemo, useState } from 'react';
5
5
  import {
6
6
  ActivityIndicator,
7
- Modal,
8
7
  Pressable,
9
8
  ScrollView,
10
9
  StyleSheet,
10
+ Switch,
11
11
  Text,
12
12
  View,
13
13
  } from 'react-native';
14
14
  import { SafeAreaView } from 'react-native-safe-area-context';
15
15
 
16
16
  import type { HostBridgeApiClient } from '../api/client';
17
- import type { ApprovalMode, ModelOption, ReasoningEffort } from '../api/types';
17
+ import type {
18
+ AccountSnapshot,
19
+ ApprovalMode,
20
+ ModelOption,
21
+ PlanType,
22
+ ReasoningEffort,
23
+ } from '../api/types';
18
24
  import type { HostBridgeWsClient } from '../api/ws';
25
+ import { SelectionSheet, type SelectionSheetOption } from '../components/SelectionSheet';
19
26
  import { colors, radius, spacing, typography } from '../theme';
20
27
 
21
28
  interface SettingsScreenProps {
@@ -25,11 +32,13 @@ interface SettingsScreenProps {
25
32
  defaultModelId?: string | null;
26
33
  defaultReasoningEffort?: ReasoningEffort | null;
27
34
  approvalMode?: ApprovalMode;
35
+ showToolCalls?: boolean;
28
36
  onDefaultModelSettingsChange?: (
29
37
  modelId: string | null,
30
38
  effort: ReasoningEffort | null
31
39
  ) => void;
32
40
  onApprovalModeChange?: (mode: ApprovalMode) => void;
41
+ onShowToolCallsChange?: (value: boolean) => void;
33
42
  onEditBridgeUrl?: () => void;
34
43
  onResetOnboarding?: () => void;
35
44
  onOpenDrawer: () => void;
@@ -44,8 +53,10 @@ export function SettingsScreen({
44
53
  defaultModelId,
45
54
  defaultReasoningEffort,
46
55
  approvalMode,
56
+ showToolCalls = false,
47
57
  onDefaultModelSettingsChange,
48
58
  onApprovalModeChange,
59
+ onShowToolCallsChange,
49
60
  onEditBridgeUrl,
50
61
  onResetOnboarding,
51
62
  onOpenDrawer,
@@ -61,6 +72,9 @@ export function SettingsScreen({
61
72
  const [modelModalVisible, setModelModalVisible] = useState(false);
62
73
  const [effortModalVisible, setEffortModalVisible] = useState(false);
63
74
  const [approvalModeModalVisible, setApprovalModeModalVisible] = useState(false);
75
+ const [account, setAccount] = useState<AccountSnapshot | null>(null);
76
+ const [accountLoading, setAccountLoading] = useState(false);
77
+ const [accountError, setAccountError] = useState<string | null>(null);
64
78
 
65
79
  const normalizedDefaultModelId = normalizeModelId(defaultModelId);
66
80
  const normalizedDefaultEffort = normalizeReasoningEffort(defaultReasoningEffort);
@@ -115,15 +129,48 @@ export function SettingsScreen({
115
129
  }
116
130
  }, [api]);
117
131
 
132
+ const loadAccount = useCallback(async () => {
133
+ setAccountLoading(true);
134
+ try {
135
+ const snapshot = await api.readAccount();
136
+ setAccount(snapshot);
137
+ setAccountError(null);
138
+ } catch (err) {
139
+ setAccountError((err as Error).message);
140
+ } finally {
141
+ setAccountLoading(false);
142
+ }
143
+ }, [api]);
144
+
118
145
  useEffect(() => {
119
146
  const t = setTimeout(() => {
120
147
  void checkHealth();
121
148
  void refreshModelOptions();
149
+ void loadAccount();
122
150
  }, 0);
123
151
  return () => clearTimeout(t);
124
- }, [checkHealth, refreshModelOptions]);
152
+ }, [checkHealth, loadAccount, refreshModelOptions]);
125
153
 
126
- useEffect(() => ws.onStatus(setWsConnected), [ws]);
154
+ useEffect(
155
+ () =>
156
+ ws.onStatus((connected) => {
157
+ setWsConnected(connected);
158
+ if (connected) {
159
+ void loadAccount();
160
+ }
161
+ }),
162
+ [loadAccount, ws]
163
+ );
164
+
165
+ useEffect(
166
+ () =>
167
+ ws.onEvent((event) => {
168
+ if (event.method === 'account/updated') {
169
+ void loadAccount();
170
+ }
171
+ }),
172
+ [loadAccount, ws]
173
+ );
127
174
 
128
175
  const openModelModal = useCallback(() => {
129
176
  setModelModalVisible(true);
@@ -223,6 +270,88 @@ export function SettingsScreen({
223
270
  [onApprovalModeChange]
224
271
  );
225
272
 
273
+ const approvalModeOptions = useMemo<SelectionSheetOption[]>(
274
+ () => [
275
+ {
276
+ key: 'normal',
277
+ title: 'Normal approvals',
278
+ description: 'Ask before commands and file-changing actions run.',
279
+ icon: 'shield-checkmark-outline',
280
+ selected: normalizedApprovalMode === 'normal',
281
+ onPress: () => selectApprovalMode('normal'),
282
+ },
283
+ {
284
+ key: 'yolo',
285
+ title: 'YOLO approvals',
286
+ description: 'Run commands without prompting for approval.',
287
+ icon: 'flash-outline',
288
+ meta: 'Unsafe',
289
+ selected: normalizedApprovalMode === 'yolo',
290
+ onPress: () => selectApprovalMode('yolo'),
291
+ },
292
+ ],
293
+ [normalizedApprovalMode, selectApprovalMode]
294
+ );
295
+
296
+ const modelPickerOptions = useMemo<SelectionSheetOption[]>(
297
+ () => [
298
+ {
299
+ key: 'server-default',
300
+ title: 'Use server default',
301
+ description: 'Follow the bridge default model for new chats.',
302
+ icon: 'sparkles-outline',
303
+ badge: 'Auto',
304
+ selected: normalizedDefaultModelId === null,
305
+ onPress: () => selectDefaultModel(null),
306
+ },
307
+ ...modelOptions.map((model) => ({
308
+ key: model.id,
309
+ title: model.displayName,
310
+ description: model.description?.trim() || model.id,
311
+ icon: 'hardware-chip-outline' as const,
312
+ badge: model.isDefault ? 'Default' : undefined,
313
+ meta: model.defaultReasoningEffort
314
+ ? formatReasoningEffort(model.defaultReasoningEffort)
315
+ : undefined,
316
+ selected: model.id === normalizedDefaultModelId,
317
+ onPress: () => selectDefaultModel(model.id),
318
+ })),
319
+ ],
320
+ [modelOptions, normalizedDefaultModelId, selectDefaultModel]
321
+ );
322
+
323
+ const effortPickerOptions = useMemo<SelectionSheetOption[]>(
324
+ () => [
325
+ {
326
+ key: 'model-default',
327
+ title: 'Use model default',
328
+ description: selectedDefaultModel
329
+ ? `Follow ${selectedDefaultModel.displayName}'s default reasoning.`
330
+ : 'Follow the model default reasoning level.',
331
+ icon: 'sparkles-outline',
332
+ badge: 'Auto',
333
+ selected: normalizedDefaultEffort === null,
334
+ onPress: () => selectDefaultEffort(null),
335
+ },
336
+ ...selectedDefaultModelEfforts.map((option) => ({
337
+ key: option.effort,
338
+ title: formatReasoningEffort(option.effort),
339
+ description:
340
+ option.description?.trim() ||
341
+ 'Override the default reasoning depth for new chats.',
342
+ icon: 'pulse-outline' as const,
343
+ selected: option.effort === normalizedDefaultEffort,
344
+ onPress: () => selectDefaultEffort(option.effort),
345
+ })),
346
+ ],
347
+ [
348
+ normalizedDefaultEffort,
349
+ selectDefaultEffort,
350
+ selectedDefaultModel,
351
+ selectedDefaultModelEfforts,
352
+ ]
353
+ );
354
+
226
355
  return (
227
356
  <View style={styles.container}>
228
357
  <LinearGradient
@@ -297,9 +426,68 @@ export function SettingsScreen({
297
426
  </BlurView>
298
427
  <Text style={styles.subtleHintText}>
299
428
  This controls command/file-change approvals only. It does not affect
300
- request_user_input questions.
429
+ request_user_input questions. Mobile chats request full Codex sandbox
430
+ access by default.
301
431
  </Text>
302
432
 
433
+ <Text style={[styles.sectionLabel, styles.sectionLabelGap]}>Transcript</Text>
434
+ <BlurView intensity={50} tint="dark" style={styles.card}>
435
+ <View style={[styles.settingRow, styles.settingRowLast]}>
436
+ <View style={styles.settingRowLeft}>
437
+ <Text style={styles.rowLabel}>Show tool calls</Text>
438
+ <Text style={styles.settingValue} numberOfLines={2}>
439
+ Show web searches, MCP/OpenAI docs calls, commands, and file changes.
440
+ </Text>
441
+ </View>
442
+ <Switch
443
+ value={showToolCalls}
444
+ onValueChange={(value) => onShowToolCallsChange?.(value)}
445
+ trackColor={{ false: colors.borderLight, true: colors.accent }}
446
+ thumbColor={colors.textPrimary}
447
+ ios_backgroundColor={colors.borderLight}
448
+ />
449
+ </View>
450
+ </BlurView>
451
+ <Text style={styles.subtleHintText}>
452
+ Live tool activity stays in a capped panel so the chat list does not
453
+ start jumping while a turn is running.
454
+ </Text>
455
+
456
+ <Text style={[styles.sectionLabel, styles.sectionLabelGap]}>Account & Auth</Text>
457
+ <BlurView intensity={50} tint="dark" style={styles.card}>
458
+ {accountLoading ? (
459
+ <View style={styles.accountLoadingState}>
460
+ <ActivityIndicator color={colors.textPrimary} />
461
+ <Text style={styles.settingValue}>Loading account details…</Text>
462
+ </View>
463
+ ) : (
464
+ <>
465
+ <Row
466
+ label="Status"
467
+ value={formatAccountType(account)}
468
+ valueColor={account?.type ? colors.statusComplete : colors.textMuted}
469
+ />
470
+ {account?.email ? (
471
+ <Row label="Email" value={account.email} />
472
+ ) : null}
473
+ {account?.planType ? (
474
+ <Row label="Plan" value={formatPlanType(account.planType)} />
475
+ ) : null}
476
+ <Row
477
+ label="Bridge auth"
478
+ value={account?.requiresOpenaiAuth ? 'Required' : 'Optional'}
479
+ isLast
480
+ />
481
+ </>
482
+ )}
483
+ </BlurView>
484
+ {account?.type === null && account?.requiresOpenaiAuth ? (
485
+ <Text style={styles.subtleHintText}>
486
+ The bridge expects OpenAI auth, but mobile does not expose a login flow yet.
487
+ </Text>
488
+ ) : null}
489
+ {accountError ? <Text style={styles.errorText}>{accountError}</Text> : null}
490
+
303
491
  <Text style={[styles.sectionLabel, styles.sectionLabelGap]}>Bridge</Text>
304
492
  <BlurView intensity={50} tint="dark" style={styles.card}>
305
493
  <Text selectable style={styles.valueText}>
@@ -348,11 +536,12 @@ export function SettingsScreen({
348
536
  onPress={() => {
349
537
  void checkHealth();
350
538
  void refreshModelOptions();
539
+ void loadAccount();
351
540
  }}
352
541
  style={({ pressed }) => [styles.refreshBtn, pressed && styles.refreshBtnPressed]}
353
542
  >
354
543
  <Ionicons name="refresh" size={16} color={colors.white} />
355
- <Text style={styles.refreshBtnText}>Refresh health</Text>
544
+ <Text style={styles.refreshBtnText}>Refresh settings</Text>
356
545
  </Pressable>
357
546
 
358
547
  <Text style={[styles.sectionLabel, styles.sectionLabelGap]}>Legal</Text>
@@ -383,127 +572,40 @@ export function SettingsScreen({
383
572
  </ScrollView>
384
573
  </SafeAreaView>
385
574
 
386
- <Modal
575
+ <SelectionSheet
387
576
  visible={approvalModeModalVisible}
388
- transparent
389
- animationType="fade"
390
- onRequestClose={() => setApprovalModeModalVisible(false)}
391
- >
392
- <View style={styles.modalBackdrop}>
393
- <View style={styles.modalCard}>
394
- <Text style={styles.modalTitle}>Execution approval mode</Text>
395
- <ScrollView style={styles.modalList} contentContainerStyle={styles.modalListContent}>
396
- <OptionRow
397
- label="Normal — Ask for approvals"
398
- selected={normalizedApprovalMode === 'normal'}
399
- onPress={() => selectApprovalMode('normal')}
400
- />
401
- <OptionRow
402
- label="YOLO — Do not ask approvals"
403
- selected={normalizedApprovalMode === 'yolo'}
404
- onPress={() => selectApprovalMode('yolo')}
405
- />
406
- </ScrollView>
407
- <View style={styles.modalActions}>
408
- <Pressable
409
- onPress={() => setApprovalModeModalVisible(false)}
410
- style={({ pressed }) => [
411
- styles.modalCloseBtn,
412
- pressed && styles.workspaceModalCloseBtnPressed,
413
- ]}
414
- >
415
- <Text style={styles.modalCloseText}>Close</Text>
416
- </Pressable>
417
- </View>
418
- </View>
419
- </View>
420
- </Modal>
577
+ eyebrow="Approvals"
578
+ title="Execution approval mode"
579
+ subtitle="This only affects command and file-change approvals."
580
+ options={approvalModeOptions}
581
+ onClose={() => setApprovalModeModalVisible(false)}
582
+ />
421
583
 
422
- <Modal
584
+ <SelectionSheet
423
585
  visible={modelModalVisible}
424
- transparent
425
- animationType="fade"
426
- onRequestClose={closeModelModal}
427
- >
428
- <View style={styles.modalBackdrop}>
429
- <View style={styles.modalCard}>
430
- <Text style={styles.modalTitle}>Default model</Text>
431
- {loadingModels ? (
432
- <ActivityIndicator color={colors.textPrimary} style={styles.modalLoader} />
433
- ) : (
434
- <ScrollView style={styles.modalList} contentContainerStyle={styles.modalListContent}>
435
- <OptionRow
436
- label="Server default"
437
- selected={normalizedDefaultModelId === null}
438
- onPress={() => selectDefaultModel(null)}
439
- />
440
- {modelOptions.map((model) => (
441
- <OptionRow
442
- key={model.id}
443
- label={`${model.displayName} (${model.id})`}
444
- selected={model.id === normalizedDefaultModelId}
445
- onPress={() => selectDefaultModel(model.id)}
446
- />
447
- ))}
448
- </ScrollView>
449
- )}
450
- <View style={styles.modalActions}>
451
- <Pressable
452
- onPress={closeModelModal}
453
- style={({ pressed }) => [
454
- styles.modalCloseBtn,
455
- pressed && styles.workspaceModalCloseBtnPressed,
456
- ]}
457
- >
458
- <Text style={styles.modalCloseText}>Close</Text>
459
- </Pressable>
460
- </View>
461
- </View>
462
- </View>
463
- </Modal>
586
+ eyebrow="Defaults"
587
+ title="Default model"
588
+ subtitle="Pick the model new chats should start with."
589
+ options={modelPickerOptions}
590
+ loading={loadingModels}
591
+ loadingLabel="Refreshing available models…"
592
+ presentation="expanded"
593
+ onClose={closeModelModal}
594
+ />
464
595
 
465
- <Modal
596
+ <SelectionSheet
466
597
  visible={effortModalVisible}
467
- transparent
468
- animationType="fade"
469
- onRequestClose={() => setEffortModalVisible(false)}
470
- >
471
- <View style={styles.modalBackdrop}>
472
- <View style={styles.modalCard}>
473
- <Text style={styles.modalTitle}>Default reasoning</Text>
474
- <ScrollView style={styles.modalList} contentContainerStyle={styles.modalListContent}>
475
- <OptionRow
476
- label="Model default"
477
- selected={normalizedDefaultEffort === null}
478
- onPress={() => selectDefaultEffort(null)}
479
- />
480
- {selectedDefaultModelEfforts.map((option) => (
481
- <OptionRow
482
- key={option.effort}
483
- label={
484
- option.description
485
- ? `${formatReasoningEffort(option.effort)} — ${option.description}`
486
- : formatReasoningEffort(option.effort)
487
- }
488
- selected={option.effort === normalizedDefaultEffort}
489
- onPress={() => selectDefaultEffort(option.effort)}
490
- />
491
- ))}
492
- </ScrollView>
493
- <View style={styles.modalActions}>
494
- <Pressable
495
- onPress={() => setEffortModalVisible(false)}
496
- style={({ pressed }) => [
497
- styles.modalCloseBtn,
498
- pressed && styles.workspaceModalCloseBtnPressed,
499
- ]}
500
- >
501
- <Text style={styles.modalCloseText}>Close</Text>
502
- </Pressable>
503
- </View>
504
- </View>
505
- </View>
506
- </Modal>
598
+ eyebrow="Defaults"
599
+ title="Default reasoning"
600
+ subtitle={
601
+ selectedDefaultModel
602
+ ? `Current model: ${selectedDefaultModel.displayName}`
603
+ : 'Choose the default reasoning depth for new chats.'
604
+ }
605
+ options={effortPickerOptions}
606
+ presentation="expanded"
607
+ onClose={() => setEffortModalVisible(false)}
608
+ />
507
609
  </View>
508
610
  );
509
611
  }
@@ -529,30 +631,6 @@ function Row({
529
631
  );
530
632
  }
531
633
 
532
- function OptionRow({
533
- label,
534
- selected,
535
- onPress,
536
- }: {
537
- label: string;
538
- selected: boolean;
539
- onPress: () => void;
540
- }) {
541
- return (
542
- <Pressable
543
- onPress={onPress}
544
- style={({ pressed }) => [
545
- styles.optionRow,
546
- selected && styles.optionRowSelected,
547
- pressed && styles.optionRowPressed,
548
- ]}
549
- >
550
- <Text style={[styles.optionRowText, selected && styles.optionRowTextSelected]}>{label}</Text>
551
- {selected ? <Ionicons name="checkmark" size={16} color={colors.textPrimary} /> : null}
552
- </Pressable>
553
- );
554
- }
555
-
556
634
  const styles = StyleSheet.create({
557
635
  container: { flex: 1, backgroundColor: colors.bgMain },
558
636
  safeArea: { flex: 1 },
@@ -674,6 +752,12 @@ const styles = StyleSheet.create({
674
752
  marginTop: spacing.xs,
675
753
  marginHorizontal: spacing.xs,
676
754
  },
755
+ accountLoadingState: {
756
+ minHeight: 88,
757
+ alignItems: 'center',
758
+ justifyContent: 'center',
759
+ gap: spacing.sm,
760
+ },
677
761
  refreshBtn: {
678
762
  flexDirection: 'row',
679
763
  alignItems: 'center',
@@ -684,11 +768,7 @@ const styles = StyleSheet.create({
684
768
  paddingHorizontal: spacing.md,
685
769
  backgroundColor: colors.accent,
686
770
  borderRadius: radius.md,
687
- shadowColor: colors.accent,
688
- shadowOffset: { width: 0, height: 4 },
689
- shadowOpacity: 0.3,
690
- shadowRadius: 8,
691
- elevation: 4,
771
+ boxShadow: `0px 4px 8px ${colors.accent}4D`,
692
772
  },
693
773
  refreshBtnPressed: { backgroundColor: colors.accentPressed },
694
774
  refreshBtnText: { ...typography.headline, color: colors.white, fontSize: 15 },
@@ -711,81 +791,6 @@ const styles = StyleSheet.create({
711
791
  color: colors.textPrimary,
712
792
  fontWeight: '600',
713
793
  },
714
- modalBackdrop: {
715
- flex: 1,
716
- backgroundColor: 'rgba(0, 0, 0, 0.55)',
717
- justifyContent: 'center',
718
- paddingHorizontal: spacing.lg,
719
- },
720
- modalCard: {
721
- backgroundColor: colors.bgItem,
722
- borderRadius: 14,
723
- borderWidth: 1,
724
- borderColor: colors.border,
725
- padding: spacing.lg,
726
- gap: spacing.md,
727
- maxHeight: '74%',
728
- },
729
- modalTitle: {
730
- ...typography.headline,
731
- color: colors.textPrimary,
732
- },
733
- modalLoader: {
734
- marginVertical: spacing.lg,
735
- },
736
- modalList: {
737
- maxHeight: 320,
738
- },
739
- modalListContent: {
740
- gap: spacing.xs,
741
- },
742
- optionRow: {
743
- flexDirection: 'row',
744
- alignItems: 'center',
745
- justifyContent: 'space-between',
746
- gap: spacing.sm,
747
- borderRadius: 10,
748
- borderWidth: StyleSheet.hairlineWidth,
749
- borderColor: colors.borderLight,
750
- backgroundColor: colors.bgMain,
751
- paddingHorizontal: spacing.md,
752
- paddingVertical: spacing.sm,
753
- },
754
- optionRowSelected: {
755
- borderColor: colors.borderHighlight,
756
- backgroundColor: 'rgba(255, 255, 255, 0.08)',
757
- },
758
- optionRowPressed: {
759
- opacity: 0.86,
760
- },
761
- optionRowText: {
762
- ...typography.caption,
763
- color: colors.textSecondary,
764
- flex: 1,
765
- },
766
- optionRowTextSelected: {
767
- color: colors.textPrimary,
768
- fontWeight: '600',
769
- },
770
- modalActions: {
771
- flexDirection: 'row',
772
- justifyContent: 'flex-end',
773
- },
774
- modalCloseBtn: {
775
- borderRadius: 10,
776
- paddingHorizontal: spacing.lg,
777
- paddingVertical: spacing.sm,
778
- borderWidth: 1,
779
- borderColor: colors.border,
780
- backgroundColor: colors.bgMain,
781
- },
782
- workspaceModalCloseBtnPressed: {
783
- opacity: 0.85,
784
- },
785
- modalCloseText: {
786
- ...typography.body,
787
- color: colors.textPrimary,
788
- },
789
794
  errorText: {
790
795
  ...typography.caption,
791
796
  color: colors.error,
@@ -838,3 +843,28 @@ function formatReasoningEffort(effort: ReasoningEffort): string {
838
843
 
839
844
  return effort.charAt(0).toUpperCase() + effort.slice(1);
840
845
  }
846
+
847
+ function formatAccountType(account: AccountSnapshot | null): string {
848
+ if (!account?.type) {
849
+ return 'Signed out';
850
+ }
851
+
852
+ return account.type === 'chatgpt' ? 'ChatGPT' : 'API key';
853
+ }
854
+
855
+ function formatPlanType(planType: PlanType): string {
856
+ if (planType === 'pro') {
857
+ return 'Pro';
858
+ }
859
+ if (planType === 'plus') {
860
+ return 'Plus';
861
+ }
862
+ if (planType === 'go') {
863
+ return 'Go';
864
+ }
865
+ if (planType === 'edu') {
866
+ return 'Edu';
867
+ }
868
+
869
+ return planType.charAt(0).toUpperCase() + planType.slice(1);
870
+ }
@@ -127,6 +127,7 @@ export function TerminalScreen({ api, ws, onOpenDrawer }: TerminalScreenProps) {
127
127
  style={styles.input}
128
128
  value={command}
129
129
  onChangeText={setCommand}
130
+ keyboardAppearance="dark"
130
131
  autoCapitalize="none"
131
132
  autoCorrect={false}
132
133
  returnKeyType="send"
@@ -171,11 +172,7 @@ const styles = StyleSheet.create({
171
172
  borderWidth: StyleSheet.hairlineWidth,
172
173
  borderColor: 'rgba(255,255,255,0.1)',
173
174
  overflow: 'hidden',
174
- shadowColor: '#000',
175
- shadowOffset: { width: 0, height: 10 },
176
- shadowOpacity: 0.5,
177
- shadowRadius: 20,
178
- elevation: 8,
175
+ boxShadow: '0px 10px 20px rgba(0, 0, 0, 0.5)',
179
176
  },
180
177
  windowHeader: {
181
178
  flexDirection: 'row',