clawdex-mobile 2.0.0 → 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.
- package/.github/workflows/pages.yml +41 -0
- package/AGENTS.md +263 -110
- package/README.md +11 -0
- package/apps/mobile/.env.example +2 -2
- package/apps/mobile/App.tsx +175 -14
- package/apps/mobile/app.json +27 -9
- package/apps/mobile/eas.json +14 -4
- package/apps/mobile/package.json +13 -13
- package/apps/mobile/src/api/__tests__/chatMapping.test.ts +219 -0
- package/apps/mobile/src/api/__tests__/client.test.ts +579 -6
- package/apps/mobile/src/api/__tests__/ws.test.ts +27 -0
- package/apps/mobile/src/api/account.ts +47 -0
- package/apps/mobile/src/api/chatMapping.ts +435 -18
- package/apps/mobile/src/api/client.ts +296 -36
- package/apps/mobile/src/api/rateLimits.ts +143 -0
- package/apps/mobile/src/api/types.ts +106 -0
- package/apps/mobile/src/api/ws.ts +10 -1
- package/apps/mobile/src/components/ChatHeader.tsx +12 -12
- package/apps/mobile/src/components/ChatInput.tsx +154 -88
- package/apps/mobile/src/components/ChatMessage.tsx +548 -93
- package/apps/mobile/src/components/ComposerUsageLimits.tsx +167 -0
- package/apps/mobile/src/components/SelectionSheet.tsx +466 -0
- package/apps/mobile/src/components/ToolBlock.tsx +17 -15
- package/apps/mobile/src/components/VoiceRecordingWaveform.tsx +181 -0
- package/apps/mobile/src/components/WorkspacePickerModal.tsx +572 -0
- package/apps/mobile/src/components/__tests__/chat-input-layout.test.ts +35 -0
- package/apps/mobile/src/components/__tests__/chatImageSource.test.ts +44 -0
- package/apps/mobile/src/components/__tests__/composerUsageLimits.test.ts +138 -0
- package/apps/mobile/src/components/__tests__/voiceWaveform.test.ts +31 -0
- package/apps/mobile/src/components/chat-input-layout.ts +59 -0
- package/apps/mobile/src/components/chatImageSource.ts +86 -0
- package/apps/mobile/src/components/usageLimitBadges.ts +109 -0
- package/apps/mobile/src/components/voiceWaveform.ts +46 -0
- package/apps/mobile/src/config.ts +9 -2
- package/apps/mobile/src/hooks/useVoiceRecorder.ts +8 -1
- package/apps/mobile/src/navigation/DrawerContent.tsx +607 -457
- package/apps/mobile/src/navigation/__tests__/chatThreadTree.test.ts +89 -0
- package/apps/mobile/src/navigation/__tests__/drawerChats.test.ts +65 -0
- package/apps/mobile/src/navigation/chatThreadTree.ts +191 -0
- package/apps/mobile/src/navigation/drawerChats.ts +9 -0
- package/apps/mobile/src/screens/GitScreen.tsx +2 -0
- package/apps/mobile/src/screens/MainScreen.tsx +4244 -1237
- package/apps/mobile/src/screens/OnboardingScreen.tsx +2 -0
- package/apps/mobile/src/screens/SettingsScreen.tsx +256 -226
- package/apps/mobile/src/screens/TerminalScreen.tsx +2 -5
- package/apps/mobile/src/screens/__tests__/agentThreadDisplay.test.ts +80 -0
- package/apps/mobile/src/screens/__tests__/agentThreads.test.ts +170 -0
- package/apps/mobile/src/screens/__tests__/planCardState.test.ts +88 -0
- package/apps/mobile/src/screens/__tests__/subAgentTranscript.test.ts +102 -0
- package/apps/mobile/src/screens/__tests__/transcriptMessages.test.ts +97 -0
- package/apps/mobile/src/screens/agentThreadDisplay.ts +261 -0
- package/apps/mobile/src/screens/agentThreads.ts +167 -0
- package/apps/mobile/src/screens/planCardState.ts +40 -0
- package/apps/mobile/src/screens/subAgentTranscript.ts +149 -0
- package/apps/mobile/src/screens/transcriptMessages.ts +102 -0
- package/apps/mobile/src/theme.ts +6 -12
- package/docs/codex-app-server-cli-gap-tracker.md +14 -5
- package/docs/privacy-policy.md +54 -0
- package/docs/setup-and-operations.md +4 -3
- package/docs/terms-of-service.md +33 -0
- package/package.json +3 -3
- package/services/mac-bridge/package.json +6 -6
- package/services/rust-bridge/Cargo.lock +58 -363
- package/services/rust-bridge/Cargo.toml +2 -2
- package/services/rust-bridge/package.json +1 -1
- package/services/rust-bridge/src/main.rs +507 -9
- package/site/index.html +54 -0
- package/site/privacy/index.html +80 -0
- package/site/styles.css +135 -0
- package/site/support/index.html +51 -0
- package/site/terms/index.html +68 -0
|
@@ -2,11 +2,21 @@ export type ChatStatus = 'idle' | 'running' | 'error' | 'complete';
|
|
|
2
2
|
|
|
3
3
|
export type ChatMessageRole = 'user' | 'assistant' | 'system';
|
|
4
4
|
|
|
5
|
+
export interface ChatMessageSubAgentMeta {
|
|
6
|
+
tool?: string;
|
|
7
|
+
prompt?: string;
|
|
8
|
+
senderThreadId?: string;
|
|
9
|
+
receiverThreadIds?: string[];
|
|
10
|
+
agentStatus?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
5
13
|
export interface ChatMessage {
|
|
6
14
|
id: string;
|
|
7
15
|
role: ChatMessageRole;
|
|
8
16
|
content: string;
|
|
9
17
|
createdAt: string;
|
|
18
|
+
systemKind?: 'tool' | 'subAgent';
|
|
19
|
+
subAgentMeta?: ChatMessageSubAgentMeta;
|
|
10
20
|
}
|
|
11
21
|
|
|
12
22
|
export interface ChatSummary {
|
|
@@ -19,7 +29,11 @@ export interface ChatSummary {
|
|
|
19
29
|
lastMessagePreview: string;
|
|
20
30
|
cwd?: string;
|
|
21
31
|
modelProvider?: string;
|
|
32
|
+
agentNickname?: string;
|
|
33
|
+
agentRole?: string;
|
|
22
34
|
sourceKind?: string;
|
|
35
|
+
parentThreadId?: string;
|
|
36
|
+
subAgentDepth?: number;
|
|
23
37
|
lastRunStartedAt?: string;
|
|
24
38
|
lastRunFinishedAt?: string;
|
|
25
39
|
lastRunDurationMs?: number;
|
|
@@ -28,8 +42,18 @@ export interface ChatSummary {
|
|
|
28
42
|
lastError?: string;
|
|
29
43
|
}
|
|
30
44
|
|
|
45
|
+
export interface ChatPlanSnapshot {
|
|
46
|
+
threadId: string;
|
|
47
|
+
turnId: string;
|
|
48
|
+
explanation: string | null;
|
|
49
|
+
steps: TurnPlanStep[];
|
|
50
|
+
}
|
|
51
|
+
|
|
31
52
|
export interface Chat extends ChatSummary {
|
|
32
53
|
messages: ChatMessage[];
|
|
54
|
+
latestPlan?: ChatPlanSnapshot | null;
|
|
55
|
+
latestTurnPlan?: ChatPlanSnapshot | null;
|
|
56
|
+
latestTurnStatus?: string | null;
|
|
33
57
|
}
|
|
34
58
|
|
|
35
59
|
export interface CreateChatRequest {
|
|
@@ -38,6 +62,7 @@ export interface CreateChatRequest {
|
|
|
38
62
|
cwd?: string;
|
|
39
63
|
model?: string;
|
|
40
64
|
effort?: ReasoningEffort;
|
|
65
|
+
serviceTier?: ServiceTier;
|
|
41
66
|
approvalPolicy?: ApprovalPolicy;
|
|
42
67
|
}
|
|
43
68
|
|
|
@@ -49,12 +74,19 @@ export interface SendChatMessageRequest {
|
|
|
49
74
|
cwd?: string;
|
|
50
75
|
model?: string;
|
|
51
76
|
effort?: ReasoningEffort;
|
|
77
|
+
serviceTier?: ServiceTier;
|
|
52
78
|
approvalPolicy?: ApprovalPolicy;
|
|
53
79
|
collaborationMode?: CollaborationMode;
|
|
54
80
|
mentions?: MentionInput[];
|
|
55
81
|
localImages?: LocalImageInput[];
|
|
56
82
|
}
|
|
57
83
|
|
|
84
|
+
export interface SteerChatTurnRequest {
|
|
85
|
+
content: string;
|
|
86
|
+
mentions?: MentionInput[];
|
|
87
|
+
localImages?: LocalImageInput[];
|
|
88
|
+
}
|
|
89
|
+
|
|
58
90
|
export interface MentionInput {
|
|
59
91
|
path: string;
|
|
60
92
|
name?: string;
|
|
@@ -82,6 +114,39 @@ export interface UploadAttachmentResponse {
|
|
|
82
114
|
kind: AttachmentUploadKind;
|
|
83
115
|
}
|
|
84
116
|
|
|
117
|
+
export interface WorkspaceSummary {
|
|
118
|
+
path: string;
|
|
119
|
+
chatCount: number;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface WorkspaceListResponse {
|
|
123
|
+
bridgeRoot: string;
|
|
124
|
+
allowOutsideRootCwd: boolean;
|
|
125
|
+
workspaces: WorkspaceSummary[];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface FileSystemListRequest {
|
|
129
|
+
path?: string | null;
|
|
130
|
+
includeHidden?: boolean;
|
|
131
|
+
directoriesOnly?: boolean;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface FileSystemEntry {
|
|
135
|
+
name: string;
|
|
136
|
+
path: string;
|
|
137
|
+
kind: string;
|
|
138
|
+
hidden: boolean;
|
|
139
|
+
selectable: boolean;
|
|
140
|
+
isGitRepo: boolean;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface FileSystemListResponse {
|
|
144
|
+
bridgeRoot: string;
|
|
145
|
+
path: string;
|
|
146
|
+
parentPath: string | null;
|
|
147
|
+
entries: FileSystemEntry[];
|
|
148
|
+
}
|
|
149
|
+
|
|
85
150
|
export type ReasoningEffort =
|
|
86
151
|
| 'none'
|
|
87
152
|
| 'minimal'
|
|
@@ -90,6 +155,47 @@ export type ReasoningEffort =
|
|
|
90
155
|
| 'high'
|
|
91
156
|
| 'xhigh';
|
|
92
157
|
|
|
158
|
+
export type ServiceTier = 'flex' | 'fast';
|
|
159
|
+
|
|
160
|
+
export type PlanType =
|
|
161
|
+
| 'free'
|
|
162
|
+
| 'go'
|
|
163
|
+
| 'plus'
|
|
164
|
+
| 'pro'
|
|
165
|
+
| 'team'
|
|
166
|
+
| 'business'
|
|
167
|
+
| 'enterprise'
|
|
168
|
+
| 'edu'
|
|
169
|
+
| 'unknown';
|
|
170
|
+
|
|
171
|
+
export interface AccountCreditsSnapshot {
|
|
172
|
+
hasCredits: boolean;
|
|
173
|
+
unlimited: boolean;
|
|
174
|
+
balance: string | null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface AccountRateLimitWindow {
|
|
178
|
+
usedPercent: number;
|
|
179
|
+
windowDurationMins: number | null;
|
|
180
|
+
resetsAt: number | null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface AccountRateLimitSnapshot {
|
|
184
|
+
limitId: string | null;
|
|
185
|
+
limitName: string | null;
|
|
186
|
+
primary: AccountRateLimitWindow | null;
|
|
187
|
+
secondary: AccountRateLimitWindow | null;
|
|
188
|
+
credits: AccountCreditsSnapshot | null;
|
|
189
|
+
planType: PlanType | null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface AccountSnapshot {
|
|
193
|
+
type: 'apiKey' | 'chatgpt' | null;
|
|
194
|
+
email: string | null;
|
|
195
|
+
planType: PlanType | null;
|
|
196
|
+
requiresOpenaiAuth: boolean;
|
|
197
|
+
}
|
|
198
|
+
|
|
93
199
|
export type ApprovalPolicy =
|
|
94
200
|
| 'untrusted'
|
|
95
201
|
| 'on-request'
|
|
@@ -778,9 +778,13 @@ function extractNotificationThreadId(
|
|
|
778
778
|
toRecord(params?.threadState) ??
|
|
779
779
|
toRecord(params?.thread_state) ??
|
|
780
780
|
toRecord(msg?.thread);
|
|
781
|
+
const threadSourceRecord = toRecord(threadRecord?.source);
|
|
781
782
|
const sourceRecord = toRecord(params?.source) ?? toRecord(msg?.source);
|
|
782
783
|
const subagentThreadSpawnRecord = toRecord(
|
|
783
|
-
toRecord(sourceRecord?.subagent)?.thread_spawn
|
|
784
|
+
toRecord(sourceRecord?.subagent ?? sourceRecord?.subAgent)?.thread_spawn
|
|
785
|
+
);
|
|
786
|
+
const threadSubagentThreadSpawnRecord = toRecord(
|
|
787
|
+
toRecord(threadSourceRecord?.subagent ?? threadSourceRecord?.subAgent)?.thread_spawn
|
|
784
788
|
);
|
|
785
789
|
|
|
786
790
|
return (
|
|
@@ -804,6 +808,11 @@ function extractNotificationThreadId(
|
|
|
804
808
|
readString(sourceRecord?.parent_thread_id) ??
|
|
805
809
|
readString(sourceRecord?.parentThreadId) ??
|
|
806
810
|
readString(subagentThreadSpawnRecord?.parent_thread_id) ??
|
|
811
|
+
readString(subagentThreadSpawnRecord?.parentThreadId) ??
|
|
812
|
+
readString(threadSourceRecord?.parent_thread_id) ??
|
|
813
|
+
readString(threadSourceRecord?.parentThreadId) ??
|
|
814
|
+
readString(threadSubagentThreadSpawnRecord?.parent_thread_id) ??
|
|
815
|
+
readString(threadSubagentThreadSpawnRecord?.parentThreadId) ??
|
|
807
816
|
null
|
|
808
817
|
);
|
|
809
818
|
}
|
|
@@ -26,9 +26,9 @@ export function ChatHeader({
|
|
|
26
26
|
<SafeAreaView edges={['top', 'left', 'right']}>
|
|
27
27
|
<View style={styles.header}>
|
|
28
28
|
<Pressable onPress={onOpenDrawer} hitSlop={8} style={styles.menuBtn}>
|
|
29
|
-
<Ionicons name="menu" size={
|
|
29
|
+
<Ionicons name="menu" size={20} color={colors.textPrimary} />
|
|
30
30
|
</Pressable>
|
|
31
|
-
<BrandMark size={
|
|
31
|
+
<BrandMark size={18} />
|
|
32
32
|
{onOpenTitleMenu ? (
|
|
33
33
|
<Pressable
|
|
34
34
|
onPress={onOpenTitleMenu}
|
|
@@ -38,7 +38,7 @@ export function ChatHeader({
|
|
|
38
38
|
<Text numberOfLines={1} style={styles.modelName}>
|
|
39
39
|
{titleDisplay}
|
|
40
40
|
</Text>
|
|
41
|
-
<Ionicons name="chevron-down" size={
|
|
41
|
+
<Ionicons name="chevron-down" size={12} color={colors.textMuted} />
|
|
42
42
|
</Pressable>
|
|
43
43
|
) : (
|
|
44
44
|
<View style={styles.modelNameRow}>
|
|
@@ -50,10 +50,10 @@ export function ChatHeader({
|
|
|
50
50
|
<View style={{ flex: 1 }} />
|
|
51
51
|
{onRightActionPress ? (
|
|
52
52
|
<Pressable onPress={onRightActionPress} hitSlop={8} style={styles.rightBtn}>
|
|
53
|
-
<Ionicons name={rightIconName} size={
|
|
53
|
+
<Ionicons name={rightIconName} size={18} color={colors.textMuted} />
|
|
54
54
|
</Pressable>
|
|
55
55
|
) : (
|
|
56
|
-
<Ionicons name={rightIconName} size={
|
|
56
|
+
<Ionicons name={rightIconName} size={18} color={colors.textMuted} />
|
|
57
57
|
)}
|
|
58
58
|
</View>
|
|
59
59
|
</SafeAreaView>
|
|
@@ -72,13 +72,13 @@ const styles = StyleSheet.create({
|
|
|
72
72
|
alignItems: 'center',
|
|
73
73
|
gap: spacing.sm,
|
|
74
74
|
paddingHorizontal: spacing.lg,
|
|
75
|
-
paddingVertical: spacing.
|
|
75
|
+
paddingVertical: spacing.sm,
|
|
76
76
|
},
|
|
77
77
|
menuBtn: {
|
|
78
|
-
padding:
|
|
78
|
+
padding: 2,
|
|
79
79
|
},
|
|
80
80
|
rightBtn: {
|
|
81
|
-
padding:
|
|
81
|
+
padding: 2,
|
|
82
82
|
},
|
|
83
83
|
modelNameRow: {
|
|
84
84
|
flexDirection: 'row',
|
|
@@ -91,16 +91,16 @@ const styles = StyleSheet.create({
|
|
|
91
91
|
alignItems: 'center',
|
|
92
92
|
gap: spacing.xs,
|
|
93
93
|
borderRadius: 8,
|
|
94
|
-
paddingHorizontal:
|
|
95
|
-
paddingVertical:
|
|
94
|
+
paddingHorizontal: 2,
|
|
95
|
+
paddingVertical: 1,
|
|
96
96
|
flexShrink: 1,
|
|
97
97
|
},
|
|
98
98
|
titleButtonPressed: {
|
|
99
99
|
backgroundColor: colors.bgItem,
|
|
100
100
|
},
|
|
101
101
|
modelName: {
|
|
102
|
-
...typography.
|
|
103
|
-
fontSize:
|
|
102
|
+
...typography.headline,
|
|
103
|
+
fontSize: 17,
|
|
104
104
|
color: colors.textPrimary,
|
|
105
105
|
flexShrink: 1,
|
|
106
106
|
},
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Ionicons } from '@expo/vector-icons';
|
|
2
|
-
import {
|
|
3
|
-
import { useEffect, useState } from 'react';
|
|
2
|
+
import { useEffect, useState, type ReactNode } from 'react';
|
|
4
3
|
import {
|
|
5
4
|
ActivityIndicator,
|
|
6
5
|
type NativeSyntheticEvent,
|
|
@@ -16,6 +15,8 @@ import {
|
|
|
16
15
|
} from 'react-native';
|
|
17
16
|
|
|
18
17
|
import type { VoiceState } from '../hooks/useVoiceRecorder';
|
|
18
|
+
import { resolveComposerBottomSpacing } from './chat-input-layout';
|
|
19
|
+
import { VoiceRecordingWaveform } from './VoiceRecordingWaveform';
|
|
19
20
|
import { colors, radius, spacing } from '../theme';
|
|
20
21
|
|
|
21
22
|
interface ChatInputProps {
|
|
@@ -25,6 +26,7 @@ interface ChatInputProps {
|
|
|
25
26
|
onSubmit: () => void;
|
|
26
27
|
onStop?: () => void;
|
|
27
28
|
onAttachPress: () => void;
|
|
29
|
+
attachDisabled?: boolean;
|
|
28
30
|
attachments?: Array<{ id: string; label: string }>;
|
|
29
31
|
onRemoveAttachment?: (id: string) => void;
|
|
30
32
|
isLoading: boolean;
|
|
@@ -33,8 +35,11 @@ interface ChatInputProps {
|
|
|
33
35
|
placeholder?: string;
|
|
34
36
|
onVoiceToggle?: () => void;
|
|
35
37
|
voiceState?: VoiceState;
|
|
38
|
+
voiceRecordingDurationMillis?: number;
|
|
39
|
+
voiceMetering?: number | null;
|
|
36
40
|
safeAreaBottomInset?: number;
|
|
37
41
|
keyboardVisible?: boolean;
|
|
42
|
+
footer?: ReactNode;
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
export function ChatInput({
|
|
@@ -44,6 +49,7 @@ export function ChatInput({
|
|
|
44
49
|
onSubmit,
|
|
45
50
|
onStop,
|
|
46
51
|
onAttachPress,
|
|
52
|
+
attachDisabled = false,
|
|
47
53
|
attachments = [],
|
|
48
54
|
onRemoveAttachment,
|
|
49
55
|
isLoading,
|
|
@@ -52,8 +58,11 @@ export function ChatInput({
|
|
|
52
58
|
placeholder = 'Message Codex...',
|
|
53
59
|
onVoiceToggle,
|
|
54
60
|
voiceState = 'idle',
|
|
61
|
+
voiceRecordingDurationMillis = 0,
|
|
62
|
+
voiceMetering = null,
|
|
55
63
|
safeAreaBottomInset = 0,
|
|
56
64
|
keyboardVisible = false,
|
|
65
|
+
footer = null,
|
|
57
66
|
}: ChatInputProps) {
|
|
58
67
|
const INPUT_TEXT_LINE_HEIGHT = 20;
|
|
59
68
|
const INPUT_TEXT_VERTICAL_PADDING = Platform.OS === 'ios' ? 2 : 0;
|
|
@@ -82,30 +91,24 @@ export function ChatInput({
|
|
|
82
91
|
const showVoiceButton = Boolean(onVoiceToggle);
|
|
83
92
|
const showSendButton = canSend || isLoading;
|
|
84
93
|
const inputScrollEnabled = inputHeight >= INPUT_TEXT_MAX_HEIGHT;
|
|
94
|
+
const showVoiceRecordingUi = voiceState === 'recording';
|
|
95
|
+
const showVoiceTranscribingUi = voiceState === 'transcribing';
|
|
96
|
+
const showVoiceStatusUi = showVoiceRecordingUi || showVoiceTranscribingUi;
|
|
85
97
|
const shouldShowActionButton =
|
|
86
98
|
canStop || showSendButton || showVoiceButton || voiceState !== 'idle';
|
|
87
|
-
const
|
|
88
|
-
Platform.OS
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
: spacing.md;
|
|
93
|
-
const extraBottomInset = keyboardVisible ? 0 : safeAreaBottomInset;
|
|
99
|
+
const composerBottomSpacing = resolveComposerBottomSpacing(
|
|
100
|
+
Platform.OS,
|
|
101
|
+
safeAreaBottomInset,
|
|
102
|
+
keyboardVisible
|
|
103
|
+
);
|
|
94
104
|
|
|
95
105
|
return (
|
|
96
106
|
<View style={styles.shell}>
|
|
97
|
-
<BlurView
|
|
98
|
-
intensity={26}
|
|
99
|
-
tint={Platform.OS === 'ios' ? 'systemUltraThinMaterialDark' : 'dark'}
|
|
100
|
-
blurMethod="dimezisBlurViewSdk31Plus"
|
|
101
|
-
style={StyleSheet.absoluteFill}
|
|
102
|
-
/>
|
|
103
107
|
<View
|
|
104
108
|
style={[
|
|
105
109
|
styles.container,
|
|
106
110
|
{
|
|
107
|
-
paddingBottom:
|
|
108
|
-
baseBottomPadding + extraBottomInset,
|
|
111
|
+
paddingBottom: composerBottomSpacing.totalBottomPadding,
|
|
109
112
|
},
|
|
110
113
|
]}
|
|
111
114
|
>
|
|
@@ -143,78 +146,102 @@ export function ChatInput({
|
|
|
143
146
|
|
|
144
147
|
<View style={styles.row}>
|
|
145
148
|
<Pressable
|
|
149
|
+
disabled={attachDisabled}
|
|
146
150
|
onPress={onAttachPress}
|
|
147
|
-
style={({ pressed }) => [
|
|
151
|
+
style={({ pressed }) => [
|
|
152
|
+
styles.plusBtn,
|
|
153
|
+
attachDisabled && styles.plusBtnDisabled,
|
|
154
|
+
pressed && !attachDisabled && styles.plusBtnPressed,
|
|
155
|
+
]}
|
|
148
156
|
>
|
|
149
157
|
<Ionicons name="add" size={20} color={colors.textMuted} />
|
|
150
158
|
</Pressable>
|
|
151
159
|
|
|
152
|
-
<View
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
160
|
+
<View
|
|
161
|
+
style={[
|
|
162
|
+
styles.inputWrapper,
|
|
163
|
+
showVoiceStatusUi && styles.inputWrapperVoiceActive,
|
|
164
|
+
]}
|
|
165
|
+
>
|
|
166
|
+
{showVoiceStatusUi ? (
|
|
167
|
+
showVoiceRecordingUi ? (
|
|
168
|
+
<VoiceRecordingWaveform
|
|
169
|
+
durationMillis={voiceRecordingDurationMillis}
|
|
170
|
+
metering={voiceMetering}
|
|
171
|
+
/>
|
|
172
|
+
) : (
|
|
173
|
+
<View
|
|
174
|
+
accessible
|
|
175
|
+
accessibilityLabel="Transcribing recorded audio into text"
|
|
176
|
+
style={styles.voiceStatusContent}
|
|
177
|
+
>
|
|
178
|
+
<View style={styles.voiceStatusLabelRow}>
|
|
179
|
+
<View style={[styles.voiceStatusDot, styles.voiceStatusDotBusy]} />
|
|
180
|
+
<Text style={styles.voiceStatusTitle}>Transcribing audio</Text>
|
|
181
|
+
</View>
|
|
182
|
+
<Text style={styles.voiceStatusHint}>
|
|
183
|
+
Converting your latest recording into text.
|
|
184
|
+
</Text>
|
|
185
|
+
</View>
|
|
186
|
+
)
|
|
187
|
+
) : (
|
|
188
|
+
<>
|
|
189
|
+
<Text
|
|
190
|
+
pointerEvents="none"
|
|
191
|
+
accessibilityElementsHidden
|
|
192
|
+
importantForAccessibility="no-hide-descendants"
|
|
193
|
+
style={[
|
|
194
|
+
styles.inputMeasure,
|
|
195
|
+
{
|
|
196
|
+
width: inputWidth,
|
|
197
|
+
lineHeight: INPUT_TEXT_LINE_HEIGHT,
|
|
198
|
+
paddingVertical: INPUT_TEXT_VERTICAL_PADDING,
|
|
199
|
+
},
|
|
200
|
+
]}
|
|
201
|
+
onTextLayout={(event: NativeSyntheticEvent<TextLayoutEventData>) => {
|
|
202
|
+
if (inputWidth <= 0) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const lineCount = Math.max(1, event.nativeEvent.lines.length);
|
|
206
|
+
const measuredHeight =
|
|
207
|
+
lineCount * INPUT_TEXT_LINE_HEIGHT + INPUT_TEXT_VERTICAL_PADDING * 2;
|
|
208
|
+
updateInputHeight(measuredHeight);
|
|
209
|
+
}}
|
|
210
|
+
>
|
|
211
|
+
{value.length > 0 ? `${value}\u200b` : ' '}
|
|
212
|
+
</Text>
|
|
213
|
+
<TextInput
|
|
214
|
+
style={[styles.input, { height: inputHeight }]}
|
|
215
|
+
value={value}
|
|
216
|
+
onChangeText={onChangeText}
|
|
217
|
+
keyboardAppearance="dark"
|
|
218
|
+
onLayout={(event) => {
|
|
219
|
+
const nextWidth = Math.floor(event.nativeEvent.layout.width);
|
|
220
|
+
setInputWidth((previousWidth) =>
|
|
221
|
+
previousWidth === nextWidth ? previousWidth : nextWidth
|
|
222
|
+
);
|
|
223
|
+
}}
|
|
224
|
+
onFocus={onFocus}
|
|
225
|
+
placeholder={placeholder}
|
|
226
|
+
placeholderTextColor={colors.textMuted}
|
|
227
|
+
multiline
|
|
228
|
+
scrollEnabled={inputScrollEnabled}
|
|
229
|
+
onKeyPress={(e: NativeSyntheticEvent<TextInputKeyPressEventData>) => {
|
|
230
|
+
const keyEvent = e.nativeEvent as TextInputKeyPressEventData & {
|
|
231
|
+
shiftKey?: boolean;
|
|
232
|
+
};
|
|
233
|
+
if (
|
|
234
|
+
Platform.OS === 'web' &&
|
|
235
|
+
keyEvent.key === 'Enter' &&
|
|
236
|
+
!keyEvent.shiftKey
|
|
237
|
+
) {
|
|
238
|
+
e.preventDefault();
|
|
239
|
+
if (canSend) onSubmit();
|
|
240
|
+
}
|
|
241
|
+
}}
|
|
242
|
+
/>
|
|
243
|
+
</>
|
|
244
|
+
)}
|
|
218
245
|
{shouldShowActionButton ? (
|
|
219
246
|
<View style={styles.actionButtons}>
|
|
220
247
|
{showVoiceButton || voiceState !== 'idle' ? (
|
|
@@ -271,6 +298,7 @@ export function ChatInput({
|
|
|
271
298
|
) : null}
|
|
272
299
|
</View>
|
|
273
300
|
</View>
|
|
301
|
+
{footer ? <View style={styles.footer}>{footer}</View> : null}
|
|
274
302
|
</View>
|
|
275
303
|
</View>
|
|
276
304
|
);
|
|
@@ -279,20 +307,21 @@ export function ChatInput({
|
|
|
279
307
|
const styles = StyleSheet.create({
|
|
280
308
|
shell: {
|
|
281
309
|
overflow: 'hidden',
|
|
282
|
-
borderTopWidth: StyleSheet.hairlineWidth,
|
|
283
|
-
borderTopColor: colors.borderLight,
|
|
284
310
|
},
|
|
285
311
|
container: {
|
|
286
312
|
gap: spacing.xs,
|
|
287
313
|
paddingHorizontal: spacing.lg,
|
|
288
|
-
|
|
289
|
-
backgroundColor: 'rgba(6, 9, 13, 0.42)',
|
|
314
|
+
paddingTop: spacing.sm,
|
|
290
315
|
},
|
|
291
316
|
row: {
|
|
292
317
|
flexDirection: 'row',
|
|
293
318
|
alignItems: 'center',
|
|
294
319
|
gap: spacing.sm,
|
|
295
320
|
},
|
|
321
|
+
footer: {
|
|
322
|
+
alignItems: 'flex-start',
|
|
323
|
+
marginTop: 2,
|
|
324
|
+
},
|
|
296
325
|
attachmentList: {
|
|
297
326
|
maxHeight: 34,
|
|
298
327
|
},
|
|
@@ -332,6 +361,9 @@ const styles = StyleSheet.create({
|
|
|
332
361
|
plusBtnPressed: {
|
|
333
362
|
backgroundColor: colors.bgItem,
|
|
334
363
|
},
|
|
364
|
+
plusBtnDisabled: {
|
|
365
|
+
opacity: 0.45,
|
|
366
|
+
},
|
|
335
367
|
inputWrapper: {
|
|
336
368
|
flex: 1,
|
|
337
369
|
flexDirection: 'row',
|
|
@@ -345,6 +377,10 @@ const styles = StyleSheet.create({
|
|
|
345
377
|
minHeight: 40,
|
|
346
378
|
maxHeight: 120,
|
|
347
379
|
},
|
|
380
|
+
inputWrapperVoiceActive: {
|
|
381
|
+
minHeight: 58,
|
|
382
|
+
paddingVertical: spacing.sm,
|
|
383
|
+
},
|
|
348
384
|
input: {
|
|
349
385
|
flex: 1,
|
|
350
386
|
color: colors.textPrimary,
|
|
@@ -361,6 +397,36 @@ const styles = StyleSheet.create({
|
|
|
361
397
|
left: spacing.md,
|
|
362
398
|
top: spacing.xs,
|
|
363
399
|
},
|
|
400
|
+
voiceStatusContent: {
|
|
401
|
+
flex: 1,
|
|
402
|
+
gap: 2,
|
|
403
|
+
justifyContent: 'center',
|
|
404
|
+
minHeight: 40,
|
|
405
|
+
},
|
|
406
|
+
voiceStatusLabelRow: {
|
|
407
|
+
alignItems: 'center',
|
|
408
|
+
flexDirection: 'row',
|
|
409
|
+
gap: spacing.xs,
|
|
410
|
+
},
|
|
411
|
+
voiceStatusDot: {
|
|
412
|
+
backgroundColor: colors.error,
|
|
413
|
+
borderRadius: 4,
|
|
414
|
+
height: 8,
|
|
415
|
+
width: 8,
|
|
416
|
+
},
|
|
417
|
+
voiceStatusDotBusy: {
|
|
418
|
+
opacity: 0.82,
|
|
419
|
+
},
|
|
420
|
+
voiceStatusTitle: {
|
|
421
|
+
color: colors.textPrimary,
|
|
422
|
+
fontSize: 13,
|
|
423
|
+
fontWeight: '600',
|
|
424
|
+
},
|
|
425
|
+
voiceStatusHint: {
|
|
426
|
+
color: colors.textMuted,
|
|
427
|
+
fontSize: 12,
|
|
428
|
+
lineHeight: 16,
|
|
429
|
+
},
|
|
364
430
|
actionButtons: {
|
|
365
431
|
flexDirection: 'row',
|
|
366
432
|
alignItems: 'center',
|