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
|
@@ -260,6 +260,33 @@ describe('HostBridgeWsClient', () => {
|
|
|
260
260
|
await expect(waitPromise).resolves.toBeUndefined();
|
|
261
261
|
});
|
|
262
262
|
|
|
263
|
+
it('waitForTurnCompletion prefers the direct child thread id over parent_thread_id', async () => {
|
|
264
|
+
const client = new HostBridgeWsClient('http://localhost:8787');
|
|
265
|
+
client.connect();
|
|
266
|
+
|
|
267
|
+
const waitPromise = client.waitForTurnCompletion('thr_child', 'turn_child', 100);
|
|
268
|
+
latestMockSocket().simulateMessage(
|
|
269
|
+
JSON.stringify({
|
|
270
|
+
method: 'codex/event/task_complete',
|
|
271
|
+
params: {
|
|
272
|
+
msg: {
|
|
273
|
+
type: 'task_complete',
|
|
274
|
+
thread_id: 'thr_child',
|
|
275
|
+
source: {
|
|
276
|
+
subagent: {
|
|
277
|
+
thread_spawn: {
|
|
278
|
+
parent_thread_id: 'thr_parent',
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
})
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
await expect(waitPromise).resolves.toBeUndefined();
|
|
288
|
+
});
|
|
289
|
+
|
|
263
290
|
it('deduplicates notifications by eventId', () => {
|
|
264
291
|
const client = new HostBridgeWsClient('http://localhost:8787');
|
|
265
292
|
const listener = jest.fn();
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { readString, toRecord } from './chatMapping';
|
|
2
|
+
import type { AccountSnapshot, PlanType } from './types';
|
|
3
|
+
|
|
4
|
+
const PLAN_TYPES = new Set<PlanType>([
|
|
5
|
+
'free',
|
|
6
|
+
'go',
|
|
7
|
+
'plus',
|
|
8
|
+
'pro',
|
|
9
|
+
'team',
|
|
10
|
+
'business',
|
|
11
|
+
'enterprise',
|
|
12
|
+
'edu',
|
|
13
|
+
'unknown',
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
export function readAccountSnapshot(value: unknown): AccountSnapshot {
|
|
17
|
+
const record = toRecord(value);
|
|
18
|
+
const accountRecord = toRecord(record?.account);
|
|
19
|
+
const accountType = readAccountType(accountRecord?.type);
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
type: accountType,
|
|
23
|
+
email: accountType === 'chatgpt' ? readString(accountRecord?.email) : null,
|
|
24
|
+
planType:
|
|
25
|
+
accountType === 'chatgpt'
|
|
26
|
+
? readPlanType(accountRecord?.planType ?? accountRecord?.plan_type)
|
|
27
|
+
: null,
|
|
28
|
+
requiresOpenaiAuth:
|
|
29
|
+
record?.requiresOpenaiAuth === true || record?.requires_openai_auth === true,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readAccountType(value: unknown): AccountSnapshot['type'] {
|
|
34
|
+
if (value === 'apiKey' || value === 'chatgpt') {
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readPlanType(value: unknown): PlanType | null {
|
|
42
|
+
if (typeof value !== 'string') {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return PLAN_TYPES.has(value as PlanType) ? (value as PlanType) : null;
|
|
47
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
Chat,
|
|
3
3
|
ChatMessage,
|
|
4
|
+
ChatMessageSubAgentMeta,
|
|
5
|
+
ChatPlanSnapshot,
|
|
4
6
|
ChatStatus,
|
|
5
7
|
ChatSummary,
|
|
8
|
+
TurnPlanStep,
|
|
6
9
|
} from './types';
|
|
7
10
|
|
|
8
11
|
export type RawThreadStatus =
|
|
@@ -43,6 +46,8 @@ export interface RawThread {
|
|
|
43
46
|
title?: string;
|
|
44
47
|
preview?: string;
|
|
45
48
|
modelProvider?: string;
|
|
49
|
+
agentNickname?: string;
|
|
50
|
+
agentRole?: string;
|
|
46
51
|
createdAt?: number;
|
|
47
52
|
updatedAt?: number;
|
|
48
53
|
status?: RawThreadStatus;
|
|
@@ -51,6 +56,12 @@ export interface RawThread {
|
|
|
51
56
|
turns?: RawTurn[];
|
|
52
57
|
}
|
|
53
58
|
|
|
59
|
+
interface ThreadSourceMetadata {
|
|
60
|
+
kind?: string;
|
|
61
|
+
parentThreadId?: string;
|
|
62
|
+
subAgentDepth?: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
54
65
|
export function toRecord(value: unknown): Record<string, unknown> | null {
|
|
55
66
|
return typeof value === 'object' && value !== null
|
|
56
67
|
? (value as Record<string, unknown>)
|
|
@@ -61,6 +72,16 @@ export function readString(value: unknown): string | null {
|
|
|
61
72
|
return typeof value === 'string' ? value : null;
|
|
62
73
|
}
|
|
63
74
|
|
|
75
|
+
function readStringArray(value: unknown): string[] {
|
|
76
|
+
if (!Array.isArray(value)) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return value
|
|
81
|
+
.map((entry) => readString(entry)?.trim() ?? '')
|
|
82
|
+
.filter((entry): entry is string => entry.length > 0);
|
|
83
|
+
}
|
|
84
|
+
|
|
64
85
|
function readNumber(value: unknown): number | null {
|
|
65
86
|
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
66
87
|
}
|
|
@@ -197,6 +218,14 @@ export function toRawThread(value: unknown): RawThread {
|
|
|
197
218
|
title: threadName,
|
|
198
219
|
preview: readString(record.preview) ?? undefined,
|
|
199
220
|
modelProvider: readString(record.modelProvider) ?? undefined,
|
|
221
|
+
agentNickname:
|
|
222
|
+
readString(record.agentNickname) ??
|
|
223
|
+
readString(record.agent_nickname) ??
|
|
224
|
+
undefined,
|
|
225
|
+
agentRole:
|
|
226
|
+
readString(record.agentRole) ??
|
|
227
|
+
readString(record.agent_role) ??
|
|
228
|
+
undefined,
|
|
200
229
|
createdAt: readNumber(record.createdAt) ?? undefined,
|
|
201
230
|
updatedAt: readNumber(record.updatedAt) ?? undefined,
|
|
202
231
|
status: (record.status as RawThreadStatus) ?? undefined,
|
|
@@ -236,6 +265,7 @@ export function mapChatSummary(raw: RawThread): ChatSummary | null {
|
|
|
236
265
|
const createdAt = unixSecondsToIso(raw.createdAt);
|
|
237
266
|
const updatedAt = unixSecondsToIso(raw.updatedAt);
|
|
238
267
|
const turns = Array.isArray(raw.turns) ? raw.turns : [];
|
|
268
|
+
const sourceMetadata = readThreadSourceMetadata(raw.source);
|
|
239
269
|
|
|
240
270
|
const lastError = extractLastError(turns);
|
|
241
271
|
const displayTitle = raw.name ?? raw.preview;
|
|
@@ -250,59 +280,125 @@ export function mapChatSummary(raw: RawThread): ChatSummary | null {
|
|
|
250
280
|
lastMessagePreview: toPreview(raw.preview || ''),
|
|
251
281
|
cwd: readString(raw.cwd) ?? undefined,
|
|
252
282
|
modelProvider: readString(raw.modelProvider) ?? undefined,
|
|
253
|
-
|
|
283
|
+
agentNickname: readString(raw.agentNickname) ?? undefined,
|
|
284
|
+
agentRole: readString(raw.agentRole) ?? undefined,
|
|
285
|
+
sourceKind: sourceMetadata.kind,
|
|
286
|
+
parentThreadId: sourceMetadata.parentThreadId,
|
|
287
|
+
subAgentDepth: sourceMetadata.subAgentDepth,
|
|
254
288
|
lastError: lastError ?? undefined,
|
|
255
289
|
};
|
|
256
290
|
}
|
|
257
291
|
|
|
258
|
-
function
|
|
292
|
+
function readThreadSourceMetadata(source: unknown): ThreadSourceMetadata {
|
|
259
293
|
if (typeof source === 'string') {
|
|
260
|
-
return
|
|
294
|
+
return {
|
|
295
|
+
kind: source,
|
|
296
|
+
};
|
|
261
297
|
}
|
|
262
298
|
|
|
263
299
|
const sourceRecord = toRecord(source);
|
|
264
300
|
if (!sourceRecord) {
|
|
265
|
-
return
|
|
301
|
+
return {};
|
|
266
302
|
}
|
|
267
303
|
|
|
268
304
|
// Legacy shape used by older adapters.
|
|
269
305
|
const legacyKind = readString(sourceRecord.kind);
|
|
270
306
|
if (legacyKind) {
|
|
271
|
-
return
|
|
307
|
+
return {
|
|
308
|
+
kind: legacyKind,
|
|
309
|
+
parentThreadId:
|
|
310
|
+
readString(sourceRecord.parentThreadId) ??
|
|
311
|
+
readString(sourceRecord.parent_thread_id) ??
|
|
312
|
+
undefined,
|
|
313
|
+
subAgentDepth:
|
|
314
|
+
readNumber(sourceRecord.depth) ??
|
|
315
|
+
readNumber(sourceRecord.agentDepth) ??
|
|
316
|
+
readNumber(sourceRecord.agent_depth) ??
|
|
317
|
+
undefined,
|
|
318
|
+
};
|
|
272
319
|
}
|
|
273
320
|
|
|
274
321
|
// Current app-server shape: { subAgent: ... } tagged union.
|
|
275
|
-
|
|
276
|
-
|
|
322
|
+
const subAgentValue =
|
|
323
|
+
sourceRecord.subAgent ??
|
|
324
|
+
sourceRecord.subagent;
|
|
325
|
+
|
|
326
|
+
if (subAgentValue !== undefined) {
|
|
327
|
+
const subAgent = subAgentValue;
|
|
277
328
|
if (typeof subAgent === 'string') {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
329
|
+
const kind =
|
|
330
|
+
subAgent === 'review'
|
|
331
|
+
? 'subAgentReview'
|
|
332
|
+
: subAgent === 'compact'
|
|
333
|
+
? 'subAgentCompact'
|
|
334
|
+
: subAgent === 'memory_consolidation'
|
|
335
|
+
? 'subAgentOther'
|
|
336
|
+
: 'subAgent';
|
|
337
|
+
return {
|
|
338
|
+
kind,
|
|
339
|
+
};
|
|
282
340
|
}
|
|
283
341
|
|
|
284
342
|
const subAgentRecord = toRecord(subAgent);
|
|
285
343
|
if (!subAgentRecord) {
|
|
286
|
-
return
|
|
344
|
+
return {
|
|
345
|
+
kind: 'subAgent',
|
|
346
|
+
};
|
|
287
347
|
}
|
|
288
348
|
|
|
289
|
-
|
|
290
|
-
|
|
349
|
+
const threadSpawn = toRecord(subAgentRecord.thread_spawn);
|
|
350
|
+
if (threadSpawn) {
|
|
351
|
+
return {
|
|
352
|
+
kind: 'subAgentThreadSpawn',
|
|
353
|
+
parentThreadId:
|
|
354
|
+
readString(threadSpawn.parentThreadId) ??
|
|
355
|
+
readString(threadSpawn.parent_thread_id) ??
|
|
356
|
+
undefined,
|
|
357
|
+
subAgentDepth:
|
|
358
|
+
readNumber(threadSpawn.depth) ??
|
|
359
|
+
readNumber(threadSpawn.agentDepth) ??
|
|
360
|
+
readNumber(threadSpawn.agent_depth) ??
|
|
361
|
+
undefined,
|
|
362
|
+
};
|
|
291
363
|
}
|
|
292
364
|
|
|
293
365
|
if (readString(subAgentRecord.other)) {
|
|
294
|
-
return
|
|
366
|
+
return {
|
|
367
|
+
kind: 'subAgentOther',
|
|
368
|
+
};
|
|
295
369
|
}
|
|
296
370
|
|
|
297
|
-
return
|
|
371
|
+
return {
|
|
372
|
+
kind: 'subAgent',
|
|
373
|
+
parentThreadId:
|
|
374
|
+
readString(subAgentRecord.parentThreadId) ??
|
|
375
|
+
readString(subAgentRecord.parent_thread_id) ??
|
|
376
|
+
undefined,
|
|
377
|
+
subAgentDepth:
|
|
378
|
+
readNumber(subAgentRecord.depth) ??
|
|
379
|
+
readNumber(subAgentRecord.agentDepth) ??
|
|
380
|
+
readNumber(subAgentRecord.agent_depth) ??
|
|
381
|
+
undefined,
|
|
382
|
+
};
|
|
298
383
|
}
|
|
299
384
|
|
|
300
385
|
const typeKind = readString(sourceRecord.type);
|
|
301
386
|
if (typeKind && typeKind.startsWith('subAgent')) {
|
|
302
|
-
return
|
|
387
|
+
return {
|
|
388
|
+
kind: typeKind,
|
|
389
|
+
parentThreadId:
|
|
390
|
+
readString(sourceRecord.parentThreadId) ??
|
|
391
|
+
readString(sourceRecord.parent_thread_id) ??
|
|
392
|
+
undefined,
|
|
393
|
+
subAgentDepth:
|
|
394
|
+
readNumber(sourceRecord.depth) ??
|
|
395
|
+
readNumber(sourceRecord.agentDepth) ??
|
|
396
|
+
readNumber(sourceRecord.agent_depth) ??
|
|
397
|
+
undefined,
|
|
398
|
+
};
|
|
303
399
|
}
|
|
304
400
|
|
|
305
|
-
return
|
|
401
|
+
return {};
|
|
306
402
|
}
|
|
307
403
|
|
|
308
404
|
export function mapChat(raw: RawThread): Chat {
|
|
@@ -312,6 +408,7 @@ export function mapChat(raw: RawThread): Chat {
|
|
|
312
408
|
}
|
|
313
409
|
|
|
314
410
|
const messages = mapMessages(raw, summary.createdAt);
|
|
411
|
+
const plans = extractChatPlans(raw);
|
|
315
412
|
|
|
316
413
|
const lastPreview =
|
|
317
414
|
messages.length > 0
|
|
@@ -322,6 +419,67 @@ export function mapChat(raw: RawThread): Chat {
|
|
|
322
419
|
...summary,
|
|
323
420
|
lastMessagePreview: lastPreview,
|
|
324
421
|
messages,
|
|
422
|
+
latestPlan: plans.latestPlan,
|
|
423
|
+
latestTurnPlan: plans.latestTurnPlan,
|
|
424
|
+
latestTurnStatus: plans.latestTurnStatus,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function extractChatPlans(raw: RawThread): {
|
|
429
|
+
latestPlan: ChatPlanSnapshot | null;
|
|
430
|
+
latestTurnPlan: ChatPlanSnapshot | null;
|
|
431
|
+
latestTurnStatus: string | null;
|
|
432
|
+
} {
|
|
433
|
+
const threadId = raw.id?.trim();
|
|
434
|
+
const turns = Array.isArray(raw.turns) ? raw.turns : [];
|
|
435
|
+
const latestTurn = turns.length > 0 ? turns[turns.length - 1] : null;
|
|
436
|
+
const latestTurnStatus = readString(latestTurn?.status);
|
|
437
|
+
|
|
438
|
+
if (!threadId || turns.length === 0) {
|
|
439
|
+
return {
|
|
440
|
+
latestPlan: null,
|
|
441
|
+
latestTurnPlan: null,
|
|
442
|
+
latestTurnStatus,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
let latestPlan: ChatPlanSnapshot | null = null;
|
|
447
|
+
let latestTurnPlan: ChatPlanSnapshot | null = null;
|
|
448
|
+
|
|
449
|
+
for (const turn of turns) {
|
|
450
|
+
const turnId = readString(turn.id);
|
|
451
|
+
const items = Array.isArray(turn.items) ? turn.items : [];
|
|
452
|
+
let latestPlanInTurn: ChatPlanSnapshot | null = null;
|
|
453
|
+
|
|
454
|
+
for (const item of items) {
|
|
455
|
+
const itemRecord = toRecord(item);
|
|
456
|
+
if (!itemRecord) {
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const itemType = normalizeType(readString(itemRecord.type) ?? '');
|
|
461
|
+
if (itemType !== 'plan') {
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const plan = toPlanSnapshot(itemRecord, threadId, turnId);
|
|
466
|
+
if (!plan) {
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
latestPlan = plan;
|
|
471
|
+
latestPlanInTurn = plan;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (turn === latestTurn) {
|
|
475
|
+
latestTurnPlan = latestPlanInTurn;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return {
|
|
480
|
+
latestPlan,
|
|
481
|
+
latestTurnPlan,
|
|
482
|
+
latestTurnStatus,
|
|
325
483
|
};
|
|
326
484
|
}
|
|
327
485
|
|
|
@@ -406,10 +564,13 @@ function mapMessages(raw: RawThread, fallbackCreatedAt: string): ChatMessage[] {
|
|
|
406
564
|
|
|
407
565
|
const toolLikeMessage = toToolLikeMessage(itemRecord);
|
|
408
566
|
if (toolLikeMessage) {
|
|
567
|
+
const systemKind = itemType === 'collabToolCall' ? 'subAgent' : 'tool';
|
|
409
568
|
messages.push({
|
|
410
569
|
id: readString(itemRecord.id) ?? generateLocalId(),
|
|
411
570
|
role: 'system',
|
|
412
571
|
content: toolLikeMessage,
|
|
572
|
+
systemKind,
|
|
573
|
+
subAgentMeta: systemKind === 'subAgent' ? toSubAgentMeta(itemRecord) : undefined,
|
|
413
574
|
createdAt: new Date(baseTs + messages.length * 1000).toISOString(),
|
|
414
575
|
});
|
|
415
576
|
}
|
|
@@ -423,6 +584,147 @@ function generateLocalId(): string {
|
|
|
423
584
|
return `msg-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
424
585
|
}
|
|
425
586
|
|
|
587
|
+
function toPlanSnapshot(
|
|
588
|
+
item: Record<string, unknown>,
|
|
589
|
+
threadId: string,
|
|
590
|
+
fallbackTurnId?: string | null
|
|
591
|
+
): ChatPlanSnapshot | null {
|
|
592
|
+
const turnId =
|
|
593
|
+
readString(item.turnId) ??
|
|
594
|
+
readString(item.turn_id) ??
|
|
595
|
+
fallbackTurnId ??
|
|
596
|
+
readString(item.id);
|
|
597
|
+
if (!turnId) {
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const rawSteps = Array.isArray(item.plan)
|
|
602
|
+
? item.plan
|
|
603
|
+
: Array.isArray(item.steps)
|
|
604
|
+
? item.steps
|
|
605
|
+
: [];
|
|
606
|
+
const steps: TurnPlanStep[] = rawSteps
|
|
607
|
+
.map((entry) => {
|
|
608
|
+
const entryRecord = toRecord(entry);
|
|
609
|
+
if (!entryRecord) {
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const step = readString(entryRecord.step);
|
|
614
|
+
const status = normalizePlanStepStatus(readString(entryRecord.status));
|
|
615
|
+
if (!step || !status) {
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return {
|
|
620
|
+
step,
|
|
621
|
+
status,
|
|
622
|
+
} satisfies TurnPlanStep;
|
|
623
|
+
})
|
|
624
|
+
.filter((entry): entry is TurnPlanStep => entry !== null);
|
|
625
|
+
const explanation = readString(item.explanation);
|
|
626
|
+
|
|
627
|
+
if (steps.length === 0 && !explanation?.trim()) {
|
|
628
|
+
return parsePlanTextSnapshot(readString(item.text), threadId, turnId);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
return {
|
|
632
|
+
threadId,
|
|
633
|
+
turnId,
|
|
634
|
+
explanation,
|
|
635
|
+
steps,
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function parsePlanTextSnapshot(
|
|
640
|
+
text: string | null | undefined,
|
|
641
|
+
threadId: string,
|
|
642
|
+
turnId: string
|
|
643
|
+
): ChatPlanSnapshot | null {
|
|
644
|
+
const trimmed = text?.trim();
|
|
645
|
+
if (!trimmed) {
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const lines = trimmed
|
|
650
|
+
.split(/\r?\n/)
|
|
651
|
+
.map((line) => line.trim())
|
|
652
|
+
.filter((line) => line.length > 0);
|
|
653
|
+
if (lines.length === 0) {
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const hasSummaryHeader = lines.some((line) => /^summary$/i.test(line));
|
|
658
|
+
const steps: TurnPlanStep[] = [];
|
|
659
|
+
for (const line of lines) {
|
|
660
|
+
const match = line.match(/^\d+[.)]\s+(.+)$/);
|
|
661
|
+
if (!match?.[1]) {
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
steps.push({
|
|
666
|
+
step: match[1].trim(),
|
|
667
|
+
status: 'pending',
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (!hasSummaryHeader && steps.length === 0) {
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
let startIndex = 0;
|
|
676
|
+
if (lines.length > 1 && /plan$/i.test(lines[0])) {
|
|
677
|
+
startIndex = 1;
|
|
678
|
+
}
|
|
679
|
+
if (lines[startIndex] && /^summary$/i.test(lines[startIndex])) {
|
|
680
|
+
startIndex += 1;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const explanationLines: string[] = [];
|
|
684
|
+
for (let index = startIndex; index < lines.length; index += 1) {
|
|
685
|
+
const line = lines[index];
|
|
686
|
+
if (/^\d+[.)]\s+/.test(line)) {
|
|
687
|
+
break;
|
|
688
|
+
}
|
|
689
|
+
if (/^(summary|implementation plan|proposed plan)$/i.test(line)) {
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
explanationLines.push(line);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const explanation =
|
|
696
|
+
explanationLines.length > 0 ? explanationLines.join(' ').trim() : null;
|
|
697
|
+
|
|
698
|
+
if (steps.length === 0 && !explanation) {
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
return {
|
|
703
|
+
threadId,
|
|
704
|
+
turnId,
|
|
705
|
+
explanation,
|
|
706
|
+
steps,
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function normalizePlanStepStatus(value: string | null | undefined): TurnPlanStep['status'] | null {
|
|
711
|
+
if (!value) {
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const normalized = value.trim().toLowerCase().replace(/[^a-z]/g, '');
|
|
716
|
+
if (normalized === 'pending') {
|
|
717
|
+
return 'pending';
|
|
718
|
+
}
|
|
719
|
+
if (normalized === 'inprogress') {
|
|
720
|
+
return 'inProgress';
|
|
721
|
+
}
|
|
722
|
+
if (normalized === 'completed' || normalized === 'complete') {
|
|
723
|
+
return 'completed';
|
|
724
|
+
}
|
|
725
|
+
return null;
|
|
726
|
+
}
|
|
727
|
+
|
|
426
728
|
function toToolLikeMessage(item: Record<string, unknown>): string | null {
|
|
427
729
|
const rawType = readString(item.type);
|
|
428
730
|
if (!rawType) {
|
|
@@ -473,6 +775,72 @@ function toToolLikeMessage(item: Record<string, unknown>): string | null {
|
|
|
473
775
|
return withNestedDetail(title, detail);
|
|
474
776
|
}
|
|
475
777
|
|
|
778
|
+
if (type === 'collabtoolcall') {
|
|
779
|
+
const tool = normalizeType(readString(item.tool) ?? '');
|
|
780
|
+
const status = normalizeType(readString(item.status) ?? '');
|
|
781
|
+
const prompt = normalizeInline(readString(item.prompt), 220);
|
|
782
|
+
const receiverThreadIds = readReceiverThreadIds(item);
|
|
783
|
+
const primaryReceiverThreadId = normalizeInline(receiverThreadIds[0], 120);
|
|
784
|
+
const newThreadId = normalizeInline(
|
|
785
|
+
readString(item.newThreadId) ??
|
|
786
|
+
readString(item.new_thread_id) ??
|
|
787
|
+
primaryReceiverThreadId,
|
|
788
|
+
120
|
|
789
|
+
);
|
|
790
|
+
const senderThreadId = normalizeInline(
|
|
791
|
+
readString(item.senderThreadId) ?? readString(item.sender_thread_id),
|
|
792
|
+
120
|
|
793
|
+
);
|
|
794
|
+
const agentStatus = normalizeInline(
|
|
795
|
+
readString(item.agentStatus) ?? readString(item.agent_status),
|
|
796
|
+
120
|
|
797
|
+
);
|
|
798
|
+
|
|
799
|
+
const title = (() => {
|
|
800
|
+
if (tool === 'spawnagent') {
|
|
801
|
+
if (status === 'failed' || status === 'error') {
|
|
802
|
+
return '• Sub-agent spawn failed';
|
|
803
|
+
}
|
|
804
|
+
if (status === 'completed' || status === 'complete' || status === 'succeeded') {
|
|
805
|
+
return '• Spawned sub-agent';
|
|
806
|
+
}
|
|
807
|
+
return '• Spawning sub-agent';
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (tool === 'sendinput') {
|
|
811
|
+
return status === 'failed' || status === 'error'
|
|
812
|
+
? '• Sub-agent update failed'
|
|
813
|
+
: '• Sent follow-up to sub-agent';
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (tool === 'wait') {
|
|
817
|
+
return status === 'failed' || status === 'error'
|
|
818
|
+
? '• Waiting on sub-agent failed'
|
|
819
|
+
: '• Waiting on sub-agent';
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (tool === 'closeagent') {
|
|
823
|
+
return status === 'failed' || status === 'error'
|
|
824
|
+
? '• Closing sub-agent failed'
|
|
825
|
+
: '• Closed sub-agent thread';
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
return status === 'failed' || status === 'error'
|
|
829
|
+
? '• Sub-agent action failed'
|
|
830
|
+
: '• Updated sub-agent thread';
|
|
831
|
+
})();
|
|
832
|
+
|
|
833
|
+
const detailParts = [
|
|
834
|
+
prompt ? `Prompt: ${prompt}` : null,
|
|
835
|
+
newThreadId ? `Thread: ${newThreadId}` : null,
|
|
836
|
+
primaryReceiverThreadId ? `Target: ${primaryReceiverThreadId}` : null,
|
|
837
|
+
senderThreadId ? `From: ${senderThreadId}` : null,
|
|
838
|
+
agentStatus ? `Status: ${agentStatus}` : null,
|
|
839
|
+
].filter(Boolean);
|
|
840
|
+
|
|
841
|
+
return withNestedDetail(title, detailParts.join('\n') || null);
|
|
842
|
+
}
|
|
843
|
+
|
|
476
844
|
if (type === 'websearch') {
|
|
477
845
|
const query = normalizeInline(readString(item.query), 180);
|
|
478
846
|
const actionRecord = toRecord(item.action);
|
|
@@ -528,6 +896,55 @@ function toToolLikeMessage(item: Record<string, unknown>): string | null {
|
|
|
528
896
|
return null;
|
|
529
897
|
}
|
|
530
898
|
|
|
899
|
+
function toSubAgentMeta(item: Record<string, unknown>): ChatMessageSubAgentMeta | undefined {
|
|
900
|
+
const tool = readString(item.tool) ?? undefined;
|
|
901
|
+
const prompt = normalizeInline(readString(item.prompt), 4000) ?? undefined;
|
|
902
|
+
const senderThreadId =
|
|
903
|
+
normalizeInline(
|
|
904
|
+
readString(item.senderThreadId) ?? readString(item.sender_thread_id),
|
|
905
|
+
200
|
|
906
|
+
) ?? undefined;
|
|
907
|
+
const agentStatus =
|
|
908
|
+
normalizeInline(
|
|
909
|
+
readString(item.agentStatus) ?? readString(item.agent_status),
|
|
910
|
+
200
|
|
911
|
+
) ?? undefined;
|
|
912
|
+
const receiverThreadIds = readReceiverThreadIds(item);
|
|
913
|
+
|
|
914
|
+
if (!tool && !prompt && !senderThreadId && receiverThreadIds.length === 0 && !agentStatus) {
|
|
915
|
+
return undefined;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
return {
|
|
919
|
+
tool,
|
|
920
|
+
prompt,
|
|
921
|
+
senderThreadId,
|
|
922
|
+
receiverThreadIds,
|
|
923
|
+
agentStatus,
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function readReceiverThreadIds(item: Record<string, unknown>): string[] {
|
|
928
|
+
const pluralIds = [
|
|
929
|
+
...readStringArray(item.receiverThreadIds),
|
|
930
|
+
...readStringArray(item.receiver_thread_ids),
|
|
931
|
+
];
|
|
932
|
+
if (pluralIds.length > 0) {
|
|
933
|
+
return Array.from(new Set(pluralIds));
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const singularIds = [
|
|
937
|
+
readString(item.newThreadId),
|
|
938
|
+
readString(item.new_thread_id),
|
|
939
|
+
readString(item.receiverThreadId),
|
|
940
|
+
readString(item.receiver_thread_id),
|
|
941
|
+
]
|
|
942
|
+
.map((value) => value?.trim() ?? '')
|
|
943
|
+
.filter((value): value is string => value.length > 0);
|
|
944
|
+
|
|
945
|
+
return singularIds;
|
|
946
|
+
}
|
|
947
|
+
|
|
531
948
|
function normalizeType(value: string): string {
|
|
532
949
|
return value.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
|
|
533
950
|
}
|