clawdex-mobile 2.0.1 → 4.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 (81) hide show
  1. package/.github/workflows/ci.yml +4 -3
  2. package/.github/workflows/npm-release.yml +62 -2
  3. package/.github/workflows/pages.yml +41 -0
  4. package/AGENTS.md +263 -110
  5. package/README.md +15 -4
  6. package/apps/mobile/.env.example +2 -2
  7. package/apps/mobile/App.tsx +175 -14
  8. package/apps/mobile/app.json +27 -9
  9. package/apps/mobile/eas.json +14 -4
  10. package/apps/mobile/package.json +14 -13
  11. package/apps/mobile/src/api/__tests__/chatMapping.test.ts +219 -0
  12. package/apps/mobile/src/api/__tests__/client.test.ts +587 -6
  13. package/apps/mobile/src/api/__tests__/ws.test.ts +27 -0
  14. package/apps/mobile/src/api/account.ts +47 -0
  15. package/apps/mobile/src/api/chatMapping.ts +435 -18
  16. package/apps/mobile/src/api/client.ts +321 -36
  17. package/apps/mobile/src/api/rateLimits.ts +143 -0
  18. package/apps/mobile/src/api/types.ts +107 -0
  19. package/apps/mobile/src/api/ws.ts +10 -1
  20. package/apps/mobile/src/components/ChatHeader.tsx +12 -12
  21. package/apps/mobile/src/components/ChatInput.tsx +154 -88
  22. package/apps/mobile/src/components/ChatMessage.tsx +548 -93
  23. package/apps/mobile/src/components/ComposerUsageLimits.tsx +167 -0
  24. package/apps/mobile/src/components/SelectionSheet.tsx +466 -0
  25. package/apps/mobile/src/components/ToolBlock.tsx +17 -15
  26. package/apps/mobile/src/components/VoiceRecordingWaveform.tsx +181 -0
  27. package/apps/mobile/src/components/WorkspacePickerModal.tsx +812 -0
  28. package/apps/mobile/src/components/__tests__/chat-input-layout.test.ts +35 -0
  29. package/apps/mobile/src/components/__tests__/chatImageSource.test.ts +44 -0
  30. package/apps/mobile/src/components/__tests__/composerUsageLimits.test.ts +138 -0
  31. package/apps/mobile/src/components/__tests__/voiceWaveform.test.ts +31 -0
  32. package/apps/mobile/src/components/chat-input-layout.ts +59 -0
  33. package/apps/mobile/src/components/chatImageSource.ts +86 -0
  34. package/apps/mobile/src/components/usageLimitBadges.ts +109 -0
  35. package/apps/mobile/src/components/voiceWaveform.ts +46 -0
  36. package/apps/mobile/src/config.ts +9 -2
  37. package/apps/mobile/src/hooks/useVoiceRecorder.ts +8 -1
  38. package/apps/mobile/src/navigation/DrawerContent.tsx +607 -457
  39. package/apps/mobile/src/navigation/__tests__/chatThreadTree.test.ts +89 -0
  40. package/apps/mobile/src/navigation/__tests__/drawerChats.test.ts +65 -0
  41. package/apps/mobile/src/navigation/chatThreadTree.ts +191 -0
  42. package/apps/mobile/src/navigation/drawerChats.ts +9 -0
  43. package/apps/mobile/src/screens/GitScreen.tsx +2 -0
  44. package/apps/mobile/src/screens/MainScreen.tsx +4239 -1237
  45. package/apps/mobile/src/screens/OnboardingScreen.tsx +924 -310
  46. package/apps/mobile/src/screens/SettingsScreen.tsx +256 -226
  47. package/apps/mobile/src/screens/TerminalScreen.tsx +2 -5
  48. package/apps/mobile/src/screens/__tests__/agentThreadDisplay.test.ts +80 -0
  49. package/apps/mobile/src/screens/__tests__/agentThreads.test.ts +170 -0
  50. package/apps/mobile/src/screens/__tests__/planCardState.test.ts +88 -0
  51. package/apps/mobile/src/screens/__tests__/subAgentTranscript.test.ts +102 -0
  52. package/apps/mobile/src/screens/__tests__/transcriptMessages.test.ts +97 -0
  53. package/apps/mobile/src/screens/agentThreadDisplay.ts +261 -0
  54. package/apps/mobile/src/screens/agentThreads.ts +167 -0
  55. package/apps/mobile/src/screens/planCardState.ts +40 -0
  56. package/apps/mobile/src/screens/subAgentTranscript.ts +149 -0
  57. package/apps/mobile/src/screens/transcriptMessages.ts +102 -0
  58. package/apps/mobile/src/theme.ts +6 -12
  59. package/bin/clawdex.js +7 -6
  60. package/codex-rust-bridge +0 -0
  61. package/codex-rust-bridge.exe +0 -0
  62. package/docs/codex-app-server-cli-gap-tracker.md +14 -5
  63. package/docs/privacy-policy.md +54 -0
  64. package/docs/setup-and-operations.md +21 -15
  65. package/docs/terms-of-service.md +33 -0
  66. package/docs/troubleshooting.md +15 -19
  67. package/package.json +6 -5
  68. package/scripts/bridge-binary.js +194 -0
  69. package/scripts/setup-wizard.sh +17 -186
  70. package/scripts/start-bridge-secure.js +240 -0
  71. package/scripts/start-bridge-secure.sh +1 -40
  72. package/services/mac-bridge/package.json +6 -6
  73. package/services/rust-bridge/Cargo.lock +56 -47
  74. package/services/rust-bridge/Cargo.toml +1 -1
  75. package/services/rust-bridge/package.json +1 -1
  76. package/services/rust-bridge/src/main.rs +517 -9
  77. package/site/index.html +54 -0
  78. package/site/privacy/index.html +80 -0
  79. package/site/styles.css +135 -0
  80. package/site/support/index.html +51 -0
  81. package/site/terms/index.html +68 -0
