glad-web 1.0.25 → 1.0.26
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/lib/codex/structured-session.js +144 -43
- package/lib/web/index.html +80 -13
- package/package.json +1 -1
|
@@ -63,6 +63,22 @@ function textFromInputItems(content) {
|
|
|
63
63
|
.join('\n');
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
function recentUserQuestions(thread, limit = 2) {
|
|
67
|
+
const questions = [];
|
|
68
|
+
const turns = Array.isArray(thread?.turns) ? thread.turns : [];
|
|
69
|
+
for (let turnIndex = turns.length - 1; turnIndex >= 0 && questions.length < limit; turnIndex -= 1) {
|
|
70
|
+
const items = Array.isArray(turns[turnIndex]?.items) ? turns[turnIndex].items : [];
|
|
71
|
+
for (let itemIndex = items.length - 1; itemIndex >= 0 && questions.length < limit; itemIndex -= 1) {
|
|
72
|
+
const item = items[itemIndex];
|
|
73
|
+
if (item?.type !== 'userMessage') continue;
|
|
74
|
+
const text = (textFromInputItems(item.content) || item.text || '').trim();
|
|
75
|
+
if (text) questions.push(text);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
while (questions.length < limit) questions.push('');
|
|
79
|
+
return questions;
|
|
80
|
+
}
|
|
81
|
+
|
|
66
82
|
function toolDetails(raw) {
|
|
67
83
|
if (raw.type === 'commandExecution') {
|
|
68
84
|
return {
|
|
@@ -98,13 +114,22 @@ function toolDetails(raw) {
|
|
|
98
114
|
};
|
|
99
115
|
}
|
|
100
116
|
if (raw.type === 'collabAgentToolCall') {
|
|
117
|
+
const receiverThreadIds = Array.isArray(raw.receiverThreadIds) ? raw.receiverThreadIds.filter(Boolean) : [];
|
|
118
|
+
const input = raw.arguments || raw.input || {
|
|
119
|
+
...(raw.prompt ? { prompt: raw.prompt } : {}),
|
|
120
|
+
...(receiverThreadIds.length ? { receiverThreadIds } : {}),
|
|
121
|
+
...(raw.agentsStates && Object.keys(raw.agentsStates).length ? { agentsStates: raw.agentsStates } : {})
|
|
122
|
+
};
|
|
101
123
|
return {
|
|
102
124
|
name: 'Agent',
|
|
103
125
|
title: raw.tool || raw.action || 'Subagent',
|
|
104
|
-
|
|
105
|
-
|
|
126
|
+
tool: raw.tool || raw.action || 'subagent',
|
|
127
|
+
input,
|
|
128
|
+
result: raw.error != null ? String(raw.error) : raw.result == null ? '' : safeJson(raw.result),
|
|
106
129
|
error: raw.error != null ? String(raw.error) : null,
|
|
107
|
-
subagentId: raw.receiverThreadId ||
|
|
130
|
+
subagentId: raw.receiverThreadId || receiverThreadIds[0] || raw.agentId || null,
|
|
131
|
+
subagentIds: receiverThreadIds,
|
|
132
|
+
agentsStates: raw.agentsStates || {}
|
|
108
133
|
};
|
|
109
134
|
}
|
|
110
135
|
return {
|
|
@@ -135,6 +160,7 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
135
160
|
this.threadId = options.resume || null;
|
|
136
161
|
this.currentTurnId = null;
|
|
137
162
|
this.currentTurnStartedAt = null;
|
|
163
|
+
this.threadTurns = new Map();
|
|
138
164
|
this.tokenUsage = null;
|
|
139
165
|
this.permissionMode = normalizePermissionMode(options.permissionMode);
|
|
140
166
|
this.sandboxMode = normalizeSandboxMode(options.sandboxMode);
|
|
@@ -186,6 +212,8 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
186
212
|
}
|
|
187
213
|
|
|
188
214
|
getControlState() {
|
|
215
|
+
const activeSubagentCount = Array.from(this.threadTurns.entries())
|
|
216
|
+
.filter(([threadId, turn]) => threadId !== this.threadId && turn?.status === 'running').length;
|
|
189
217
|
return { permissionMode: this.permissionMode || 'default', sandboxMode: this.sandboxMode || 'default',
|
|
190
218
|
effectivePermissionMode: this.effectivePermissionMode, effectiveSandboxMode: this.effectiveSandboxMode,
|
|
191
219
|
model: this.model, effort: this.effort,
|
|
@@ -193,7 +221,7 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
193
221
|
canAbort: this.presentation === 'structured' && this.status !== 'idle',
|
|
194
222
|
canSwitchToTerminal: this.presentation === 'structured' && this.status === 'idle' && Boolean(this.threadId),
|
|
195
223
|
canSwitchToStructured: this.presentation === 'terminal',
|
|
196
|
-
pendingPermissionCount: this.pendingPermissions.size, models: this.models };
|
|
224
|
+
pendingPermissionCount: this.pendingPermissions.size, activeSubagentCount, models: this.models };
|
|
197
225
|
}
|
|
198
226
|
|
|
199
227
|
getHistory() {
|
|
@@ -225,6 +253,7 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
225
253
|
}
|
|
226
254
|
emitEvent(event) { this.emit('event', event); }
|
|
227
255
|
setStatus(status) { if (this.status !== status) { this.status = status; this.emitEvent({ type: 'state', state: this.getControlState() }); } }
|
|
256
|
+
emitControlState() { this.emitEvent({ type: 'state', state: this.getControlState() }); }
|
|
228
257
|
recordPermission(request, status, decision) {
|
|
229
258
|
const completed = { ...request, status, decision };
|
|
230
259
|
this.completedPermissions = [...this.completedPermissions.filter(item => item.id !== request.id), completed].slice(-50);
|
|
@@ -349,44 +378,75 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
349
378
|
return;
|
|
350
379
|
}
|
|
351
380
|
if (method === 'turn/started') {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
381
|
+
const threadId = params.threadId || this.threadId;
|
|
382
|
+
const turnId = params.turn?.id || params.turnId || null;
|
|
383
|
+
const startedAt = Number(params.turn?.startedAt || 0);
|
|
384
|
+
const startedAtMs = startedAt > 0 && startedAt < 100000000000 ? startedAt * 1000 : startedAt || Date.now();
|
|
385
|
+
if (threadId && turnId) this.threadTurns.set(threadId, { turnId, startedAt: startedAtMs, status: 'running' });
|
|
386
|
+
if (!threadId || threadId === this.threadId) {
|
|
387
|
+
this.currentTurnId = turnId || this.currentTurnId;
|
|
388
|
+
this.currentTurnStartedAt = startedAtMs;
|
|
389
|
+
this.setStatus('running');
|
|
390
|
+
} else {
|
|
391
|
+
this.emitControlState();
|
|
392
|
+
}
|
|
393
|
+
this.append({ kind: 'turn-start', threadId, turnId, createdAt: startedAtMs });
|
|
356
394
|
return;
|
|
357
395
|
}
|
|
358
396
|
if (method === 'turn/completed') {
|
|
359
|
-
const
|
|
397
|
+
const threadId = params.threadId || this.threadId;
|
|
398
|
+
const trackedTurn = threadId ? this.threadTurns.get(threadId) : null;
|
|
399
|
+
const completedTurnId = params.turn?.id || params.turnId || trackedTurn?.turnId || this.currentTurnId;
|
|
360
400
|
const turnStatus = params.turn?.status === 'failed' || params.turn?.error ? 'failed'
|
|
361
401
|
: params.turn?.status === 'interrupted' ? 'cancelled' : 'completed';
|
|
362
|
-
|
|
363
|
-
|
|
402
|
+
const completedAt = Number(params.turn?.completedAt || 0);
|
|
403
|
+
const completedAtMs = completedAt > 0 && completedAt < 100000000000 ? completedAt * 1000 : completedAt || Date.now();
|
|
404
|
+
const startedAtMs = trackedTurn?.startedAt || ((!threadId || threadId === this.threadId) ? this.currentTurnStartedAt : null);
|
|
405
|
+
const durationMs = Number(params.turn?.durationMs || 0)
|
|
406
|
+
|| (startedAtMs ? Math.max(0, completedAtMs - startedAtMs) : null);
|
|
407
|
+
this.append({ kind: 'turn-end', threadId, turnId: completedTurnId, status: turnStatus,
|
|
408
|
+
durationMs, createdAt: completedAtMs });
|
|
409
|
+
const observedNow = Date.now();
|
|
410
|
+
const observedCompletedAtMs = Math.abs(observedNow - completedAtMs) < 5000
|
|
411
|
+
? Math.max(completedAtMs, observedNow) : completedAtMs;
|
|
364
412
|
for (const item of this.messages.filter(message => message.kind === 'tool'
|
|
365
413
|
&& message.turnId === completedTurnId && ['running', 'inProgress'].includes(message.toolStatus))) {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
this.
|
|
414
|
+
const toolDurationMs = Number(item.durationMs || 0)
|
|
415
|
+
|| (item.startedAtMs || item.createdAt ? Math.max(1, observedCompletedAtMs - Number(item.startedAtMs || item.createdAt)) : null);
|
|
416
|
+
const toolStatus = turnStatus === 'failed' ? 'failed' : turnStatus === 'cancelled' ? 'cancelled' : 'completed';
|
|
417
|
+
this.patch(item.id, { toolStatus,
|
|
418
|
+
completedAtMs: observedCompletedAtMs, ...(toolDurationMs != null ? { durationMs: toolDurationMs } : {}) });
|
|
370
419
|
}
|
|
371
|
-
this.
|
|
372
|
-
this.
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
420
|
+
if (threadId) this.threadTurns.delete(threadId);
|
|
421
|
+
if (!threadId || threadId === this.threadId) {
|
|
422
|
+
for (const pending of this.pendingPermissions.values()) {
|
|
423
|
+
this.recordPermission(pending.public, 'denied', 'abort');
|
|
424
|
+
}
|
|
425
|
+
this.currentTurnId = null;
|
|
426
|
+
this.currentTurnStartedAt = null;
|
|
427
|
+
this.pendingPermissions.clear();
|
|
428
|
+
if (params.turn?.status === 'failed' || params.turn?.error) {
|
|
429
|
+
this.append({ kind: 'event', level: 'error', text: params.turn?.error?.message || 'Codex turn failed.' });
|
|
430
|
+
}
|
|
431
|
+
this.setStatus('idle');
|
|
432
|
+
this.hasUnreadCompletion = true;
|
|
433
|
+
} else {
|
|
434
|
+
this.emitControlState();
|
|
376
435
|
}
|
|
377
|
-
this.setStatus('idle');
|
|
378
|
-
this.hasUnreadCompletion = true;
|
|
379
436
|
return;
|
|
380
437
|
}
|
|
381
438
|
if (method === 'thread/started' || method === 'thread/resumed') {
|
|
382
439
|
const threadId = params.thread?.id || params.threadId;
|
|
383
|
-
if (threadId) { this.threadId = threadId; this.
|
|
440
|
+
if (threadId && !this.threadId) { this.threadId = threadId; this.emitControlState(); }
|
|
384
441
|
return;
|
|
385
442
|
}
|
|
386
443
|
if (method === 'thread/status/changed') {
|
|
444
|
+
const threadId = params.threadId || this.threadId;
|
|
387
445
|
const status = params.status?.type || params.status;
|
|
388
|
-
if (
|
|
389
|
-
|
|
446
|
+
if (!threadId || threadId === this.threadId) {
|
|
447
|
+
if (status === 'idle' && !this.currentTurnId) this.setStatus('idle');
|
|
448
|
+
if (status === 'active') this.setStatus('running');
|
|
449
|
+
}
|
|
390
450
|
return;
|
|
391
451
|
}
|
|
392
452
|
if (method === 'thread/settings/updated') {
|
|
@@ -430,11 +490,16 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
430
490
|
}
|
|
431
491
|
if (method.startsWith('item/')) {
|
|
432
492
|
const inferredStatus = method === 'item/completed' ? 'completed' : method === 'item/started' ? 'running' : null;
|
|
433
|
-
this.applyProviderItem(params.item || params, inferredStatus
|
|
493
|
+
this.applyProviderItem(params.item || params, inferredStatus, {
|
|
494
|
+
threadId: params.threadId || null,
|
|
495
|
+
turnId: params.turnId || null,
|
|
496
|
+
startedAtMs: Number(params.startedAtMs || 0) || null,
|
|
497
|
+
completedAtMs: Number(params.completedAtMs || 0) || null
|
|
498
|
+
});
|
|
434
499
|
}
|
|
435
500
|
}
|
|
436
501
|
|
|
437
|
-
applyProviderItem(raw, inferredStatus = null) {
|
|
502
|
+
applyProviderItem(raw, inferredStatus = null, context = {}) {
|
|
438
503
|
if (!raw || typeof raw !== 'object') return;
|
|
439
504
|
const providerId = String(raw.id || '');
|
|
440
505
|
const existing = providerId && this.messages.find(item => item.providerId === providerId);
|
|
@@ -443,10 +508,25 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
443
508
|
if (!kind) return;
|
|
444
509
|
const text = kind === 'user' ? textFromInputItems(raw.content) : kind === 'assistant' ? String(raw.text || '')
|
|
445
510
|
: kind === 'reasoning' ? (raw.text || (Array.isArray(raw.summary) ? raw.summary.join('\n') : Array.isArray(raw.content) ? raw.content.join('\n') : '')) : '';
|
|
446
|
-
const inferredToolStatus =
|
|
447
|
-
? raw.status : inferredStatus;
|
|
448
|
-
const
|
|
449
|
-
|
|
511
|
+
const inferredToolStatus = existing?.toolStatus === 'cancelled' ? 'cancelled'
|
|
512
|
+
: inferredStatus === 'completed' && ['failed', 'declined'].includes(raw.status) ? raw.status : inferredStatus;
|
|
513
|
+
const threadId = raw.threadId || context.threadId || null;
|
|
514
|
+
const trackedTurnId = threadId ? this.threadTurns.get(threadId)?.turnId : null;
|
|
515
|
+
const turnId = raw.turnId || context.turnId || trackedTurnId || this.currentTurnId;
|
|
516
|
+
const existingStartedAtMs = existing?.startedAtMs || existing?.createdAt || null;
|
|
517
|
+
const startedAtMs = context.startedAtMs || raw.startedAtMs || existingStartedAtMs;
|
|
518
|
+
const completedAtMs = context.completedAtMs || raw.completedAtMs || null;
|
|
519
|
+
const durationMs = Number(raw.durationMs || 0)
|
|
520
|
+
|| (completedAtMs && startedAtMs && completedAtMs >= startedAtMs ? completedAtMs - startedAtMs : null)
|
|
521
|
+
|| Number(existing?.durationMs || 0) || null;
|
|
522
|
+
const timing = {
|
|
523
|
+
...(startedAtMs ? { startedAtMs } : {}),
|
|
524
|
+
...(completedAtMs ? { completedAtMs } : {}),
|
|
525
|
+
...(durationMs != null ? { durationMs } : {})
|
|
526
|
+
};
|
|
527
|
+
const patch = kind === 'tool' ? { ...toolDetails(raw), threadId, turnId,
|
|
528
|
+
...timing, toolStatus: inferredToolStatus || raw.status || 'running' }
|
|
529
|
+
: { text, threadId, turnId, streaming: false };
|
|
450
530
|
if (existing) {
|
|
451
531
|
this.patch(existing.id, patch);
|
|
452
532
|
} else if (kind === 'user') {
|
|
@@ -454,7 +534,7 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
454
534
|
if (local) this.patch(local.id, { providerId, ...patch });
|
|
455
535
|
else this.append({ kind, providerId, ...patch });
|
|
456
536
|
} else {
|
|
457
|
-
this.append({ kind, providerId, ...patch });
|
|
537
|
+
this.append({ kind, providerId, ...(startedAtMs ? { createdAt: startedAtMs } : {}), ...patch });
|
|
458
538
|
}
|
|
459
539
|
}
|
|
460
540
|
|
|
@@ -501,14 +581,28 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
501
581
|
archived: false,
|
|
502
582
|
cwd: this.workingDir
|
|
503
583
|
});
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
584
|
+
const threads = (result?.data || []).filter(item => !item.parentThreadId);
|
|
585
|
+
const items = [];
|
|
586
|
+
for (const item of threads) {
|
|
587
|
+
let questions = [];
|
|
588
|
+
try {
|
|
589
|
+
const history = await this.request('thread/read', { threadId: item.id, includeTurns: true });
|
|
590
|
+
questions = recentUserQuestions(history?.thread);
|
|
591
|
+
} catch (error) {
|
|
592
|
+
this.logger.debugInfo?.(`[codex-app-server] unable to read resume preview for ${item.id}: ${error.message}`);
|
|
593
|
+
}
|
|
594
|
+
if (!questions[0]) questions[0] = item.preview || '';
|
|
595
|
+
if (questions.length < 2) questions.push('');
|
|
596
|
+
items.push({
|
|
597
|
+
id: item.id,
|
|
598
|
+
sessionId: item.sessionId || item.id,
|
|
599
|
+
questions: questions.slice(0, 2),
|
|
600
|
+
updatedAt: Number(item.updatedAt || item.createdAt || 0) * 1000,
|
|
601
|
+
cwd: item.cwd || '',
|
|
602
|
+
current: item.id === this.threadId
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
return items;
|
|
512
606
|
}
|
|
513
607
|
|
|
514
608
|
contextStatus() {
|
|
@@ -686,9 +780,16 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
686
780
|
this.recordPermission(pending.public, 'denied', 'abort');
|
|
687
781
|
}
|
|
688
782
|
this.pendingPermissions.clear();
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
783
|
+
const targets = Array.from(this.threadTurns.entries())
|
|
784
|
+
.filter(([, turn]) => turn?.turnId && turn.status === 'running')
|
|
785
|
+
.map(([threadId, turn]) => ({ threadId, turnId: turn.turnId }));
|
|
786
|
+
if (this.threadId && this.currentTurnId
|
|
787
|
+
&& !targets.some(target => target.threadId === this.threadId && target.turnId === this.currentTurnId)) {
|
|
788
|
+
targets.push({ threadId: this.threadId, turnId: this.currentTurnId });
|
|
789
|
+
}
|
|
790
|
+
for (const target of targets) {
|
|
791
|
+
this.request('turn/interrupt', target).catch(error => {
|
|
792
|
+
this.logger.debugInfo?.(`[codex-app-server] turn/interrupt failed for ${target.threadId}/${target.turnId}: ${error.message}`);
|
|
692
793
|
});
|
|
693
794
|
}
|
|
694
795
|
this.append({ kind: 'event', level: 'info', text: reason });
|
package/lib/web/index.html
CHANGED
|
@@ -116,6 +116,7 @@
|
|
|
116
116
|
.codex-tool-title { flex: 0 1 auto; min-width: 0; color: #f5f5f7; font-size: 13px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
117
117
|
.codex-tool-command { flex: 1; min-width: 0; color: #a9a9b0; font: 12px/1.3 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
118
118
|
.codex-tool-state { flex: 0 0 auto; color: #8e8e93; font-size: 11px; }
|
|
119
|
+
.codex-tool-duration { flex: 0 0 auto; color: #6f6f76; font-size: 10px; font-variant-numeric: tabular-nums; }
|
|
119
120
|
.codex-tool-state.running::before { content: ''; display: inline-block; width: 9px; height: 9px; margin-right: 5px; border: 1.5px solid #8e8e93; border-top-color: transparent; border-radius: 50%; animation: codex-spin .8s linear infinite; vertical-align: -1px; }
|
|
120
121
|
.codex-tool.error { border-color: rgba(255,59,48,.38); background: rgba(255,59,48,.07); }
|
|
121
122
|
.codex-tool-body { border-top: 1px solid rgba(255,255,255,.08); padding: 9px 10px; }
|
|
@@ -257,6 +258,7 @@
|
|
|
257
258
|
.claude-resume-item:last-child { border-bottom: 0; }
|
|
258
259
|
.claude-resume-item:active { background: rgba(255,255,255,0.08); }
|
|
259
260
|
.claude-resume-title { display: flex; justify-content: space-between; gap: 8px; font-size: 12px; font-weight: 800; }
|
|
261
|
+
.codex-resume-question-secondary { min-height: 1.35em; color: var(--text-dim); font-size: 11px; margin-top: 3px; overflow-wrap: anywhere; }
|
|
260
262
|
.claude-resume-meta { color: var(--text-dim); font-size: 11px; margin-top: 3px; overflow-wrap: anywhere; }
|
|
261
263
|
#terminal .xterm, #terminal .xterm-viewport, #terminal .xterm-screen, #terminal .xterm-rows, #terminal .xterm-rows span { user-select: text !important; -webkit-user-select: text !important; -webkit-touch-callout: default; touch-action: pan-y; }
|
|
262
264
|
#terminal .xterm-viewport { overflow-anchor: none; }
|
|
@@ -658,6 +660,7 @@
|
|
|
658
660
|
let codexModelPanelOpen = false;
|
|
659
661
|
let codexModelCandidate = null;
|
|
660
662
|
let codexResumePanelOpen = false;
|
|
663
|
+
let codexRenderFrame = null;
|
|
661
664
|
const modifiers = { ctrl: false };
|
|
662
665
|
|
|
663
666
|
function log(msg) {
|
|
@@ -2070,6 +2073,16 @@
|
|
|
2070
2073
|
if (status === 'completed') return item.exitCode && item.exitCode !== 0 ? 'failed' : 'completed';
|
|
2071
2074
|
return status;
|
|
2072
2075
|
}
|
|
2076
|
+
function formatCodexDuration(durationMs) {
|
|
2077
|
+
const value = Number(durationMs || 0);
|
|
2078
|
+
if (!(value > 0)) return '';
|
|
2079
|
+
if (value < 1000) return `${Math.round(value)}ms`;
|
|
2080
|
+
const seconds = value / 1000;
|
|
2081
|
+
if (seconds < 10) return `${seconds.toFixed(1).replace(/\.0$/, '')}s`;
|
|
2082
|
+
if (seconds < 60) return `${Math.round(seconds)}s`;
|
|
2083
|
+
const minutes = Math.floor(seconds / 60);
|
|
2084
|
+
return `${minutes}m ${Math.round(seconds % 60)}s`;
|
|
2085
|
+
}
|
|
2073
2086
|
function renderCodexDiff(diff) {
|
|
2074
2087
|
return `<div class="codex-diff">${String(diff || '').split('\n').map(line => {
|
|
2075
2088
|
const type = line.startsWith('+++') || line.startsWith('---') ? 'hunk' : line.startsWith('+') ? 'add' : line.startsWith('-') ? 'del' : line.startsWith('@@') ? 'hunk' : '';
|
|
@@ -2153,27 +2166,72 @@
|
|
|
2153
2166
|
const command = item.name === 'CodexBash' ? item.command : item.title || item.tool || '';
|
|
2154
2167
|
const icon = item.name === 'CodexBash' ? '>_' : item.name === 'McpTool' ? 'MCP' : item.name === 'Agent' ? 'A' : '•';
|
|
2155
2168
|
const title = item.name === 'CodexBash' ? 'Command' : item.title || item.name || 'Tool';
|
|
2156
|
-
const
|
|
2157
|
-
|
|
2169
|
+
const hasInput = item.input && typeof item.input === 'object' && Object.keys(item.input).length > 0;
|
|
2170
|
+
const result = item.result || ((item.name === 'McpTool' || item.name === 'Agent') && hasInput ? codexJson(item.input) : '');
|
|
2171
|
+
const duration = status === 'running' ? '' : formatCodexDuration(item.durationMs);
|
|
2172
|
+
return `<details class="codex-tool${isError ? ' error' : ''}" data-codex-key="tool-${escapeHtml(item.id || item.providerId || '')}"><summary class="codex-tool-header"><span class="codex-tool-icon">${escapeHtml(icon)}</span><span class="codex-tool-title">${escapeHtml(title)}</span>${command && command !== title ? `<span class="codex-tool-command">${escapeHtml(command)}</span>` : '<span class="codex-tool-command"></span>'}${duration ? `<span class="codex-tool-duration">${escapeHtml(duration)}</span>` : ''}<span class="codex-tool-state${runningClass}">${escapeHtml(status === 'completed' ? '' : status)}</span></summary>${result ? `<div class="codex-tool-body"><pre class="codex-tool-code">${escapeHtml(result)}</pre></div>` : ''}${permission ? renderCodexPermission(permission, true) : ''}</details>`;
|
|
2158
2173
|
}
|
|
2159
2174
|
function renderCodexToolGroup(items, permissionById, usedPermissions, turnEnd = null) {
|
|
2160
2175
|
const running = items.some(item => codexToolStatus(item) === 'running');
|
|
2161
2176
|
const failed = items.some(item => codexToolStatus(item) === 'failed' || item.error);
|
|
2162
|
-
const startedAt = Math.min(...items.map(item => Number(item.createdAt || Date.now())));
|
|
2177
|
+
const startedAt = Math.min(...items.map(item => Number(item.startedAtMs || item.createdAt || Date.now())));
|
|
2178
|
+
const itemCompletedAt = Math.max(...items.map(item => Number(item.completedAtMs || 0)));
|
|
2163
2179
|
const storedDuration = Number(turnEnd?.durationMs || 0);
|
|
2164
2180
|
const completedAt = Number(turnEnd?.createdAt || 0);
|
|
2165
|
-
const durationMs = storedDuration > 0 ? storedDuration
|
|
2181
|
+
const durationMs = itemCompletedAt >= startedAt ? itemCompletedAt - startedAt : storedDuration > 0 ? storedDuration
|
|
2166
2182
|
: (!running && completedAt >= startedAt ? completedAt - startedAt : 0);
|
|
2167
|
-
const
|
|
2183
|
+
const duration = formatCodexDuration(durationMs);
|
|
2168
2184
|
const tools = items.map(item => {
|
|
2169
2185
|
const permission = permissionById.get(String(item.providerId || ''));
|
|
2170
2186
|
if (permission) usedPermissions.add(permission.id);
|
|
2171
2187
|
return renderCodexTool(item, permission);
|
|
2172
2188
|
}).join('');
|
|
2173
|
-
const label = running ? 'Working' : failed ? 'Work finished with errors' :
|
|
2174
|
-
|
|
2189
|
+
const label = running ? 'Working' : failed ? 'Work finished with errors' : duration ? `Worked for ${duration}` : 'Worked';
|
|
2190
|
+
const key = items.map(item => item.id || item.providerId || '').join('-');
|
|
2191
|
+
return `<details class="codex-work-group" data-codex-key="group-${escapeHtml(key)}"${running ? ' open' : ''}><summary>${label} · ${items.length} tools</summary><div class="codex-work-group-body">${tools}</div></details>`;
|
|
2192
|
+
}
|
|
2193
|
+
function syncCodexDom(current, next) {
|
|
2194
|
+
if (!current || !next) return;
|
|
2195
|
+
if (current.nodeType !== next.nodeType || current.nodeName !== next.nodeName) {
|
|
2196
|
+
current.replaceWith(next.cloneNode(true));
|
|
2197
|
+
return;
|
|
2198
|
+
}
|
|
2199
|
+
if (current.nodeType === Node.TEXT_NODE) {
|
|
2200
|
+
if (current.nodeValue !== next.nodeValue) current.nodeValue = next.nodeValue;
|
|
2201
|
+
return;
|
|
2202
|
+
}
|
|
2203
|
+
const currentKey = current.getAttribute?.('data-codex-key');
|
|
2204
|
+
const nextKey = next.getAttribute?.('data-codex-key');
|
|
2205
|
+
if (currentKey && nextKey && currentKey !== nextKey) {
|
|
2206
|
+
current.replaceWith(next.cloneNode(true));
|
|
2207
|
+
return;
|
|
2208
|
+
}
|
|
2209
|
+
const preserveOpen = current.tagName === 'DETAILS' && currentKey === nextKey;
|
|
2210
|
+
const wasOpen = preserveOpen ? current.open : false;
|
|
2211
|
+
for (const attribute of Array.from(current.attributes || [])) {
|
|
2212
|
+
if (!next.hasAttribute(attribute.name) && !(preserveOpen && attribute.name === 'open')) current.removeAttribute(attribute.name);
|
|
2213
|
+
}
|
|
2214
|
+
for (const attribute of Array.from(next.attributes || [])) {
|
|
2215
|
+
if (!(preserveOpen && attribute.name === 'open') && current.getAttribute(attribute.name) !== attribute.value) {
|
|
2216
|
+
current.setAttribute(attribute.name, attribute.value);
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
if (preserveOpen) current.open = wasOpen;
|
|
2220
|
+
const currentChildren = Array.from(current.childNodes);
|
|
2221
|
+
const nextChildren = Array.from(next.childNodes);
|
|
2222
|
+
const shared = Math.min(currentChildren.length, nextChildren.length);
|
|
2223
|
+
for (let i = 0; i < shared; i++) syncCodexDom(currentChildren[i], nextChildren[i]);
|
|
2224
|
+
for (let i = current.childNodes.length - 1; i >= nextChildren.length; i--) current.childNodes[i].remove();
|
|
2225
|
+
for (let i = shared; i < nextChildren.length; i++) current.appendChild(nextChildren[i].cloneNode(true));
|
|
2175
2226
|
}
|
|
2176
2227
|
function renderCodexChat() {
|
|
2228
|
+
if (codexRenderFrame != null) return;
|
|
2229
|
+
codexRenderFrame = requestAnimationFrame(() => {
|
|
2230
|
+
codexRenderFrame = null;
|
|
2231
|
+
commitCodexChatRender();
|
|
2232
|
+
});
|
|
2233
|
+
}
|
|
2234
|
+
function commitCodexChatRender() {
|
|
2177
2235
|
const container = document.getElementById('codex-chat-container');
|
|
2178
2236
|
if (!container) return;
|
|
2179
2237
|
const parts = [];
|
|
@@ -2197,16 +2255,20 @@
|
|
|
2197
2255
|
}
|
|
2198
2256
|
continue;
|
|
2199
2257
|
}
|
|
2200
|
-
if (item.kind === 'assistant') parts.push(`<div class="codex-message assistant claude-md">${renderMarkdown(item.text || '')}</div>`);
|
|
2201
|
-
else if (item.kind === 'user') parts.push(`<div class="codex-message user claude-md">${renderMarkdown(item.text || '')}</div>`);
|
|
2258
|
+
if (item.kind === 'assistant') parts.push(`<div class="codex-message assistant claude-md" data-codex-key="message-${escapeHtml(item.id || '')}">${renderMarkdown(item.text || '')}</div>`);
|
|
2259
|
+
else if (item.kind === 'user') parts.push(`<div class="codex-message user claude-md" data-codex-key="message-${escapeHtml(item.id || '')}">${renderMarkdown(item.text || '')}</div>`);
|
|
2202
2260
|
else if (item.kind === 'status') parts.push(renderCodexStatus(item));
|
|
2203
2261
|
else parts.push(`<div class="codex-message ${escapeHtml(item.level === 'error' ? 'event error' : item.kind || 'event')}">${codexText(item.text)}</div>`);
|
|
2204
2262
|
i += 1;
|
|
2205
2263
|
}
|
|
2206
2264
|
for (const request of codexPendingPermissions) if (!usedPermissions.has(request.id)) parts.push(renderCodexPermission(request));
|
|
2207
|
-
const working = codexState.status === 'running'
|
|
2208
|
-
|
|
2209
|
-
|
|
2265
|
+
const working = `<div class="codex-working-indicator" role="status" aria-label="Codex is working" title="Codex is working"${codexState.status === 'running' ? '' : ' style="display:none"'}></div>`;
|
|
2266
|
+
const template = document.createElement('template');
|
|
2267
|
+
template.innerHTML = `<div class="codex-conversation">${working}${parts.join('') || '<div class="codex-message event">Send a message to start Codex.</div>'}</div>`;
|
|
2268
|
+
const next = template.content.firstElementChild;
|
|
2269
|
+
const current = container.firstElementChild;
|
|
2270
|
+
if (!current) container.appendChild(next);
|
|
2271
|
+
else syncCodexDom(current, next);
|
|
2210
2272
|
requestAnimationFrame(() => { container.scrollTop = container.scrollHeight; });
|
|
2211
2273
|
}
|
|
2212
2274
|
|
|
@@ -2247,8 +2309,10 @@
|
|
|
2247
2309
|
const el = document.getElementById('codex-state-bar');
|
|
2248
2310
|
if (!el) return;
|
|
2249
2311
|
const pending = Number(codexState.pendingPermissionCount || codexPendingPermissions.filter(item => item.status === 'pending').length) || 0;
|
|
2312
|
+
const subagents = Number(codexState.activeSubagentCount || 0) || 0;
|
|
2250
2313
|
const parts = [];
|
|
2251
2314
|
if (pending || codexState.status === 'waiting_approval') parts.push(`<span class="claude-state-pill warn">${pending || 1} approval${pending === 1 ? '' : 's'}</span>`);
|
|
2315
|
+
if (subagents) parts.push(`<span class="claude-state-pill">${subagents} subagent${subagents === 1 ? '' : 's'} running</span>`);
|
|
2252
2316
|
el.innerHTML = parts.join('');
|
|
2253
2317
|
el.style.display = parts.length ? 'flex' : 'none';
|
|
2254
2318
|
}
|
|
@@ -2317,7 +2381,10 @@
|
|
|
2317
2381
|
const data = await res.json();
|
|
2318
2382
|
if (!res.ok || !data.success) throw new Error(data.error || 'Failed to load Codex sessions');
|
|
2319
2383
|
const items = data.items || [];
|
|
2320
|
-
panel.innerHTML = items.length ? items.map(item =>
|
|
2384
|
+
panel.innerHTML = items.length ? items.map(item => {
|
|
2385
|
+
const questions = Array.isArray(item.questions) ? item.questions : [];
|
|
2386
|
+
return `<button class="claude-resume-item" onclick="selectCodexResumeThread(decodePathValue('${encodePathValue(item.id)}'))"><div class="claude-resume-title"><span>${escapeHtml((questions[0] || 'Codex session').slice(0, 120))}${item.current ? ' · current' : ''}</span><span>${escapeHtml(item.updatedAt ? new Date(item.updatedAt).toLocaleString() : '')}</span></div><div class="codex-resume-question-secondary">${escapeHtml((questions[1] || '').slice(0, 120))}</div></button>`;
|
|
2387
|
+
}).join('') : '<div class="claude-resume-meta" style="padding:12px;">No Codex sessions found for this folder.</div>';
|
|
2321
2388
|
} catch (e) { panel.innerHTML = `<div class="claude-resume-meta" style="padding:12px;color:#ff6b61;">${escapeHtml(e.message)}</div>`; }
|
|
2322
2389
|
}
|
|
2323
2390
|
async function selectCodexResumeThread(threadId) {
|