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.
Files changed (71) hide show
  1. package/.github/workflows/pages.yml +41 -0
  2. package/AGENTS.md +263 -110
  3. package/README.md +11 -0
  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 +58 -363
  64. package/services/rust-bridge/Cargo.toml +2 -2
  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
@@ -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,148 @@ 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 },
387
+ { path: '/Users/mohit/work/docs', chatCount: '1' },
388
+ { path: '', chatCount: 99 },
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
+ { path: '/Users/mohit/work/app', chatCount: 3 },
401
+ { path: '/Users/mohit/work/docs', chatCount: 1 },
402
+ ],
403
+ });
404
+ });
405
+
406
+ it('listFilesystemEntries() requests bridge/fs/list with directory browsing defaults', async () => {
407
+ const ws = createWsMock();
408
+ ws.request.mockResolvedValue({
409
+ bridgeRoot: '/Users/mohit/work',
410
+ path: '/Users/mohit/work',
411
+ parentPath: '/Users/mohit',
412
+ entries: [
413
+ {
414
+ name: 'apps',
415
+ path: '/Users/mohit/work/apps',
416
+ kind: 'directory',
417
+ hidden: false,
418
+ selectable: true,
419
+ isGitRepo: false,
420
+ },
421
+ ],
422
+ });
423
+
424
+ const client = new HostBridgeApiClient({ ws: ws as unknown as HostBridgeWsClient });
425
+ const result = await client.listFilesystemEntries({ path: '/Users/mohit/work' });
426
+
427
+ expect(ws.request).toHaveBeenCalledWith('bridge/fs/list', {
428
+ path: '/Users/mohit/work',
429
+ includeHidden: false,
430
+ directoriesOnly: true,
431
+ });
432
+ expect(result).toEqual({
433
+ bridgeRoot: '/Users/mohit/work',
434
+ path: '/Users/mohit/work',
435
+ parentPath: '/Users/mohit',
436
+ entries: [
437
+ {
438
+ name: 'apps',
439
+ path: '/Users/mohit/work/apps',
440
+ kind: 'directory',
441
+ hidden: false,
442
+ selectable: true,
443
+ isGitRepo: false,
444
+ },
445
+ ],
446
+ });
447
+ });
448
+
132
449
  it('sendChatMessage() starts a turn without waiting for completion', async () => {
133
450
  const ws = createWsMock();
134
451
  ws.request
@@ -339,6 +656,108 @@ describe('HostBridgeApiClient', () => {
339
656
  );
340
657
  });
341
658
 
659
+ it('createChat() requests danger-full-access sandbox by default', async () => {
660
+ const ws = createWsMock();
661
+ ws.request.mockResolvedValueOnce({
662
+ thread: {
663
+ id: 'thr_sandbox',
664
+ preview: '',
665
+ createdAt: 1700000000,
666
+ updatedAt: 1700000000,
667
+ status: { type: 'idle' },
668
+ turns: [],
669
+ },
670
+ });
671
+
672
+ const client = new HostBridgeApiClient({ ws: ws as unknown as HostBridgeWsClient });
673
+ await client.createChat({});
674
+
675
+ expect(ws.request).toHaveBeenCalledWith(
676
+ 'thread/start',
677
+ expect.objectContaining({
678
+ sandbox: 'danger-full-access',
679
+ })
680
+ );
681
+ });
682
+
683
+ it('createChat() forwards service tier in thread/start config', async () => {
684
+ const ws = createWsMock();
685
+ ws.request.mockResolvedValueOnce({
686
+ thread: {
687
+ id: 'thr_fast',
688
+ preview: '',
689
+ createdAt: 1700000000,
690
+ updatedAt: 1700000000,
691
+ status: { type: 'idle' },
692
+ turns: [],
693
+ },
694
+ });
695
+
696
+ const client = new HostBridgeApiClient({ ws: ws as unknown as HostBridgeWsClient });
697
+ await client.createChat({ serviceTier: 'fast' });
698
+
699
+ expect(ws.request).toHaveBeenCalledWith(
700
+ 'thread/start',
701
+ expect.objectContaining({
702
+ config: {
703
+ service_tier: 'fast',
704
+ },
705
+ })
706
+ );
707
+ });
708
+
709
+ it('forkChat() forwards service tier in thread/fork config', async () => {
710
+ const ws = createWsMock();
711
+ ws.request.mockResolvedValueOnce({
712
+ thread: {
713
+ id: 'thr_fork_fast',
714
+ preview: '',
715
+ createdAt: 1700000000,
716
+ updatedAt: 1700000000,
717
+ status: { type: 'idle' },
718
+ turns: [],
719
+ },
720
+ });
721
+
722
+ const client = new HostBridgeApiClient({ ws: ws as unknown as HostBridgeWsClient });
723
+ await client.forkChat('thr_parent', { serviceTier: 'fast' });
724
+
725
+ expect(ws.request).toHaveBeenCalledWith(
726
+ 'thread/fork',
727
+ expect.objectContaining({
728
+ threadId: 'thr_parent',
729
+ config: {
730
+ service_tier: 'fast',
731
+ },
732
+ })
733
+ );
734
+ });
735
+
736
+ it('forkChat() requests danger-full-access sandbox by default', async () => {
737
+ const ws = createWsMock();
738
+ ws.request.mockResolvedValueOnce({
739
+ thread: {
740
+ id: 'thr_fork_sandbox',
741
+ preview: '',
742
+ createdAt: 1700000000,
743
+ updatedAt: 1700000000,
744
+ status: { type: 'idle' },
745
+ turns: [],
746
+ },
747
+ });
748
+
749
+ const client = new HostBridgeApiClient({ ws: ws as unknown as HostBridgeWsClient });
750
+ await client.forkChat('thr_parent');
751
+
752
+ expect(ws.request).toHaveBeenCalledWith(
753
+ 'thread/fork',
754
+ expect.objectContaining({
755
+ threadId: 'thr_parent',
756
+ sandbox: 'danger-full-access',
757
+ })
758
+ );
759
+ });
760
+
342
761
  it('renameChat() retries with threadName when name payload is rejected', async () => {
343
762
  const ws = createWsMock();
344
763
  ws.request
@@ -425,6 +844,90 @@ describe('HostBridgeApiClient', () => {
425
844
  );
426
845
  });
427
846
 
847
+ it('sendChatMessage() forwards service tier to turn/start', async () => {
848
+ const ws = createWsMock();
849
+ ws.request
850
+ .mockResolvedValueOnce({}) // thread/resume
851
+ .mockResolvedValueOnce({ turn: { id: 'turn_fast' } }) // turn/start
852
+ .mockResolvedValueOnce({
853
+ thread: {
854
+ id: 'thr_fast',
855
+ preview: 'done',
856
+ createdAt: 1700000000,
857
+ updatedAt: 1700000002,
858
+ status: { type: 'idle' },
859
+ turns: [
860
+ {
861
+ id: 'turn_fast',
862
+ items: [
863
+ {
864
+ type: 'userMessage',
865
+ id: 'u_fast',
866
+ content: [{ type: 'text', text: 'hello' }],
867
+ },
868
+ {
869
+ type: 'agentMessage',
870
+ id: 'a_fast',
871
+ text: 'ok',
872
+ },
873
+ ],
874
+ },
875
+ ],
876
+ },
877
+ });
878
+
879
+ const client = new HostBridgeApiClient({ ws: ws as unknown as HostBridgeWsClient });
880
+ await client.sendChatMessage('thr_fast', {
881
+ content: 'hello',
882
+ serviceTier: 'fast',
883
+ });
884
+
885
+ expect(ws.request).toHaveBeenNthCalledWith(1, 'thread/resume', expect.any(Object));
886
+ expect(ws.request).toHaveBeenNthCalledWith(
887
+ 2,
888
+ 'turn/start',
889
+ expect.objectContaining({
890
+ serviceTier: 'fast',
891
+ })
892
+ );
893
+ });
894
+
895
+ it('steerChatTurn() forwards expected turn id and structured input', async () => {
896
+ const ws = createWsMock();
897
+ ws.request.mockResolvedValueOnce({});
898
+
899
+ const client = new HostBridgeApiClient({ ws: ws as unknown as HostBridgeWsClient });
900
+ await client.steerChatTurn('thr_steer', 'turn_steer', {
901
+ content: 'continue with this direction',
902
+ mentions: [{ path: '/tmp/src', name: 'src' }],
903
+ localImages: [{ path: '/tmp/screenshot.png' }],
904
+ });
905
+
906
+ expect(ws.request).toHaveBeenCalledWith(
907
+ 'turn/steer',
908
+ expect.objectContaining({
909
+ threadId: 'thr_steer',
910
+ expectedTurnId: 'turn_steer',
911
+ input: [
912
+ {
913
+ type: 'text',
914
+ text: 'continue with this direction',
915
+ text_elements: [],
916
+ },
917
+ {
918
+ type: 'mention',
919
+ path: '/tmp/src',
920
+ name: 'src',
921
+ },
922
+ {
923
+ type: 'localImage',
924
+ path: '/tmp/screenshot.png',
925
+ },
926
+ ],
927
+ })
928
+ );
929
+ });
930
+
428
931
  it('sendChatMessage() forwards selected approval policy to resume and turn/start', async () => {
429
932
  const ws = createWsMock();
430
933
  ws.request
@@ -481,7 +984,10 @@ describe('HostBridgeApiClient', () => {
481
984
  .mockResolvedValueOnce({});
482
985
 
483
986
  const client = new HostBridgeApiClient({ ws: ws as unknown as HostBridgeWsClient });
484
- await expect(client.resumeThread('thr_resume')).resolves.toBeUndefined();
987
+ await expect(client.resumeThread('thr_resume')).resolves.toEqual({
988
+ model: null,
989
+ effort: null,
990
+ });
485
991
 
486
992
  expect(ws.request).toHaveBeenNthCalledWith(
487
993
  1,
@@ -490,6 +996,7 @@ describe('HostBridgeApiClient', () => {
490
996
  threadId: 'thr_resume',
491
997
  experimentalRawEvents: true,
492
998
  approvalPolicy: 'untrusted',
999
+ sandbox: 'danger-full-access',
493
1000
  })
494
1001
  );
495
1002
  expect(ws.request).toHaveBeenNthCalledWith(
@@ -500,6 +1007,7 @@ describe('HostBridgeApiClient', () => {
500
1007
  approvalPolicy: 'on-request',
501
1008
  developerInstructions: expect.any(String),
502
1009
  experimentalRawEvents: true,
1010
+ sandbox: 'danger-full-access',
503
1011
  })
504
1012
  );
505
1013
  });
@@ -512,7 +1020,10 @@ describe('HostBridgeApiClient', () => {
512
1020
  .mockResolvedValueOnce({});
513
1021
 
514
1022
  const client = new HostBridgeApiClient({ ws: ws as unknown as HostBridgeWsClient });
515
- await expect(client.resumeThread('thr_resume_legacy')).resolves.toBeUndefined();
1023
+ await expect(client.resumeThread('thr_resume_legacy')).resolves.toEqual({
1024
+ model: null,
1025
+ effort: null,
1026
+ });
516
1027
 
517
1028
  expect(ws.request).toHaveBeenNthCalledWith(
518
1029
  1,
@@ -521,6 +1032,7 @@ describe('HostBridgeApiClient', () => {
521
1032
  threadId: 'thr_resume_legacy',
522
1033
  experimentalRawEvents: true,
523
1034
  approvalPolicy: 'untrusted',
1035
+ sandbox: 'danger-full-access',
524
1036
  })
525
1037
  );
526
1038
  expect(ws.request).toHaveBeenNthCalledWith(
@@ -531,6 +1043,7 @@ describe('HostBridgeApiClient', () => {
531
1043
  approvalPolicy: 'on-request',
532
1044
  developerInstructions: expect.any(String),
533
1045
  experimentalRawEvents: true,
1046
+ sandbox: 'danger-full-access',
534
1047
  })
535
1048
  );
536
1049
  expect(ws.request).toHaveBeenNthCalledWith(
@@ -540,6 +1053,7 @@ describe('HostBridgeApiClient', () => {
540
1053
  threadId: 'thr_resume_legacy',
541
1054
  approvalPolicy: 'on-request',
542
1055
  developerInstructions: null,
1056
+ sandbox: 'danger-full-access',
543
1057
  })
544
1058
  );
545
1059
 
@@ -556,7 +1070,10 @@ describe('HostBridgeApiClient', () => {
556
1070
  const client = new HostBridgeApiClient({ ws: ws as unknown as HostBridgeWsClient });
557
1071
  await expect(
558
1072
  client.resumeThread('thr_resume_never', { approvalPolicy: 'never' })
559
- ).resolves.toBeUndefined();
1073
+ ).resolves.toEqual({
1074
+ model: null,
1075
+ effort: null,
1076
+ });
560
1077
 
561
1078
  expect(ws.request).toHaveBeenNthCalledWith(
562
1079
  1,
@@ -564,6 +1081,7 @@ describe('HostBridgeApiClient', () => {
564
1081
  expect.objectContaining({
565
1082
  threadId: 'thr_resume_never',
566
1083
  approvalPolicy: 'never',
1084
+ sandbox: 'danger-full-access',
567
1085
  })
568
1086
  );
569
1087
  expect(ws.request).toHaveBeenNthCalledWith(
@@ -572,6 +1090,7 @@ describe('HostBridgeApiClient', () => {
572
1090
  expect.objectContaining({
573
1091
  threadId: 'thr_resume_never',
574
1092
  approvalPolicy: 'never',
1093
+ sandbox: 'danger-full-access',
575
1094
  })
576
1095
  );
577
1096
  });
@@ -804,9 +1323,64 @@ describe('HostBridgeApiClient', () => {
804
1323
  );
805
1324
  });
806
1325
 
1326
+ it('sendChatMessage() sends structured collaborationMode for default mode using resumed thread settings', async () => {
1327
+ const ws = createWsMock();
1328
+ ws.request
1329
+ .mockResolvedValueOnce({
1330
+ model: 'gpt-5.3-codex',
1331
+ reasoningEffort: 'medium',
1332
+ }) // thread/resume
1333
+ .mockResolvedValueOnce({ turn: { id: 'turn_default' } }) // turn/start
1334
+ .mockResolvedValueOnce({
1335
+ thread: {
1336
+ id: 'thr_default',
1337
+ preview: 'done',
1338
+ createdAt: 1700000000,
1339
+ updatedAt: 1700000002,
1340
+ status: { type: 'idle' },
1341
+ turns: [
1342
+ {
1343
+ id: 'turn_default',
1344
+ items: [
1345
+ {
1346
+ type: 'userMessage',
1347
+ id: 'u_default',
1348
+ content: [{ type: 'text', text: 'implement it' }],
1349
+ },
1350
+ ],
1351
+ },
1352
+ ],
1353
+ },
1354
+ });
1355
+
1356
+ const client = new HostBridgeApiClient({ ws: ws as unknown as HostBridgeWsClient });
1357
+ await client.sendChatMessage('thr_default', {
1358
+ content: 'implement it',
1359
+ collaborationMode: 'default',
1360
+ });
1361
+
1362
+ expect(ws.request).toHaveBeenNthCalledWith(
1363
+ 2,
1364
+ 'turn/start',
1365
+ expect.objectContaining({
1366
+ model: 'gpt-5.3-codex',
1367
+ effort: 'medium',
1368
+ collaborationMode: {
1369
+ mode: 'default',
1370
+ settings: {
1371
+ model: 'gpt-5.3-codex',
1372
+ reasoning_effort: 'medium',
1373
+ developer_instructions: null,
1374
+ },
1375
+ },
1376
+ })
1377
+ );
1378
+ });
1379
+
807
1380
  it('sendChatMessage() resolves default model before plan mode turn when model is unset', async () => {
808
1381
  const ws = createWsMock();
809
1382
  ws.request
1383
+ .mockResolvedValueOnce({}) // thread/resume
810
1384
  .mockResolvedValueOnce({
811
1385
  data: [
812
1386
  {
@@ -816,7 +1390,6 @@ describe('HostBridgeApiClient', () => {
816
1390
  },
817
1391
  ],
818
1392
  }) // model/list fallback
819
- .mockResolvedValueOnce({}) // thread/resume
820
1393
  .mockResolvedValueOnce({ turn: { id: 'turn_plan_fallback' } }) // turn/start
821
1394
  .mockResolvedValueOnce({
822
1395
  thread: {
@@ -847,7 +1420,7 @@ describe('HostBridgeApiClient', () => {
847
1420
  });
848
1421
 
849
1422
  expect(ws.request).toHaveBeenNthCalledWith(
850
- 1,
1423
+ 2,
851
1424
  'model/list',
852
1425
  expect.objectContaining({
853
1426
  includeHidden: false,