@@ -21,6 +21,181 @@ describe('HostBridgeApiClient', () => {
21
21
  expect(result.status).toBe('ok');
22
22
  });
23
23
 
24
+ it('readAccountRateLimits() requests account/rateLimits/read and prefers codex bucket', async () => {
25
+ const ws = createWsMock();
26
+ ws.request.mockResolvedValue({
27
+ rateLimitsByLimitId: {
28
+ codex: {
29
+ limitId: 'codex',
30
+ primary: {
31
+ usedPercent: 22,
32
+ windowDurationMins: 300,
33
+ resetsAt: 1_700_000_000,
34
+ },
35
+ secondary: {
36
+ usedPercent: 61,
37
+ windowDurationMins: 10_080,
38
+ resetsAt: 1_700_000_100,
39
+ },
40
+ planType: 'plus',
41
+ },
42
+ },
43
+ rateLimits: {
44
+ limitId: 'legacy',
45
+ primary: {
46
+ usedPercent: 99,
47
+ windowDurationMins: 60,
48
+ resetsAt: 1_700_000_200,
49
+ },
50
+ },
51
+ });
52
+
53
+ const client = new HostBridgeApiClient({ ws: ws as unknown as HostBridgeWsClient });
54
+ const result = await client.readAccountRateLimits();
55
+
56
+ expect(ws.request).toHaveBeenCalledWith('account/rateLimits/read');
57
+ expect(result).toMatchObject({
58
+ limitId: 'codex',
59
+ planType: 'plus',
60
+ primary: {
61
+ usedPercent: 22,
62
+ windowDurationMins: 300,
63
+ },
64
+ secondary: {
65
+ usedPercent: 61,
66
+ windowDurationMins: 10080,
67
+ },
68
+ });
69
+ });
70
+
71
+ it('readAccountRateLimits() falls back to first populated keyed snapshot with snake_case payloads', async () => {
72
+ const ws = createWsMock();
73
+ ws.request.mockResolvedValue({
74
+ rate_limits_by_limit_id: {
75
+ empty: {
76
+ limit_id: 'empty',
77
+ primary: null,
78
+ secondary: null,
79
+ },
80
+ shared: {
81
+ limit_id: 'shared',
82
+ limit_name: 'Shared',
83
+ primary: {
84
+ used_percent: '15',
85
+ window_duration_mins: '300',
86
+ resets_at: '1700000000',
87
+ },
88
+ secondary: null,
89
+ plan_type: 'team',
90
+ },
91
+ },
92
+ });
93
+
94
+ const client = new HostBridgeApiClient({ ws: ws as unknown as HostBridgeWsClient });
95
+ const result = await client.readAccountRateLimits();
96
+
97
+ expect(result).toMatchObject({
98
+ limitId: 'shared',
99
+ limitName: 'Shared',
100
+ planType: 'team',
101
+ primary: {
102
+ usedPercent: 15,
103
+ windowDurationMins: 300,
104
+ resetsAt: 1700000000,
105
+ },
106
+ secondary: null,
107
+ });
108
+ });
109
+
110
+ it('readAccountRateLimits() falls back to top-level rate limits when keyed buckets are unavailable', async () => {
111
+ const ws = createWsMock();
112
+ ws.request.mockResolvedValue({
113
+ rateLimitsByLimitId: {
114
+ codex: {
115
+ limitId: 'codex',
116
+ primary: null,
117
+ secondary: null,
118
+ },
119
+ },
120
+ rate_limits: {
121
+ limit_id: 'legacy',
122
+ primary: {
123
+ used_percent: 44,
124
+ window_duration_mins: 60,
125
+ resets_at: 1700001234,
126
+ },
127
+ secondary: null,
128
+ plan_type: 'pro',
129
+ },
130
+ });
131
+
132
+ const client = new HostBridgeApiClient({ ws: ws as unknown as HostBridgeWsClient });
133
+ const result = await client.readAccountRateLimits();
134
+
135
+ expect(result).toMatchObject({
136
+ limitId: 'legacy',
137
+ planType: 'pro',
138
+ primary: {
139
+ usedPercent: 44,
140
+ windowDurationMins: 60,
141
+ resetsAt: 1700001234,
142
+ },
143
+ });
144
+ });
145
+
146
+ it('readAccount() requests account/read and maps ChatGPT account details', async () => {
147
+ const ws = createWsMock();
148
+ ws.request.mockResolvedValue({
149
+ account: {
150
+ type: 'chatgpt',
151
+ email: 'mohit@example.com',
152
+ planType: 'plus',
153
+ },
154
+ requiresOpenaiAuth: true,
155
+ });
156
+
157
+ const client = new HostBridgeApiClient({ ws: ws as unknown as HostBridgeWsClient });
158
+ const result = await client.readAccount();
159
+
160
+ expect(ws.request).toHaveBeenCalledWith('account/read', { refreshToken: false });
161
+ expect(result).toEqual({
162
+ type: 'chatgpt',
163
+ email: 'mohit@example.com',
164
+ planType: 'plus',
165
+ requiresOpenaiAuth: true,
166
+ });
167
+ });
168
+
169
+ it('readAccount() maps API key auth without ChatGPT fields', async () => {
170
+ const ws = createWsMock();
171
+ ws.request.mockResolvedValue({
172
+ account: {
173
+ type: 'apiKey',
174
+ },
175
+ requires_openai_auth: false,
176
+ });
177
+
178
+ const client = new HostBridgeApiClient({ ws: ws as unknown as HostBridgeWsClient });
179
+ const result = await client.readAccount();
180
+
181
+ expect(result).toEqual({
182
+ type: 'apiKey',
183
+ email: null,
184
+ planType: null,
185
+ requiresOpenaiAuth: false,
186
+ });
187
+ });
188
+
189
+ it('logoutAccount() requests account/logout', async () => {
190
+ const ws = createWsMock();
191
+ ws.request.mockResolvedValue({});
192
+
193
+ const client = new HostBridgeApiClient({ ws: ws as unknown as HostBridgeWsClient });
194
+ await client.logoutAccount();
195
+
196
+ expect(ws.request).toHaveBeenCalledWith('account/logout');
197
+ });
198
+
24
199
  it('listChats() maps app-server list response', async () => {
25
200
  const ws = createWsMock();
26
201
  ws.request.mockResolvedValue({
@@ -102,7 +277,7 @@ describe('HostBridgeApiClient', () => {
102
277
  updatedAt: 1700000002,
103
278
  status: { type: 'idle' },
104
279
  source: {
105
- subAgent: {
280
+ subagent: {
106
281
  thread_spawn: {
107
282
  parent_thread_id: 'thr_root',
108
283
  depth: 1,
@@ -129,6 +304,156 @@ describe('HostBridgeApiClient', () => {
129
304
  expect(chats.map((chat) => chat.id)).toEqual(['thr_root']);
130
305
  });
131
306
 
307
+ it('listChats() can include sub-agent source kinds when requested', async () => {
308
+ const ws = createWsMock();
309
+ ws.request.mockResolvedValue({
310
+ data: [
311
+ {
312
+ id: 'thr_root',
313
+ preview: 'root chat',
314
+ createdAt: 1700000000,
315
+ updatedAt: 1700000001,
316
+ status: { type: 'idle' },
317
+ source: 'appServer',
318
+ turns: [],
319
+ },
320
+ {
321
+ id: 'thr_sub',
322
+ preview: 'spawned worker',
323
+ createdAt: 1700000000,
324
+ updatedAt: 1700000002,
325
+ status: { type: 'idle' },
326
+ source: {
327
+ subAgent: {
328
+ thread_spawn: {
329
+ parent_thread_id: 'thr_root',
330
+ depth: 1,
331
+ },
332
+ },
333
+ },
334
+ turns: [],
335
+ },
336
+ ],
337
+ });
338
+
339
+ const client = new HostBridgeApiClient({ ws: ws as unknown as HostBridgeWsClient });
340
+ const chats = await client.listChats({ includeSubAgents: true });
341
+
342
+ expect(ws.request).toHaveBeenCalledWith('thread/list', {
343
+ cursor: null,
344
+ limit: 200,
345
+ sortKey: null,
346
+ modelProviders: null,
347
+ sourceKinds: [
348
+ 'cli',
349
+ 'vscode',
350
+ 'exec',
351
+ 'appServer',
352
+ 'unknown',
353
+ 'subAgent',
354
+ 'subAgentReview',
355
+ 'subAgentCompact',
356
+ 'subAgentThreadSpawn',
357
+ 'subAgentOther',
358
+ ],
359
+ archived: false,
360
+ cwd: null,
361
+ });
362
+ expect(chats.map((chat) => chat.id)).toEqual(['thr_sub', 'thr_root']);
363
+ expect(chats[0].parentThreadId).toBe('thr_root');
364
+ expect(chats[0].subAgentDepth).toBe(1);
365
+ });
366
+
367
+ it('listLoadedChatIds() returns loaded in-memory thread ids', async () => {
368
+ const ws = createWsMock();
369
+ ws.request.mockResolvedValue({
370
+ data: ['thr_root', 'thr_sub', null, ''],
371
+ });
372
+
373
+ const client = new HostBridgeApiClient({ ws: ws as unknown as HostBridgeWsClient });
374
+ const ids = await client.listLoadedChatIds();
375
+
376
+ expect(ws.request).toHaveBeenCalledWith('thread/loaded/list', undefined);
377
+ expect(ids).toEqual(['thr_root', 'thr_sub']);
378
+ });
379
+
380
+ it('listWorkspaceRoots() requests bridge/workspaces/list and maps workspaces', async () => {
381
+ const ws = createWsMock();
382
+ ws.request.mockResolvedValue({
383
+ bridgeRoot: '/Users/mohit/work',
384
+ allowOutsideRootCwd: true,
385
+ workspaces: [
386
+ { path: '/Users/mohit/work/app', chatCount: 3, updatedAt: 1700000000 },
387
+ { path: '/Users/mohit/work/docs', chatCount: '1', updatedAt: '1700001000' },
388
+ { path: '', chatCount: 99, updatedAt: 1700002000 },
389
+ ],
390
+ });
391
+
392
+ const client = new HostBridgeApiClient({ ws: ws as unknown as HostBridgeWsClient });
393
+ const result = await client.listWorkspaceRoots();
394
+
395
+ expect(ws.request).toHaveBeenCalledWith('bridge/workspaces/list', { limit: 200 });
396
+ expect(result).toEqual({
397
+ bridgeRoot: '/Users/mohit/work',
398
+ allowOutsideRootCwd: true,
399
+ workspaces: [
400
+ {
401
+ path: '/Users/mohit/work/app',
402
+ chatCount: 3,
403
+ updatedAt: new Date(1700000000 * 1000).toISOString(),
404
+ },
405
+ {
406
+ path: '/Users/mohit/work/docs',
407
+ chatCount: 1,
408
+ updatedAt: new Date(1700001000 * 1000).toISOString(),
409
+ },
410
+ ],
411
+ });
412
+ });
413
+
414
+ it('listFilesystemEntries() requests bridge/fs/list with directory browsing defaults', async () => {
415
+ const ws = createWsMock();
416
+ ws.request.mockResolvedValue({
417
+ bridgeRoot: '/Users/mohit/work',
418
+ path: '/Users/mohit/work',
419
+ parentPath: '/Users/mohit',
420
+ entries: [
421
+ {
422
+ name: 'apps',
423
+ path: '/Users/mohit/work/apps',
424
+ kind: 'directory',
425
+ hidden: false,
426
+ selectable: true,
427
+ isGitRepo: false,
428
+ },
429
+ ],
430
+ });
431
+
432
+ const client = new HostBridgeApiClient({ ws: ws as unknown as HostBridgeWsClient });
433
+ const result = await client.listFilesystemEntries({ path: '/Users/mohit/work' });
434
+
435
+ expect(ws.request).toHaveBeenCalledWith('bridge/fs/list', {
436
+ path: '/Users/mohit/work',
437
+ includeHidden: false,
438
+ directoriesOnly: true,
439
+ });
440
+ expect(result).toEqual({
441
+ bridgeRoot: '/Users/mohit/work',
442
+ path: '/Users/mohit/work',
443
+ parentPath: '/Users/mohit',
444
+ entries: [
445
+ {
446
+ name: 'apps',
447
+ path: '/Users/mohit/work/apps',
448
+ kind: 'directory',
449
+ hidden: false,
450
+ selectable: true,
451
+ isGitRepo: false,
452
+ },
453
+ ],
454
+ });
455
+ });
456
+
132
457
  it('sendChatMessage() starts a turn without waiting for completion', async () => {
133
458
  const ws = createWsMock();
134
459
  ws.request
@@ -339,6 +664,108 @@ describe('HostBridgeApiClient', () => {
339
664
  );
340
665
  });
341
666
 
667
+ it('createChat() requests danger-full-access sandbox by default', async () => {
668
+ const ws = createWsMock();
669
+ ws.request.mockResolvedValueOnce({
670
+ thread: {
671
+ id: 'thr_sandbox',
672
+ preview: '',
673
+ createdAt: 1700000000,
674
+ updatedAt: 1700000000,
675
+ status: { type: 'idle' },
676
+ turns: [],
677
+ },
678
+ });
679
+
680
+ const client = new HostBridgeApiClient({ ws: ws as unknown as HostBridgeWsClient });
681
+ await client.createChat({});
682
+
683
+ expect(ws.request).toHaveBeenCalledWith(
684
+ 'thread/start',
685
+ expect.objectContaining({
686
+ sandbox: 'danger-full-access',
687
+ })
688
+ );
689
+ });
690
+
691
+ it('createChat() forwards service tier in thread/start config', async () => {
692
+ const ws = createWsMock();
693
+ ws.request.mockResolvedValueOnce({
694
+ thread: {
695
+ id: 'thr_fast',
696
+ preview: '',
697
+ createdAt: 1700000000,
698
+ updatedAt: 1700000000,
699
+ status: { type: 'idle' },
700
+ turns: [],
701
+ },
702
+ });
703
+
704
+ const client = new HostBridgeApiClient({ ws: ws as unknown as HostBridgeWsClient });
705
+ await client.createChat({ serviceTier: 'fast' });
706
+
707
+ expect(ws.request).toHaveBeenCalledWith(
708
+ 'thread/start',
709
+ expect.objectContaining({
710
+ config: {
711
+ service_tier: 'fast',
712
+ },
713
+ })
714
+ );
715
+ });
716
+
717
+ it('forkChat() forwards service tier in thread/fork config', async () => {
718
+ const ws = createWsMock();
719
+ ws.request.mockResolvedValueOnce({
720
+ thread: {
721
+ id: 'thr_fork_fast',
722
+ preview: '',
723
+ createdAt: 1700000000,
724
+ updatedAt: 1700000000,
725
+ status: { type: 'idle' },
726
+ turns: [],
727
+ },
728
+ });
729
+
730
+ const client = new HostBridgeApiClient({ ws: ws as unknown as HostBridgeWsClient });
731
+ await client.forkChat('thr_parent', { serviceTier: 'fast' });
732
+
733
+ expect(ws.request).toHaveBeenCalledWith(
734
+ 'thread/fork',
735
+ expect.objectContaining({
736
+ threadId: 'thr_parent',
737
+ config: {
738
+ service_tier: 'fast',
739
+ },
740
+ })
741
+ );
742
+ });
743
+
744
+ it('forkChat() requests danger-full-access sandbox by default', async () => {
745
+ const ws = createWsMock();
746
+ ws.request.mockResolvedValueOnce({
747
+ thread: {
748
+ id: 'thr_fork_sandbox',
749
+ preview: '',
750
+ createdAt: 1700000000,
751
+ updatedAt: 1700000000,
752
+ status: { type: 'idle' },
753
+ turns: [],
754
+ },
755
+ });
756
+
757
+ const client = new HostBridgeApiClient({ ws: ws as unknown as HostBridgeWsClient });
758
+ await client.forkChat('thr_parent');
759
+
760
+ expect(ws.request).toHaveBeenCalledWith(
761
+ 'thread/fork',
762
+ expect.objectContaining({
763
+ threadId: 'thr_parent',
764
+ sandbox: 'danger-full-access',
765
+ })
766
+ );
767
+ });
768
+
342
769
  it('renameChat() retries with threadName when name payload is rejected', async () => {
343
770
  const ws = createWsMock();
344
771
  ws.request
@@ -425,6 +852,90 @@ describe('HostBridgeApiClient', () => {
425
852
  );
426
853
  });
427
854
 
855
+ it('sendChatMessage() forwards service tier to turn/start', async () => {
856
+ const ws = createWsMock();
857
+ ws.request
858
+ .mockResolvedValueOnce({}) // thread/resume
859
+ .mockResolvedValueOnce({ turn: { id: 'turn_fast' } }) // turn/start
860
+ .mockResolvedValueOnce({
861
+ thread: {
862
+ id: 'thr_fast',
863
+ preview: 'done',
864
+ createdAt: 1700000000,
865
+ updatedAt: 1700000002,
866
+ status: { type: 'idle' },
867
+ turns: [
868
+ {
869
+ id: 'turn_fast',
870
+ items: [
871
+ {
872
+ type: 'userMessage',
873
+ id: 'u_fast',
874
+ content: [{ type: 'text', text: 'hello' }],
875
+ },
876
+ {
877
+ type: 'agentMessage',
878
+ id: 'a_fast',
879
+ text: 'ok',
880
+ },
881
+ ],
882
+ },
883
+ ],
884
+ },
885
+ });
886
+
887
+ const client = new HostBridgeApiClient({ ws: ws as unknown as HostBridgeWsClient });
888
+ await client.sendChatMessage('thr_fast', {
889
+ content: 'hello',
890
+ serviceTier: 'fast',
891
+ });
892
+
893
+ expect(ws.request).toHaveBeenNthCalledWith(1, 'thread/resume', expect.any(Object));
894
+ expect(ws.request).toHaveBeenNthCalledWith(
895
+ 2,
896
+ 'turn/start',
897
+ expect.objectContaining({
898
+ serviceTier: 'fast',
899
+ })
900
+ );
901
+ });
902
+
903
+ it('steerChatTurn() forwards expected turn id and structured input', async () => {
904
+ const ws = createWsMock();
905
+ ws.request.mockResolvedValueOnce({});
906
+
907
+ const client = new HostBridgeApiClient({ ws: ws as unknown as HostBridgeWsClient });
908
+ await client.steerChatTurn('thr_steer', 'turn_steer', {
909
+ content: 'continue with this direction',
910
+ mentions: [{ path: '/tmp/src', name: 'src' }],
911
+ localImages: [{ path: '/tmp/screenshot.png' }],
912
+ });
913
+
914
+ expect(ws.request).toHaveBeenCalledWith(
915
+ 'turn/steer',
916
+ expect.objectContaining({
917
+ threadId: 'thr_steer',
918
+ expectedTurnId: 'turn_steer',
919
+ input: [
920
+ {
921
+ type: 'text',
922
+ text: 'continue with this direction',
923
+ text_elements: [],
924
+ },
925
+ {
926
+ type: 'mention',
927
+ path: '/tmp/src',
928
+ name: 'src',
929
+ },
930
+ {
931
+ type: 'localImage',
932
+ path: '/tmp/screenshot.png',
933
+ },
934
+ ],
935
+ })
936
+ );
937
+ });
938
+
428
939
  it('sendChatMessage() forwards selected approval policy to resume and turn/start', async () => {
429
940
  const ws = createWsMock();
430
941
  ws.request
@@ -481,7 +992,10 @@ describe('HostBridgeApiClient', () => {
481
992
  .mockResolvedValueOnce({});
482
993
 
483
994
  const client = new HostBridgeApiClient({ ws: ws as unknown as HostBridgeWsClient });
484
- await expect(client.resumeThread('thr_resume')).resolves.toBeUndefined();
995
+ await expect(client.resumeThread('thr_resume')).resolves.toEqual({
996
+ model: null,
997
+ effort: null,
998
+ });
485
999
 
486
1000
  expect(ws.request).toHaveBeenNthCalledWith(
487
1001
  1,
@@ -490,6 +1004,7 @@ describe('HostBridgeApiClient', () => {
490
1004
  threadId: 'thr_resume',
491
1005
  experimentalRawEvents: true,
492
1006
  approvalPolicy: 'untrusted',
1007
+ sandbox: 'danger-full-access',
493
1008
  })
494
1009
  );
495
1010
  expect(ws.request).toHaveBeenNthCalledWith(
@@ -500,6 +1015,7 @@ describe('HostBridgeApiClient', () => {
500
1015
  approvalPolicy: 'on-request',
501
1016
  developerInstructions: expect.any(String),
502
1017
  experimentalRawEvents: true,
1018
+ sandbox: 'danger-full-access',
503
1019
  })
504
1020
  );
505
1021
  });
@@ -512,7 +1028,10 @@ describe('HostBridgeApiClient', () => {
512
1028
  .mockResolvedValueOnce({});
513
1029
 
514
1030
  const client = new HostBridgeApiClient({ ws: ws as unknown as HostBridgeWsClient });
515
- await expect(client.resumeThread('thr_resume_legacy')).resolves.toBeUndefined();
1031
+ await expect(client.resumeThread('thr_resume_legacy')).resolves.toEqual({
1032
+ model: null,
1033
+ effort: null,
1034
+ });
516
1035
 
517
1036
  expect(ws.request).toHaveBeenNthCalledWith(
518
1037
  1,
@@ -521,6 +1040,7 @@ describe('HostBridgeApiClient', () => {
521
1040
  threadId: 'thr_resume_legacy',
522
1041
  experimentalRawEvents: true,
523
1042
  approvalPolicy: 'untrusted',
1043
+ sandbox: 'danger-full-access',
524
1044
  })
525
1045
  );
526
1046
  expect(ws.request).toHaveBeenNthCalledWith(
@@ -531,6 +1051,7 @@ describe('HostBridgeApiClient', () => {
531
1051
  approvalPolicy: 'on-request',
532
1052
  developerInstructions: expect.any(String),
533
1053
  experimentalRawEvents: true,
1054
+ sandbox: 'danger-full-access',
534
1055
  })
535
1056
  );
536
1057
  expect(ws.request).toHaveBeenNthCalledWith(
@@ -540,6 +1061,7 @@ describe('HostBridgeApiClient', () => {
540
1061
  threadId: 'thr_resume_legacy',
541
1062
  approvalPolicy: 'on-request',
542
1063
  developerInstructions: null,
1064
+ sandbox: 'danger-full-access',
543
1065
  })
544
1066
  );
545
1067
 
@@ -556,7 +1078,10 @@ describe('HostBridgeApiClient', () => {
556
1078
  const client = new HostBridgeApiClient({ ws: ws as unknown as HostBridgeWsClient });
557
1079
  await expect(
558
1080
  client.resumeThread('thr_resume_never', { approvalPolicy: 'never' })
559
- ).resolves.toBeUndefined();
1081
+ ).resolves.toEqual({
1082
+ model: null,
1083
+ effort: null,
1084
+ });
560
1085
 
561
1086
  expect(ws.request).toHaveBeenNthCalledWith(
562
1087
  1,
@@ -564,6 +1089,7 @@ describe('HostBridgeApiClient', () => {
564
1089
  expect.objectContaining({
565
1090
  threadId: 'thr_resume_never',
566
1091
  approvalPolicy: 'never',
1092
+ sandbox: 'danger-full-access',
567
1093
  })
568
1094
  );
569
1095
  expect(ws.request).toHaveBeenNthCalledWith(
@@ -572,6 +1098,7 @@ describe('HostBridgeApiClient', () => {
572
1098
  expect.objectContaining({
573
1099
  threadId: 'thr_resume_never',
574
1100
  approvalPolicy: 'never',
1101
+ sandbox: 'danger-full-access',
575
1102
  })
576
1103
  );
577
1104
  });
@@ -804,9 +1331,64 @@ describe('HostBridgeApiClient', () => {
804
1331
  );
805
1332
  });
806
1333
 
1334
+ it('sendChatMessage() sends structured collaborationMode for default mode using resumed thread settings', async () => {
1335
+ const ws = createWsMock();
1336
+ ws.request
1337
+ .mockResolvedValueOnce({
1338
+ model: 'gpt-5.3-codex',
1339
+ reasoningEffort: 'medium',
1340
+ }) // thread/resume
1341
+ .mockResolvedValueOnce({ turn: { id: 'turn_default' } }) // turn/start
1342
+ .mockResolvedValueOnce({
1343
+ thread: {
1344
+ id: 'thr_default',
1345
+ preview: 'done',
1346
+ createdAt: 1700000000,
1347
+ updatedAt: 1700000002,
1348
+ status: { type: 'idle' },
1349
+ turns: [
1350
+ {
1351
+ id: 'turn_default',
1352
+ items: [
1353
+ {
1354
+ type: 'userMessage',
1355
+ id: 'u_default',
1356
+ content: [{ type: 'text', text: 'implement it' }],
1357
+ },
1358
+ ],
1359
+ },
1360
+ ],
1361
+ },
1362
+ });
1363
+
1364
+ const client = new HostBridgeApiClient({ ws: ws as unknown as HostBridgeWsClient });
1365
+ await client.sendChatMessage('thr_default', {
1366
+ content: 'implement it',
1367
+ collaborationMode: 'default',
1368
+ });
1369
+
1370
+ expect(ws.request).toHaveBeenNthCalledWith(
1371
+ 2,
1372
+ 'turn/start',
1373
+ expect.objectContaining({
1374
+ model: 'gpt-5.3-codex',
1375
+ effort: 'medium',
1376
+ collaborationMode: {
1377
+ mode: 'default',
1378
+ settings: {
1379
+ model: 'gpt-5.3-codex',
1380
+ reasoning_effort: 'medium',
1381
+ developer_instructions: null,
1382
+ },
1383
+ },
1384
+ })
1385
+ );
1386
+ });
1387
+
807
1388
  it('sendChatMessage() resolves default model before plan mode turn when model is unset', async () => {
808
1389
  const ws = createWsMock();
809
1390
  ws.request
1391
+ .mockResolvedValueOnce({}) // thread/resume
810
1392
  .mockResolvedValueOnce({
811
1393
  data: [
812
1394
  {
@@ -816,7 +1398,6 @@ describe('HostBridgeApiClient', () => {
816
1398
  },
817
1399
  ],
818
1400
  }) // model/list fallback
819
- .mockResolvedValueOnce({}) // thread/resume
820
1401
  .mockResolvedValueOnce({ turn: { id: 'turn_plan_fallback' } }) // turn/start
821
1402
  .mockResolvedValueOnce({
822
1403
  thread: {
@@ -847,7 +1428,7 @@ describe('HostBridgeApiClient', () => {
847
1428
  });
848
1429
 
849
1430
  expect(ws.request).toHaveBeenNthCalledWith(
850
- 1,
1431
+ 2,
851
1432
  'model/list',
852
1433
  expect.objectContaining({
853
1434
  includeHidden: false,