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,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
  />