clawdex-mobile 2.0.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/pages.yml +41 -0
- package/AGENTS.md +263 -110
- package/README.md +1 -1
- 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 +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 +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
|
@@ -6,7 +6,11 @@ import {
|
|
|
6
6
|
type RawThread,
|
|
7
7
|
toRawThread,
|
|
8
8
|
} from './chatMapping';
|
|
9
|
+
import { readAccountSnapshot } from './account';
|
|
10
|
+
import { readAccountRateLimits as readSelectedAccountRateLimits } from './rateLimits';
|
|
9
11
|
import type {
|
|
12
|
+
AccountSnapshot,
|
|
13
|
+
AccountRateLimitSnapshot,
|
|
10
14
|
ApprovalPolicy,
|
|
11
15
|
ApprovalDecision,
|
|
12
16
|
CollaborationMode,
|
|
@@ -28,6 +32,7 @@ import type {
|
|
|
28
32
|
ResolveUserInputRequest,
|
|
29
33
|
ResolveUserInputResponse,
|
|
30
34
|
SendChatMessageRequest,
|
|
35
|
+
SteerChatTurnRequest,
|
|
31
36
|
MentionInput,
|
|
32
37
|
LocalImageInput,
|
|
33
38
|
UploadAttachmentRequest,
|
|
@@ -37,8 +42,12 @@ import type {
|
|
|
37
42
|
ModelOption,
|
|
38
43
|
ReasoningEffort,
|
|
39
44
|
ModelReasoningEffortOption,
|
|
45
|
+
ServiceTier,
|
|
40
46
|
TerminalExecRequest,
|
|
41
47
|
TerminalExecResponse,
|
|
48
|
+
WorkspaceListResponse,
|
|
49
|
+
FileSystemListRequest,
|
|
50
|
+
FileSystemListResponse,
|
|
42
51
|
} from './types';
|
|
43
52
|
import type { HostBridgeWsClient } from './ws';
|
|
44
53
|
|
|
@@ -56,6 +65,10 @@ interface AppServerListResponse {
|
|
|
56
65
|
data?: unknown[];
|
|
57
66
|
}
|
|
58
67
|
|
|
68
|
+
interface AppServerLoadedThreadListResponse {
|
|
69
|
+
data?: unknown[];
|
|
70
|
+
}
|
|
71
|
+
|
|
59
72
|
interface AppServerReadResponse {
|
|
60
73
|
thread?: unknown;
|
|
61
74
|
}
|
|
@@ -80,8 +93,18 @@ interface AppServerModelListResponse {
|
|
|
80
93
|
data?: unknown[];
|
|
81
94
|
}
|
|
82
95
|
|
|
96
|
+
interface AppServerConfigReadResponse {
|
|
97
|
+
config?: unknown;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface AppServerAccountReadResponse {
|
|
101
|
+
account?: unknown;
|
|
102
|
+
requiresOpenaiAuth?: boolean;
|
|
103
|
+
requires_openai_auth?: boolean;
|
|
104
|
+
}
|
|
105
|
+
|
|
83
106
|
interface AppServerCollaborationMode {
|
|
84
|
-
mode: 'plan';
|
|
107
|
+
mode: 'plan' | 'default';
|
|
85
108
|
settings: {
|
|
86
109
|
model: string;
|
|
87
110
|
reasoning_effort: ReasoningEffort | null;
|
|
@@ -89,11 +112,25 @@ interface AppServerCollaborationMode {
|
|
|
89
112
|
};
|
|
90
113
|
}
|
|
91
114
|
|
|
115
|
+
interface AppServerThreadRuntimeSettings {
|
|
116
|
+
model: string | null;
|
|
117
|
+
effort: ReasoningEffort | null;
|
|
118
|
+
}
|
|
119
|
+
|
|
92
120
|
type AppServerThreadSetNameResponse = Record<string, never>;
|
|
93
121
|
|
|
94
122
|
const CHAT_LIST_SOURCE_KINDS = ['cli', 'vscode', 'exec', 'appServer', 'unknown'] as const;
|
|
123
|
+
const CHAT_LIST_SOURCE_KINDS_WITH_SUBAGENTS = [
|
|
124
|
+
...CHAT_LIST_SOURCE_KINDS,
|
|
125
|
+
'subAgent',
|
|
126
|
+
'subAgentReview',
|
|
127
|
+
'subAgentCompact',
|
|
128
|
+
'subAgentThreadSpawn',
|
|
129
|
+
'subAgentOther',
|
|
130
|
+
] as const;
|
|
95
131
|
const MOBILE_DEVELOPER_INSTRUCTIONS =
|
|
96
132
|
'When you need clarification, call request_user_input instead of asking only in plain text. Provide 2-3 concise options whenever possible and use isOther when free-form input is appropriate.';
|
|
133
|
+
const MOBILE_DEFAULT_SANDBOX = 'danger-full-access';
|
|
97
134
|
|
|
98
135
|
interface ChatSnapshot {
|
|
99
136
|
rawThread: RawThread;
|
|
@@ -121,6 +158,10 @@ interface SendChatMessageOptions {
|
|
|
121
158
|
onTurnStarted?: (turnId: string) => void;
|
|
122
159
|
}
|
|
123
160
|
|
|
161
|
+
interface ListChatsOptions {
|
|
162
|
+
includeSubAgents?: boolean;
|
|
163
|
+
}
|
|
164
|
+
|
|
124
165
|
const ACTIVE_TURN_STATUSES = new Set([
|
|
125
166
|
'inprogress',
|
|
126
167
|
'in_progress',
|
|
@@ -142,13 +183,32 @@ export class HostBridgeApiClient {
|
|
|
142
183
|
return this.ws.request<HealthResponse>('bridge/health/read');
|
|
143
184
|
}
|
|
144
185
|
|
|
145
|
-
async
|
|
186
|
+
async readAccountRateLimits(): Promise<AccountRateLimitSnapshot | null> {
|
|
187
|
+
const response = await this.ws.request<Record<string, unknown>>('account/rateLimits/read');
|
|
188
|
+
return readSelectedAccountRateLimits(response);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async readAccount(): Promise<AccountSnapshot> {
|
|
192
|
+
const response = await this.ws.request<AppServerAccountReadResponse>('account/read', {
|
|
193
|
+
refreshToken: false,
|
|
194
|
+
});
|
|
195
|
+
return readAccountSnapshot(response);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async logoutAccount(): Promise<void> {
|
|
199
|
+
await this.ws.request('account/logout');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async listChats(options?: ListChatsOptions): Promise<ChatSummary[]> {
|
|
203
|
+
const includeSubAgents = options?.includeSubAgents === true;
|
|
146
204
|
const response = await this.ws.request<AppServerListResponse>('thread/list', {
|
|
147
205
|
cursor: null,
|
|
148
206
|
limit: 200,
|
|
149
207
|
sortKey: null,
|
|
150
208
|
modelProviders: null,
|
|
151
|
-
sourceKinds:
|
|
209
|
+
sourceKinds: includeSubAgents
|
|
210
|
+
? CHAT_LIST_SOURCE_KINDS_WITH_SUBAGENTS
|
|
211
|
+
: CHAT_LIST_SOURCE_KINDS,
|
|
152
212
|
archived: false,
|
|
153
213
|
cwd: null,
|
|
154
214
|
});
|
|
@@ -178,22 +238,52 @@ export class HostBridgeApiClient {
|
|
|
178
238
|
return mapped;
|
|
179
239
|
})
|
|
180
240
|
.filter((item): item is ChatSummary => item !== null)
|
|
181
|
-
.filter((item) => !isSubAgentSource(item.sourceKind))
|
|
241
|
+
.filter((item) => includeSubAgents || !isSubAgentSource(item.sourceKind))
|
|
182
242
|
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
183
243
|
}
|
|
184
244
|
|
|
245
|
+
async listLoadedChatIds(): Promise<string[]> {
|
|
246
|
+
const response = await this.ws.request<AppServerLoadedThreadListResponse>(
|
|
247
|
+
'thread/loaded/list',
|
|
248
|
+
undefined
|
|
249
|
+
);
|
|
250
|
+
const ids = Array.isArray(response.data) ? response.data : [];
|
|
251
|
+
return ids
|
|
252
|
+
.map((value) => readString(value)?.trim() ?? '')
|
|
253
|
+
.filter((value): value is string => value.length > 0);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async listWorkspaceRoots(limit = 200): Promise<WorkspaceListResponse> {
|
|
257
|
+
const response = await this.ws.request<Record<string, unknown>>('bridge/workspaces/list', {
|
|
258
|
+
limit,
|
|
259
|
+
});
|
|
260
|
+
return readWorkspaceListResponse(response);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async listFilesystemEntries(
|
|
264
|
+
request?: FileSystemListRequest
|
|
265
|
+
): Promise<FileSystemListResponse> {
|
|
266
|
+
const response = await this.ws.request<Record<string, unknown>>('bridge/fs/list', {
|
|
267
|
+
path: normalizeCwd(request?.path) ?? null,
|
|
268
|
+
includeHidden: request?.includeHidden === true,
|
|
269
|
+
directoriesOnly: request?.directoriesOnly !== false,
|
|
270
|
+
});
|
|
271
|
+
return readFileSystemListResponse(response);
|
|
272
|
+
}
|
|
273
|
+
|
|
185
274
|
async createChat(body: CreateChatRequest): Promise<Chat> {
|
|
186
275
|
const requestedCwd = normalizeCwd(body.cwd);
|
|
187
276
|
const requestedModel = normalizeModel(body.model);
|
|
188
277
|
const requestedEffort = normalizeEffort(body.effort);
|
|
278
|
+
const requestedServiceTier = normalizeServiceTier(body.serviceTier);
|
|
189
279
|
const requestedApprovalPolicy = normalizeApprovalPolicy(body.approvalPolicy) ?? 'untrusted';
|
|
190
280
|
const started = await this.ws.request<AppServerStartResponse>('thread/start', {
|
|
191
281
|
model: requestedModel ?? null,
|
|
192
282
|
modelProvider: null,
|
|
193
283
|
cwd: requestedCwd ?? null,
|
|
194
284
|
approvalPolicy: requestedApprovalPolicy,
|
|
195
|
-
sandbox:
|
|
196
|
-
config:
|
|
285
|
+
sandbox: MOBILE_DEFAULT_SANDBOX,
|
|
286
|
+
config: toThreadConfig(requestedServiceTier),
|
|
197
287
|
baseInstructions: null,
|
|
198
288
|
developerInstructions: MOBILE_DEVELOPER_INSTRUCTIONS,
|
|
199
289
|
personality: null,
|
|
@@ -309,7 +399,7 @@ export class HostBridgeApiClient {
|
|
|
309
399
|
model?: string | null;
|
|
310
400
|
approvalPolicy?: ApprovalPolicy | null;
|
|
311
401
|
}
|
|
312
|
-
): Promise<
|
|
402
|
+
): Promise<AppServerThreadRuntimeSettings> {
|
|
313
403
|
const threadId = id.trim();
|
|
314
404
|
if (!threadId) {
|
|
315
405
|
throw new Error('thread id is required');
|
|
@@ -327,7 +417,7 @@ export class HostBridgeApiClient {
|
|
|
327
417
|
modelProvider: null,
|
|
328
418
|
cwd: normalizeCwd(options?.cwd) ?? null,
|
|
329
419
|
approvalPolicy: requestedApprovalPolicy,
|
|
330
|
-
sandbox:
|
|
420
|
+
sandbox: MOBILE_DEFAULT_SANDBOX,
|
|
331
421
|
config: null,
|
|
332
422
|
baseInstructions: null,
|
|
333
423
|
developerInstructions: MOBILE_DEVELOPER_INSTRUCTIONS,
|
|
@@ -337,8 +427,11 @@ export class HostBridgeApiClient {
|
|
|
337
427
|
};
|
|
338
428
|
|
|
339
429
|
try {
|
|
340
|
-
await this.ws.request
|
|
341
|
-
|
|
430
|
+
const response = await this.ws.request<Record<string, unknown>>(
|
|
431
|
+
'thread/resume',
|
|
432
|
+
primaryRequest
|
|
433
|
+
);
|
|
434
|
+
return readThreadRuntimeSettings(response);
|
|
342
435
|
} catch (primaryError) {
|
|
343
436
|
// First fallback: keep raw-event streaming enabled, but relax approval policy.
|
|
344
437
|
const compatibilityRequest = {
|
|
@@ -346,8 +439,11 @@ export class HostBridgeApiClient {
|
|
|
346
439
|
approvalPolicy: fallbackApprovalPolicy,
|
|
347
440
|
};
|
|
348
441
|
try {
|
|
349
|
-
await this.ws.request
|
|
350
|
-
|
|
442
|
+
const response = await this.ws.request<Record<string, unknown>>(
|
|
443
|
+
'thread/resume',
|
|
444
|
+
compatibilityRequest
|
|
445
|
+
);
|
|
446
|
+
return readThreadRuntimeSettings(response);
|
|
351
447
|
} catch (compatibilityError) {
|
|
352
448
|
// Final compatibility fallback for older app-server builds that reject
|
|
353
449
|
// experimentalRawEvents/developerInstructions on resume.
|
|
@@ -357,8 +453,11 @@ export class HostBridgeApiClient {
|
|
|
357
453
|
};
|
|
358
454
|
delete (legacyRequest as { experimentalRawEvents?: boolean }).experimentalRawEvents;
|
|
359
455
|
try {
|
|
360
|
-
await this.ws.request
|
|
361
|
-
|
|
456
|
+
const response = await this.ws.request<Record<string, unknown>>(
|
|
457
|
+
'thread/resume',
|
|
458
|
+
legacyRequest
|
|
459
|
+
);
|
|
460
|
+
return readThreadRuntimeSettings(response);
|
|
362
461
|
} catch (legacyError) {
|
|
363
462
|
throw new Error(
|
|
364
463
|
`thread/resume failed: ${(primaryError as Error).message}; compatibility failed: ${(compatibilityError as Error).message}; legacy fallback failed: ${(legacyError as Error).message}`
|
|
@@ -385,14 +484,25 @@ export class HostBridgeApiClient {
|
|
|
385
484
|
const normalizedCwd = normalizeCwd(body.cwd);
|
|
386
485
|
const normalizedModel = normalizeModel(body.model);
|
|
387
486
|
const normalizedEffort = normalizeEffort(body.effort);
|
|
487
|
+
const normalizedServiceTier = normalizeServiceTier(body.serviceTier);
|
|
388
488
|
const normalizedApprovalPolicy = normalizeApprovalPolicy(body.approvalPolicy);
|
|
389
489
|
const normalizedMentions = normalizeMentions(body.mentions);
|
|
390
490
|
const normalizedLocalImages = normalizeLocalImages(body.localImages);
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
491
|
+
const requestedCollaborationMode = normalizeCollaborationMode(body.collaborationMode);
|
|
492
|
+
let resumedThreadSettings: AppServerThreadRuntimeSettings | null = null;
|
|
493
|
+
|
|
494
|
+
try {
|
|
495
|
+
resumedThreadSettings = await this.resumeThread(id, {
|
|
496
|
+
model: normalizedModel,
|
|
497
|
+
cwd: normalizedCwd,
|
|
498
|
+
approvalPolicy: normalizedApprovalPolicy,
|
|
499
|
+
});
|
|
500
|
+
} catch {
|
|
501
|
+
// Best effort: turn/start still works for recently started chats.
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
let effectiveModel = normalizedModel ?? resumedThreadSettings?.model ?? null;
|
|
505
|
+
if (requestedCollaborationMode && !effectiveModel) {
|
|
396
506
|
try {
|
|
397
507
|
const models = await this.listModels(false);
|
|
398
508
|
effectiveModel =
|
|
@@ -401,22 +511,16 @@ export class HostBridgeApiClient {
|
|
|
401
511
|
// Best effort: fall back to the current thread settings if model lookup fails.
|
|
402
512
|
}
|
|
403
513
|
}
|
|
514
|
+
const effectiveEffort =
|
|
515
|
+
requestedCollaborationMode
|
|
516
|
+
? normalizedEffort ?? resumedThreadSettings?.effort ?? null
|
|
517
|
+
: normalizedEffort;
|
|
404
518
|
const normalizedCollaborationMode = toTurnCollaborationMode(
|
|
405
|
-
|
|
519
|
+
requestedCollaborationMode,
|
|
406
520
|
effectiveModel,
|
|
407
|
-
|
|
521
|
+
effectiveEffort
|
|
408
522
|
);
|
|
409
523
|
|
|
410
|
-
try {
|
|
411
|
-
await this.resumeThread(id, {
|
|
412
|
-
model: effectiveModel,
|
|
413
|
-
cwd: normalizedCwd,
|
|
414
|
-
approvalPolicy: normalizedApprovalPolicy,
|
|
415
|
-
});
|
|
416
|
-
} catch {
|
|
417
|
-
// Best effort: turn/start still works for recently started chats.
|
|
418
|
-
}
|
|
419
|
-
|
|
420
524
|
const turnStart = await this.ws.request<AppServerTurnResponse>('turn/start', {
|
|
421
525
|
threadId: id,
|
|
422
526
|
input: buildTurnInput(content, normalizedMentions, normalizedLocalImages),
|
|
@@ -424,7 +528,8 @@ export class HostBridgeApiClient {
|
|
|
424
528
|
approvalPolicy: normalizedApprovalPolicy ?? null,
|
|
425
529
|
sandboxPolicy: null,
|
|
426
530
|
model: effectiveModel ?? null,
|
|
427
|
-
effort:
|
|
531
|
+
effort: effectiveEffort ?? null,
|
|
532
|
+
serviceTier: normalizedServiceTier ?? null,
|
|
428
533
|
summary: null,
|
|
429
534
|
personality: null,
|
|
430
535
|
outputSchema: null,
|
|
@@ -439,6 +544,28 @@ export class HostBridgeApiClient {
|
|
|
439
544
|
return this.getChatWithUserMessage(id, turnId, content);
|
|
440
545
|
}
|
|
441
546
|
|
|
547
|
+
async steerChatTurn(
|
|
548
|
+
threadId: string,
|
|
549
|
+
expectedTurnId: string,
|
|
550
|
+
body: SteerChatTurnRequest
|
|
551
|
+
): Promise<void> {
|
|
552
|
+
const normalizedThreadId = threadId.trim();
|
|
553
|
+
const normalizedExpectedTurnId = expectedTurnId.trim();
|
|
554
|
+
const content = body.content.trim();
|
|
555
|
+
if (!normalizedThreadId || !normalizedExpectedTurnId || !content) {
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const normalizedMentions = normalizeMentions(body.mentions);
|
|
560
|
+
const normalizedLocalImages = normalizeLocalImages(body.localImages);
|
|
561
|
+
|
|
562
|
+
await this.ws.request<Record<string, never>>('turn/steer', {
|
|
563
|
+
threadId: normalizedThreadId,
|
|
564
|
+
expectedTurnId: normalizedExpectedTurnId,
|
|
565
|
+
input: buildTurnInput(content, normalizedMentions, normalizedLocalImages),
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
442
569
|
async interruptTurn(threadId: string, turnId: string): Promise<void> {
|
|
443
570
|
const normalizedThreadId = threadId.trim();
|
|
444
571
|
const normalizedTurnId = turnId.trim();
|
|
@@ -556,11 +683,13 @@ export class HostBridgeApiClient {
|
|
|
556
683
|
options?: {
|
|
557
684
|
cwd?: string;
|
|
558
685
|
model?: string;
|
|
686
|
+
serviceTier?: ServiceTier;
|
|
559
687
|
approvalPolicy?: ApprovalPolicy | null;
|
|
560
688
|
}
|
|
561
689
|
): Promise<Chat> {
|
|
562
690
|
const requestedApprovalPolicy =
|
|
563
691
|
normalizeApprovalPolicy(options?.approvalPolicy) ?? 'untrusted';
|
|
692
|
+
const requestedServiceTier = normalizeServiceTier(options?.serviceTier);
|
|
564
693
|
const response = await this.ws.request<AppServerForkResponse>('thread/fork', {
|
|
565
694
|
threadId: id,
|
|
566
695
|
path: null,
|
|
@@ -568,8 +697,8 @@ export class HostBridgeApiClient {
|
|
|
568
697
|
modelProvider: null,
|
|
569
698
|
cwd: normalizeCwd(options?.cwd) ?? null,
|
|
570
699
|
approvalPolicy: requestedApprovalPolicy,
|
|
571
|
-
sandbox:
|
|
572
|
-
config:
|
|
700
|
+
sandbox: MOBILE_DEFAULT_SANDBOX,
|
|
701
|
+
config: toThreadConfig(requestedServiceTier),
|
|
573
702
|
baseInstructions: null,
|
|
574
703
|
developerInstructions: MOBILE_DEVELOPER_INSTRUCTIONS,
|
|
575
704
|
persistExtendedHistory: true,
|
|
@@ -582,6 +711,15 @@ export class HostBridgeApiClient {
|
|
|
582
711
|
throw new Error('thread/fork did not return a chat payload');
|
|
583
712
|
}
|
|
584
713
|
|
|
714
|
+
async readServiceTierPreference(): Promise<ServiceTier | null> {
|
|
715
|
+
const response = await this.ws.request<AppServerConfigReadResponse>('config/read', {
|
|
716
|
+
includeLayers: false,
|
|
717
|
+
cwd: null,
|
|
718
|
+
});
|
|
719
|
+
const config = toRecord(response.config);
|
|
720
|
+
return normalizeServiceTier(readString(config?.service_tier));
|
|
721
|
+
}
|
|
722
|
+
|
|
585
723
|
listApprovals(): Promise<PendingApproval[]> {
|
|
586
724
|
return this.ws.request<PendingApproval[]>('bridge/approvals/list');
|
|
587
725
|
}
|
|
@@ -817,6 +955,76 @@ function normalizeCwd(cwd: string | null | undefined): string | null {
|
|
|
817
955
|
return trimmed.length > 0 ? trimmed : null;
|
|
818
956
|
}
|
|
819
957
|
|
|
958
|
+
function readWorkspaceListResponse(value: unknown): WorkspaceListResponse {
|
|
959
|
+
const record = toRecord(value) ?? {};
|
|
960
|
+
const workspacesRaw = Array.isArray(record.workspaces) ? record.workspaces : [];
|
|
961
|
+
|
|
962
|
+
return {
|
|
963
|
+
bridgeRoot: normalizeCwd(readString(record.bridgeRoot)) ?? '',
|
|
964
|
+
allowOutsideRootCwd: record.allowOutsideRootCwd === true,
|
|
965
|
+
workspaces: workspacesRaw
|
|
966
|
+
.map((entry) => {
|
|
967
|
+
const workspace = toRecord(entry);
|
|
968
|
+
if (!workspace) {
|
|
969
|
+
return null;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
const path = normalizeCwd(readString(workspace.path));
|
|
973
|
+
if (!path) {
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const rawChatCount = workspace.chatCount;
|
|
978
|
+
const chatCount =
|
|
979
|
+
typeof rawChatCount === 'number'
|
|
980
|
+
? Math.max(0, Math.trunc(rawChatCount))
|
|
981
|
+
: typeof rawChatCount === 'string'
|
|
982
|
+
? Math.max(0, Number.parseInt(rawChatCount, 10) || 0)
|
|
983
|
+
: 0;
|
|
984
|
+
|
|
985
|
+
return {
|
|
986
|
+
path,
|
|
987
|
+
chatCount,
|
|
988
|
+
};
|
|
989
|
+
})
|
|
990
|
+
.filter((entry): entry is WorkspaceListResponse['workspaces'][number] => entry !== null),
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function readFileSystemListResponse(value: unknown): FileSystemListResponse {
|
|
995
|
+
const record = toRecord(value) ?? {};
|
|
996
|
+
const entriesRaw = Array.isArray(record.entries) ? record.entries : [];
|
|
997
|
+
|
|
998
|
+
return {
|
|
999
|
+
bridgeRoot: normalizeCwd(readString(record.bridgeRoot)) ?? '',
|
|
1000
|
+
path: normalizeCwd(readString(record.path)) ?? '',
|
|
1001
|
+
parentPath: normalizeCwd(readString(record.parentPath)) ?? null,
|
|
1002
|
+
entries: entriesRaw
|
|
1003
|
+
.map((entry) => {
|
|
1004
|
+
const item = toRecord(entry);
|
|
1005
|
+
if (!item) {
|
|
1006
|
+
return null;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
const path = normalizeCwd(readString(item.path));
|
|
1010
|
+
const name = normalizeCwd(readString(item.name));
|
|
1011
|
+
if (!path || !name) {
|
|
1012
|
+
return null;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
return {
|
|
1016
|
+
name,
|
|
1017
|
+
path,
|
|
1018
|
+
kind: readString(item.kind) ?? 'directory',
|
|
1019
|
+
hidden: item.hidden === true,
|
|
1020
|
+
selectable: item.selectable !== false,
|
|
1021
|
+
isGitRepo: item.isGitRepo === true,
|
|
1022
|
+
};
|
|
1023
|
+
})
|
|
1024
|
+
.filter((entry): entry is FileSystemListResponse['entries'][number] => entry !== null),
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
|
|
820
1028
|
function normalizeModel(model: string | null | undefined): string | null {
|
|
821
1029
|
if (typeof model !== 'string') {
|
|
822
1030
|
return null;
|
|
@@ -846,6 +1054,33 @@ function normalizeEffort(effort: string | null | undefined): ReasoningEffort | n
|
|
|
846
1054
|
return null;
|
|
847
1055
|
}
|
|
848
1056
|
|
|
1057
|
+
function normalizeServiceTier(
|
|
1058
|
+
serviceTier: ServiceTier | string | null | undefined
|
|
1059
|
+
): ServiceTier | null {
|
|
1060
|
+
if (typeof serviceTier !== 'string') {
|
|
1061
|
+
return null;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
const normalized = serviceTier.trim().toLowerCase();
|
|
1065
|
+
if (normalized === 'flex' || normalized === 'fast') {
|
|
1066
|
+
return normalized;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
return null;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function toThreadConfig(
|
|
1073
|
+
serviceTier: ServiceTier | null
|
|
1074
|
+
): Record<string, ServiceTier> | null {
|
|
1075
|
+
if (!serviceTier) {
|
|
1076
|
+
return null;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
return {
|
|
1080
|
+
service_tier: serviceTier,
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
|
|
849
1084
|
function normalizeApprovalPolicy(
|
|
850
1085
|
policy: string | null | undefined
|
|
851
1086
|
): ApprovalPolicy | null {
|
|
@@ -988,7 +1223,7 @@ function toTurnCollaborationMode(
|
|
|
988
1223
|
}
|
|
989
1224
|
|
|
990
1225
|
const normalized = value.trim().toLowerCase();
|
|
991
|
-
if (normalized !== 'plan') {
|
|
1226
|
+
if (normalized !== 'plan' && normalized !== 'default') {
|
|
992
1227
|
return null;
|
|
993
1228
|
}
|
|
994
1229
|
|
|
@@ -997,7 +1232,7 @@ function toTurnCollaborationMode(
|
|
|
997
1232
|
}
|
|
998
1233
|
|
|
999
1234
|
return {
|
|
1000
|
-
mode:
|
|
1235
|
+
mode: normalized,
|
|
1001
1236
|
settings: {
|
|
1002
1237
|
model,
|
|
1003
1238
|
reasoning_effort: effort,
|
|
@@ -1006,6 +1241,31 @@ function toTurnCollaborationMode(
|
|
|
1006
1241
|
};
|
|
1007
1242
|
}
|
|
1008
1243
|
|
|
1244
|
+
function normalizeCollaborationMode(
|
|
1245
|
+
value: CollaborationMode | string | null | undefined
|
|
1246
|
+
): CollaborationMode | null {
|
|
1247
|
+
if (typeof value !== 'string') {
|
|
1248
|
+
return null;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
const normalized = value.trim().toLowerCase();
|
|
1252
|
+
if (normalized === 'plan' || normalized === 'default') {
|
|
1253
|
+
return normalized;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
return null;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
function readThreadRuntimeSettings(value: unknown): AppServerThreadRuntimeSettings {
|
|
1260
|
+
const record = toRecord(value);
|
|
1261
|
+
return {
|
|
1262
|
+
model: normalizeModel(readString(record?.model)),
|
|
1263
|
+
effort: normalizeEffort(
|
|
1264
|
+
readString(record?.reasoningEffort) ?? readString(record?.reasoning_effort)
|
|
1265
|
+
),
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1009
1269
|
function toReasoningEffortOptions(raw: unknown): ModelReasoningEffortOption[] {
|
|
1010
1270
|
if (!Array.isArray(raw)) {
|
|
1011
1271
|
return [];
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { readString, toRecord } from './chatMapping';
|
|
2
|
+
import type {
|
|
3
|
+
AccountCreditsSnapshot,
|
|
4
|
+
AccountRateLimitSnapshot,
|
|
5
|
+
AccountRateLimitWindow,
|
|
6
|
+
PlanType,
|
|
7
|
+
} from './types';
|
|
8
|
+
|
|
9
|
+
const PLAN_TYPES = new Set<PlanType>([
|
|
10
|
+
'free',
|
|
11
|
+
'go',
|
|
12
|
+
'plus',
|
|
13
|
+
'pro',
|
|
14
|
+
'team',
|
|
15
|
+
'business',
|
|
16
|
+
'enterprise',
|
|
17
|
+
'edu',
|
|
18
|
+
'unknown',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export function readAccountRateLimits(value: unknown): AccountRateLimitSnapshot | null {
|
|
22
|
+
const record = toRecord(value);
|
|
23
|
+
if (!record) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const byLimitId =
|
|
28
|
+
toRecord(record.rateLimitsByLimitId) ?? toRecord(record.rate_limits_by_limit_id);
|
|
29
|
+
|
|
30
|
+
if (byLimitId) {
|
|
31
|
+
const preferred = readAccountRateLimitSnapshot(byLimitId.codex);
|
|
32
|
+
if (preferred) {
|
|
33
|
+
return preferred;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const candidate of Object.values(byLimitId)) {
|
|
37
|
+
const snapshot = readAccountRateLimitSnapshot(candidate);
|
|
38
|
+
if (snapshot) {
|
|
39
|
+
return snapshot;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return readAccountRateLimitSnapshot(record.rateLimits ?? record.rate_limits);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function readAccountRateLimitSnapshot(
|
|
48
|
+
value: unknown
|
|
49
|
+
): AccountRateLimitSnapshot | null {
|
|
50
|
+
const record = toRecord(value);
|
|
51
|
+
if (!record) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const primary = readAccountRateLimitWindow(record.primary);
|
|
56
|
+
const secondary = readAccountRateLimitWindow(record.secondary);
|
|
57
|
+
if (!primary && !secondary) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
limitId: readString(record.limitId) ?? readString(record.limit_id),
|
|
63
|
+
limitName: readString(record.limitName) ?? readString(record.limit_name),
|
|
64
|
+
primary,
|
|
65
|
+
secondary,
|
|
66
|
+
credits: readAccountCreditsSnapshot(record.credits),
|
|
67
|
+
planType: readPlanType(record.planType ?? record.plan_type),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readAccountRateLimitWindow(value: unknown): AccountRateLimitWindow | null {
|
|
72
|
+
const record = toRecord(value);
|
|
73
|
+
if (!record) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const usedPercent = readNumberLike(record.usedPercent) ?? readNumberLike(record.used_percent);
|
|
78
|
+
if (usedPercent === null) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
usedPercent,
|
|
84
|
+
windowDurationMins:
|
|
85
|
+
readIntegerLike(record.windowDurationMins) ??
|
|
86
|
+
readIntegerLike(record.window_duration_mins),
|
|
87
|
+
resetsAt: readIntegerLike(record.resetsAt) ?? readIntegerLike(record.resets_at),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function readAccountCreditsSnapshot(value: unknown): AccountCreditsSnapshot | null {
|
|
92
|
+
const record = toRecord(value);
|
|
93
|
+
if (!record) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const hasCredits = readBoolean(record.hasCredits) ?? readBoolean(record.has_credits);
|
|
98
|
+
const unlimited = readBoolean(record.unlimited);
|
|
99
|
+
if (hasCredits === null && unlimited === null && !readString(record.balance)) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
hasCredits: hasCredits ?? false,
|
|
105
|
+
unlimited: unlimited ?? false,
|
|
106
|
+
balance: readString(record.balance),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function readPlanType(value: unknown): PlanType | null {
|
|
111
|
+
if (typeof value !== 'string') {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return PLAN_TYPES.has(value as PlanType) ? (value as PlanType) : null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function readBoolean(value: unknown): boolean | null {
|
|
119
|
+
return typeof value === 'boolean' ? value : null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function readNumberLike(value: unknown): number | null {
|
|
123
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const stringValue = readString(value)?.trim();
|
|
128
|
+
if (!stringValue) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const parsed = Number(stringValue);
|
|
133
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function readIntegerLike(value: unknown): number | null {
|
|
137
|
+
const numericValue = readNumberLike(value);
|
|
138
|
+
if (numericValue === null) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return Math.max(0, Math.floor(numericValue));
|
|
143
|
+
}
|