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
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1423
|
+
2,
|
|
851
1424
|
'model/list',
|
|
852
1425
|
expect.objectContaining({
|
|
853
1426
|
includeHidden: false,
|