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.
- package/.github/workflows/pages.yml +41 -0
- package/AGENTS.md +263 -110
- package/README.md +1 -1
- 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 +56 -47
- package/services/rust-bridge/Cargo.toml +1 -1
- 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
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { ChatSummary } from '../../api/types';
|
|
2
|
+
import { buildChatWorkspaceSections } from '../chatThreadTree';
|
|
3
|
+
|
|
4
|
+
function chat(partial: Partial<ChatSummary> & Pick<ChatSummary, 'id' | 'updatedAt'>): ChatSummary {
|
|
5
|
+
return {
|
|
6
|
+
id: partial.id,
|
|
7
|
+
title: partial.title ?? partial.id,
|
|
8
|
+
status: partial.status ?? 'idle',
|
|
9
|
+
createdAt: partial.createdAt ?? '2026-03-19T00:00:00.000Z',
|
|
10
|
+
updatedAt: partial.updatedAt,
|
|
11
|
+
statusUpdatedAt: partial.statusUpdatedAt ?? partial.updatedAt,
|
|
12
|
+
lastMessagePreview: partial.lastMessagePreview ?? '',
|
|
13
|
+
cwd: partial.cwd,
|
|
14
|
+
modelProvider: partial.modelProvider,
|
|
15
|
+
sourceKind: partial.sourceKind,
|
|
16
|
+
parentThreadId: partial.parentThreadId,
|
|
17
|
+
subAgentDepth: partial.subAgentDepth,
|
|
18
|
+
lastRunStartedAt: partial.lastRunStartedAt,
|
|
19
|
+
lastRunFinishedAt: partial.lastRunFinishedAt,
|
|
20
|
+
lastRunDurationMs: partial.lastRunDurationMs,
|
|
21
|
+
lastRunExitCode: partial.lastRunExitCode,
|
|
22
|
+
lastRunTimedOut: partial.lastRunTimedOut,
|
|
23
|
+
lastError: partial.lastError,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('buildChatWorkspaceSections', () => {
|
|
28
|
+
it('nests sub-agent rows below their root thread', () => {
|
|
29
|
+
const sections = buildChatWorkspaceSections([
|
|
30
|
+
chat({
|
|
31
|
+
id: 'root',
|
|
32
|
+
title: 'Review repo',
|
|
33
|
+
cwd: '/workspace/repo',
|
|
34
|
+
updatedAt: '2026-03-20T10:00:00.000Z',
|
|
35
|
+
}),
|
|
36
|
+
chat({
|
|
37
|
+
id: 'agent-a',
|
|
38
|
+
title: 'Review app',
|
|
39
|
+
cwd: '/workspace/repo/sub',
|
|
40
|
+
updatedAt: '2026-03-20T09:59:00.000Z',
|
|
41
|
+
parentThreadId: 'root',
|
|
42
|
+
sourceKind: 'subAgentThreadSpawn',
|
|
43
|
+
subAgentDepth: 1,
|
|
44
|
+
}),
|
|
45
|
+
chat({
|
|
46
|
+
id: 'agent-b',
|
|
47
|
+
title: 'Review bridge',
|
|
48
|
+
cwd: '/workspace/repo',
|
|
49
|
+
updatedAt: '2026-03-20T09:58:00.000Z',
|
|
50
|
+
parentThreadId: 'root',
|
|
51
|
+
sourceKind: 'subAgentReview',
|
|
52
|
+
subAgentDepth: 1,
|
|
53
|
+
}),
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
expect(sections).toHaveLength(1);
|
|
57
|
+
expect(sections[0].title).toBe('repo');
|
|
58
|
+
expect(sections[0].itemCount).toBe(3);
|
|
59
|
+
expect(sections[0].data.map((row) => [row.chat.id, row.indentLevel])).toEqual([
|
|
60
|
+
['root', 0],
|
|
61
|
+
['agent-a', 1],
|
|
62
|
+
['agent-b', 1],
|
|
63
|
+
]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('groups sub-agent rows under the root workspace', () => {
|
|
67
|
+
const sections = buildChatWorkspaceSections([
|
|
68
|
+
chat({
|
|
69
|
+
id: 'root',
|
|
70
|
+
title: 'Root',
|
|
71
|
+
cwd: '/workspace/one',
|
|
72
|
+
updatedAt: '2026-03-20T10:00:00.000Z',
|
|
73
|
+
}),
|
|
74
|
+
chat({
|
|
75
|
+
id: 'child',
|
|
76
|
+
title: 'Child',
|
|
77
|
+
cwd: '/workspace/two',
|
|
78
|
+
updatedAt: '2026-03-20T09:59:00.000Z',
|
|
79
|
+
parentThreadId: 'root',
|
|
80
|
+
sourceKind: 'subAgentThreadSpawn',
|
|
81
|
+
subAgentDepth: 1,
|
|
82
|
+
}),
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
expect(sections).toHaveLength(1);
|
|
86
|
+
expect(sections[0].key).toBe('/workspace/one');
|
|
87
|
+
expect(sections[0].data.map((row) => row.chat.id)).toEqual(['root', 'child']);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ChatSummary } from '../../api/types';
|
|
2
|
+
import { filterDrawerChats, isSubAgentChat } from '../drawerChats';
|
|
3
|
+
|
|
4
|
+
function chat(
|
|
5
|
+
id: string,
|
|
6
|
+
partial: Partial<ChatSummary> = {}
|
|
7
|
+
): ChatSummary {
|
|
8
|
+
return {
|
|
9
|
+
id,
|
|
10
|
+
title: partial.title ?? id,
|
|
11
|
+
status: partial.status ?? 'idle',
|
|
12
|
+
createdAt: partial.createdAt ?? '2026-03-20T00:00:00.000Z',
|
|
13
|
+
updatedAt: partial.updatedAt ?? '2026-03-20T00:00:00.000Z',
|
|
14
|
+
statusUpdatedAt: partial.statusUpdatedAt ?? '2026-03-20T00:00:00.000Z',
|
|
15
|
+
lastMessagePreview: partial.lastMessagePreview ?? '',
|
|
16
|
+
cwd: partial.cwd,
|
|
17
|
+
modelProvider: partial.modelProvider,
|
|
18
|
+
sourceKind: partial.sourceKind,
|
|
19
|
+
parentThreadId: partial.parentThreadId,
|
|
20
|
+
subAgentDepth: partial.subAgentDepth,
|
|
21
|
+
lastRunStartedAt: partial.lastRunStartedAt,
|
|
22
|
+
lastRunFinishedAt: partial.lastRunFinishedAt,
|
|
23
|
+
lastRunDurationMs: partial.lastRunDurationMs,
|
|
24
|
+
lastRunExitCode: partial.lastRunExitCode,
|
|
25
|
+
lastRunTimedOut: partial.lastRunTimedOut,
|
|
26
|
+
lastError: partial.lastError,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('drawerChats', () => {
|
|
31
|
+
it('recognizes sub-agent chats from parent thread or source kind', () => {
|
|
32
|
+
expect(isSubAgentChat(chat('root'))).toBe(false);
|
|
33
|
+
expect(
|
|
34
|
+
isSubAgentChat(
|
|
35
|
+
chat('child-parent', {
|
|
36
|
+
parentThreadId: 'root',
|
|
37
|
+
})
|
|
38
|
+
)
|
|
39
|
+
).toBe(true);
|
|
40
|
+
expect(
|
|
41
|
+
isSubAgentChat(
|
|
42
|
+
chat('child-source', {
|
|
43
|
+
sourceKind: 'subAgentThreadSpawn',
|
|
44
|
+
})
|
|
45
|
+
)
|
|
46
|
+
).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('filters sub-agent chats out of the top-level drawer list', () => {
|
|
50
|
+
const chats = [
|
|
51
|
+
chat('root', { title: 'Main thread' }),
|
|
52
|
+
chat('worker-1', {
|
|
53
|
+
title: 'Spawned worker',
|
|
54
|
+
sourceKind: 'subAgentThreadSpawn',
|
|
55
|
+
parentThreadId: 'root',
|
|
56
|
+
}),
|
|
57
|
+
chat('worker-2', {
|
|
58
|
+
title: 'Review worker',
|
|
59
|
+
sourceKind: 'subAgentReview',
|
|
60
|
+
}),
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
expect(filterDrawerChats(chats).map((entry) => entry.id)).toEqual(['root']);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { ChatSummary } from '../api/types';
|
|
2
|
+
|
|
3
|
+
export interface DrawerThreadRow {
|
|
4
|
+
chat: ChatSummary;
|
|
5
|
+
indentLevel: number;
|
|
6
|
+
rootThreadId: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ChatWorkspaceSection {
|
|
10
|
+
key: string;
|
|
11
|
+
title: string;
|
|
12
|
+
subtitle?: string;
|
|
13
|
+
itemCount: number;
|
|
14
|
+
data: DrawerThreadRow[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const DEFAULT_WORKSPACE_KEY = '__bridge_default_workspace__';
|
|
18
|
+
|
|
19
|
+
export function buildChatWorkspaceSections(chats: ChatSummary[]): ChatWorkspaceSection[] {
|
|
20
|
+
if (chats.length === 0) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const chatMap = new Map<string, ChatSummary>();
|
|
25
|
+
for (const chat of chats) {
|
|
26
|
+
chatMap.set(chat.id, chat);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const childrenByParentId = new Map<string, ChatSummary[]>();
|
|
30
|
+
const roots: ChatSummary[] = [];
|
|
31
|
+
|
|
32
|
+
for (const chat of chats) {
|
|
33
|
+
const parentThreadId = normalizeThreadId(chat.parentThreadId);
|
|
34
|
+
if (!parentThreadId || !chatMap.has(parentThreadId) || parentThreadId === chat.id) {
|
|
35
|
+
roots.push(chat);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const siblings = childrenByParentId.get(parentThreadId);
|
|
40
|
+
if (siblings) {
|
|
41
|
+
siblings.push(chat);
|
|
42
|
+
} else {
|
|
43
|
+
childrenByParentId.set(parentThreadId, [chat]);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const rootsByWorkspace = new Map<
|
|
48
|
+
string,
|
|
49
|
+
{
|
|
50
|
+
cwd: string | null;
|
|
51
|
+
roots: ChatSummary[];
|
|
52
|
+
latestUpdatedAt: string;
|
|
53
|
+
}
|
|
54
|
+
>();
|
|
55
|
+
|
|
56
|
+
for (const root of roots.sort(compareByUpdatedAtDesc)) {
|
|
57
|
+
const rootCwd = normalizeCwd(root.cwd);
|
|
58
|
+
const key = workspaceKey(rootCwd);
|
|
59
|
+
const existing = rootsByWorkspace.get(key);
|
|
60
|
+
|
|
61
|
+
if (existing) {
|
|
62
|
+
existing.roots.push(root);
|
|
63
|
+
if (root.updatedAt.localeCompare(existing.latestUpdatedAt) > 0) {
|
|
64
|
+
existing.latestUpdatedAt = root.updatedAt;
|
|
65
|
+
}
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
rootsByWorkspace.set(key, {
|
|
70
|
+
cwd: rootCwd,
|
|
71
|
+
roots: [root],
|
|
72
|
+
latestUpdatedAt: root.updatedAt,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return Array.from(rootsByWorkspace.entries())
|
|
77
|
+
.sort(([, left], [, right]) => right.latestUpdatedAt.localeCompare(left.latestUpdatedAt))
|
|
78
|
+
.map(([key, bucket]) => {
|
|
79
|
+
const data: DrawerThreadRow[] = [];
|
|
80
|
+
for (const root of bucket.roots.sort(compareByUpdatedAtDesc)) {
|
|
81
|
+
appendChatBranch(data, root, 0, root.id, childrenByParentId);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
key,
|
|
86
|
+
title: workspaceTitle(bucket.cwd),
|
|
87
|
+
subtitle: workspaceSubtitle(bucket.cwd),
|
|
88
|
+
itemCount: data.length,
|
|
89
|
+
data,
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function appendChatBranch(
|
|
95
|
+
rows: DrawerThreadRow[],
|
|
96
|
+
chat: ChatSummary,
|
|
97
|
+
indentLevel: number,
|
|
98
|
+
rootThreadId: string,
|
|
99
|
+
childrenByParentId: Map<string, ChatSummary[]>
|
|
100
|
+
): void {
|
|
101
|
+
rows.push({
|
|
102
|
+
chat,
|
|
103
|
+
indentLevel,
|
|
104
|
+
rootThreadId,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const children = [...(childrenByParentId.get(chat.id) ?? [])].sort(compareBranchChildren);
|
|
108
|
+
for (const child of children) {
|
|
109
|
+
appendChatBranch(rows, child, indentLevel + 1, rootThreadId, childrenByParentId);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function compareBranchChildren(left: ChatSummary, right: ChatSummary): number {
|
|
114
|
+
if (left.status === 'running' && right.status !== 'running') {
|
|
115
|
+
return -1;
|
|
116
|
+
}
|
|
117
|
+
if (right.status === 'running' && left.status !== 'running') {
|
|
118
|
+
return 1;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const depthDiff = (left.subAgentDepth ?? 0) - (right.subAgentDepth ?? 0);
|
|
122
|
+
if (depthDiff !== 0) {
|
|
123
|
+
return depthDiff;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const updatedDiff = right.updatedAt.localeCompare(left.updatedAt);
|
|
127
|
+
if (updatedDiff !== 0) {
|
|
128
|
+
return updatedDiff;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return left.title.localeCompare(right.title);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function compareByUpdatedAtDesc(left: ChatSummary, right: ChatSummary): number {
|
|
135
|
+
return right.updatedAt.localeCompare(left.updatedAt);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function normalizeThreadId(value: string | null | undefined): string | null {
|
|
139
|
+
if (typeof value !== 'string') {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const trimmed = value.trim();
|
|
144
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function normalizeCwd(value: string | null | undefined): string | null {
|
|
148
|
+
if (typeof value !== 'string') {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const trimmed = value.trim();
|
|
153
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function workspaceKey(cwd: string | null): string {
|
|
157
|
+
return cwd ?? DEFAULT_WORKSPACE_KEY;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function workspaceTitle(cwd: string | null): string {
|
|
161
|
+
if (!cwd) {
|
|
162
|
+
return 'Bridge default workspace';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const normalized = cwd.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
166
|
+
if (!normalized) {
|
|
167
|
+
return cwd;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const lastSlash = normalized.lastIndexOf('/');
|
|
171
|
+
if (lastSlash === -1) {
|
|
172
|
+
return normalized;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return normalized.slice(lastSlash + 1) || normalized;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function workspaceSubtitle(cwd: string | null): string | undefined {
|
|
179
|
+
if (!cwd) {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const normalized = cwd.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
184
|
+
const segments = normalized.split('/').filter(Boolean);
|
|
185
|
+
|
|
186
|
+
if (segments.length <= 2) {
|
|
187
|
+
return normalized;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return `.../${segments.slice(-2).join('/')}`;
|
|
191
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ChatSummary } from '../api/types';
|
|
2
|
+
|
|
3
|
+
export function filterDrawerChats(chats: ChatSummary[]): ChatSummary[] {
|
|
4
|
+
return chats.filter((chat) => !isSubAgentChat(chat));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function isSubAgentChat(chat: ChatSummary): boolean {
|
|
8
|
+
return Boolean(chat.parentThreadId) || chat.sourceKind?.startsWith('subAgent') === true;
|
|
9
|
+
}
|
|
@@ -461,6 +461,7 @@ export function GitScreen({ api, chat, onBack, onChatUpdated }: GitScreenProps)
|
|
|
461
461
|
style={styles.input}
|
|
462
462
|
value={workspaceDraft}
|
|
463
463
|
onChangeText={setWorkspaceDraft}
|
|
464
|
+
keyboardAppearance="dark"
|
|
464
465
|
onSubmitEditing={commitWorkspaceIfChanged}
|
|
465
466
|
onBlur={commitWorkspaceIfChanged}
|
|
466
467
|
placeholder="/path/to/project"
|
|
@@ -515,6 +516,7 @@ export function GitScreen({ api, chat, onBack, onChatUpdated }: GitScreenProps)
|
|
|
515
516
|
style={styles.input}
|
|
516
517
|
value={commitMessage}
|
|
517
518
|
onChangeText={setCommitMessage}
|
|
519
|
+
keyboardAppearance="dark"
|
|
518
520
|
placeholder="Commit message..."
|
|
519
521
|
placeholderTextColor={colors.textMuted}
|
|
520
522
|
/>
|