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.
- package/.github/workflows/ci.yml +4 -3
- package/.github/workflows/npm-release.yml +62 -2
- package/.github/workflows/pages.yml +41 -0
- package/AGENTS.md +263 -110
- package/README.md +15 -4
- 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 +14 -13
- package/apps/mobile/src/api/__tests__/chatMapping.test.ts +219 -0
- package/apps/mobile/src/api/__tests__/client.test.ts +587 -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 +321 -36
- package/apps/mobile/src/api/rateLimits.ts +143 -0
- package/apps/mobile/src/api/types.ts +107 -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 +812 -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 +4239 -1237
- package/apps/mobile/src/screens/OnboardingScreen.tsx +924 -310
- 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/bin/clawdex.js +7 -6
- package/codex-rust-bridge +0 -0
- package/codex-rust-bridge.exe +0 -0
- package/docs/codex-app-server-cli-gap-tracker.md +14 -5
- package/docs/privacy-policy.md +54 -0
- package/docs/setup-and-operations.md +21 -15
- package/docs/terms-of-service.md +33 -0
- package/docs/troubleshooting.md +15 -19
- package/package.json +6 -5
- package/scripts/bridge-binary.js +194 -0
- package/scripts/setup-wizard.sh +17 -186
- package/scripts/start-bridge-secure.js +240 -0
- package/scripts/start-bridge-secure.sh +1 -40
- package/services/mac-bridge/package.json +6 -6
- package/services/rust-bridge/Cargo.lock +56 -47
- package/services/rust-bridge/Cargo.toml +1 -1
- package/services/rust-bridge/package.json +1 -1
- package/services/rust-bridge/src/main.rs +517 -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,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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1431
|
+
2,
|
|
851
1432
|
'model/list',
|
|
852
1433
|
expect.objectContaining({
|
|
853
1434
|
includeHidden: false,
|