glad-web 1.0.30 → 1.0.31
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 +64 -7
- package/lib/commands/web.js +10 -3
- package/lib/session/session-manager.js +6 -0
- package/lib/web/codex.js +46 -2
- package/lib/web/core.js +1 -1
- package/lib/web/index.html +1 -0
- package/lib/web/session.js +1 -1
- package/lib/web/styles.css +13 -2
- package/package.json +1 -1
|
@@ -167,8 +167,10 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
167
167
|
this.currentTurnId = null;
|
|
168
168
|
this.currentTurnStartedAt = null;
|
|
169
169
|
this.threadTurns = new Map();
|
|
170
|
+
this.turnContexts = new Map();
|
|
170
171
|
this.providerItemContexts = new Map();
|
|
171
172
|
this.tokenUsage = null;
|
|
173
|
+
this.compacting = false;
|
|
172
174
|
this.permissionMode = normalizePermissionMode(options.permissionMode);
|
|
173
175
|
this.sandboxMode = normalizeSandboxMode(options.sandboxMode);
|
|
174
176
|
this.effectivePermissionMode = null;
|
|
@@ -226,6 +228,8 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
226
228
|
model: this.model, effort: this.effort,
|
|
227
229
|
status: this.status, threadId: this.threadId, presentation: this.presentation,
|
|
228
230
|
canAbort: this.presentation === 'structured' && this.status !== 'idle',
|
|
231
|
+
canCompact: this.presentation === 'structured' && this.status === 'idle' && !this.compacting && Boolean(this.threadId),
|
|
232
|
+
compacting: this.compacting,
|
|
229
233
|
canSwitchToTerminal: this.presentation === 'structured' && this.status === 'idle' && Boolean(this.threadId),
|
|
230
234
|
canSwitchToStructured: this.presentation === 'terminal',
|
|
231
235
|
pendingPermissionCount: this.pendingPermissions.size, activeSubagentCount, models: this.models };
|
|
@@ -289,6 +293,7 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
289
293
|
for (const request of this.pendingRequests.values()) request.reject(new Error(`Codex app-server exited (${code})`));
|
|
290
294
|
this.pendingRequests.clear();
|
|
291
295
|
if (this.running && this.presentation === 'structured') {
|
|
296
|
+
this.compacting = false;
|
|
292
297
|
this.append({ kind: 'event', level: 'error', text: 'Codex app-server exited.' });
|
|
293
298
|
this.setStatus('idle');
|
|
294
299
|
}
|
|
@@ -382,6 +387,7 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
382
387
|
handleNotification(method, params) {
|
|
383
388
|
if (method === 'thread/tokenUsage/updated') {
|
|
384
389
|
this.tokenUsage = params.tokenUsage || params.usage || params;
|
|
390
|
+
this.recordTurnContext(params.turnId, this.tokenUsage);
|
|
385
391
|
return;
|
|
386
392
|
}
|
|
387
393
|
if (method === 'turn/started') {
|
|
@@ -411,8 +417,9 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
411
417
|
const startedAtMs = trackedTurn?.startedAt || ((!threadId || threadId === this.threadId) ? this.currentTurnStartedAt : null);
|
|
412
418
|
const durationMs = Number(params.turn?.durationMs || 0)
|
|
413
419
|
|| (startedAtMs ? Math.max(0, completedAtMs - startedAtMs) : null);
|
|
420
|
+
const context = this.turnContexts.get(String(completedTurnId || ''));
|
|
414
421
|
this.append({ kind: 'turn-end', threadId, turnId: completedTurnId, status: turnStatus,
|
|
415
|
-
durationMs, createdAt: completedAtMs });
|
|
422
|
+
durationMs, createdAt: completedAtMs, ...(context ? { context } : {}) });
|
|
416
423
|
const observedNow = Date.now();
|
|
417
424
|
const observedCompletedAtMs = Math.abs(observedNow - completedAtMs) < 5000
|
|
418
425
|
? Math.max(completedAtMs, observedNow) : completedAtMs;
|
|
@@ -424,8 +431,13 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
424
431
|
this.patch(item.id, { toolStatus,
|
|
425
432
|
completedAtMs: observedCompletedAtMs, ...(toolDurationMs != null ? { durationMs: toolDurationMs } : {}) });
|
|
426
433
|
}
|
|
434
|
+
for (const item of this.messages.filter(message => message.kind === 'compaction'
|
|
435
|
+
&& message.turnId === completedTurnId && message.compactionStatus === 'running')) {
|
|
436
|
+
this.patch(item.id, { compactionStatus: 'completed', completedAtMs: observedCompletedAtMs });
|
|
437
|
+
}
|
|
427
438
|
if (threadId) this.threadTurns.delete(threadId);
|
|
428
439
|
if (!threadId || threadId === this.threadId) {
|
|
440
|
+
this.compacting = false;
|
|
429
441
|
for (const pending of this.pendingPermissions.values()) {
|
|
430
442
|
this.recordPermission(pending.public, 'denied', 'abort');
|
|
431
443
|
}
|
|
@@ -442,6 +454,14 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
442
454
|
}
|
|
443
455
|
return;
|
|
444
456
|
}
|
|
457
|
+
if (method === 'thread/compacted') {
|
|
458
|
+
const threadId = params.threadId || this.threadId;
|
|
459
|
+
const turnId = params.turnId || (threadId ? this.threadTurns.get(threadId)?.turnId : null) || this.currentTurnId;
|
|
460
|
+
this.applyProviderItem({ id: `compaction-${turnId || Date.now()}`, type: 'contextCompaction', threadId, turnId }, 'completed', {
|
|
461
|
+
threadId, turnId, completedAtMs: Date.now()
|
|
462
|
+
});
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
445
465
|
if (method === 'thread/started' || method === 'thread/resumed') {
|
|
446
466
|
const threadId = params.thread?.id || params.threadId;
|
|
447
467
|
if (threadId && !this.threadId) { this.threadId = threadId; this.emitControlState(); }
|
|
@@ -451,7 +471,7 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
451
471
|
const threadId = params.threadId || this.threadId;
|
|
452
472
|
const status = params.status?.type || params.status;
|
|
453
473
|
if (!threadId || threadId === this.threadId) {
|
|
454
|
-
if (status === 'idle' && !this.currentTurnId) this.setStatus('idle');
|
|
474
|
+
if (status === 'idle' && !this.currentTurnId) { this.compacting = false; this.setStatus('idle'); }
|
|
455
475
|
if (status === 'active') this.setStatus('running');
|
|
456
476
|
}
|
|
457
477
|
return;
|
|
@@ -467,7 +487,7 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
467
487
|
}
|
|
468
488
|
if (method === 'error') {
|
|
469
489
|
this.append({ kind: 'event', level: 'error', text: params.error?.message || 'Codex reported an error.' });
|
|
470
|
-
if (!params.willRetry) this.setStatus('idle');
|
|
490
|
+
if (!params.willRetry) { this.compacting = false; this.setStatus('idle'); }
|
|
471
491
|
return;
|
|
472
492
|
}
|
|
473
493
|
if (method === 'warning' || method === 'guardianWarning') {
|
|
@@ -519,9 +539,10 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
519
539
|
applyProviderItem(raw, inferredStatus = null, context = {}) {
|
|
520
540
|
if (!raw || typeof raw !== 'object') return;
|
|
521
541
|
const providerId = String(raw.id || '');
|
|
522
|
-
|
|
542
|
+
let existing = providerId && this.messages.find(item => item.providerId === providerId);
|
|
523
543
|
const kind = raw.type === 'userMessage' ? 'user' : raw.type === 'agentMessage' ? 'assistant' : ['reasoning', 'plan'].includes(raw.type) ? 'reasoning'
|
|
524
|
-
: ['commandExecution', 'fileChange', 'mcpToolCall', 'dynamicToolCall', 'webSearch', 'collabAgentToolCall'].includes(raw.type) ? 'tool'
|
|
544
|
+
: ['commandExecution', 'fileChange', 'mcpToolCall', 'dynamicToolCall', 'webSearch', 'collabAgentToolCall'].includes(raw.type) ? 'tool'
|
|
545
|
+
: raw.type === 'contextCompaction' ? 'compaction' : null;
|
|
525
546
|
if (!kind) return;
|
|
526
547
|
const text = kind === 'user' ? textFromInputItems(raw.content) : kind === 'assistant' ? String(raw.text || '')
|
|
527
548
|
: kind === 'reasoning' ? (raw.text || (Array.isArray(raw.summary) ? raw.summary.join('\n') : Array.isArray(raw.content) ? raw.content.join('\n') : '')) : '';
|
|
@@ -530,6 +551,9 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
530
551
|
const threadId = raw.threadId || context.threadId || null;
|
|
531
552
|
const trackedTurnId = threadId ? this.threadTurns.get(threadId)?.turnId : null;
|
|
532
553
|
const turnId = raw.turnId || context.turnId || trackedTurnId || this.currentTurnId;
|
|
554
|
+
if (!existing && kind === 'compaction' && turnId) {
|
|
555
|
+
existing = this.messages.find(item => item.kind === 'compaction' && item.turnId === turnId);
|
|
556
|
+
}
|
|
533
557
|
if (providerId && (threadId || turnId)) this.providerItemContexts.set(providerId, { threadId, turnId });
|
|
534
558
|
const existingStartedAtMs = existing?.startedAtMs || existing?.createdAt || null;
|
|
535
559
|
const startedAtMs = Number(context.startedAtMs || raw.startedAtMs || 0)
|
|
@@ -546,6 +570,8 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
546
570
|
};
|
|
547
571
|
const patch = kind === 'tool' ? { ...toolDetails(raw), threadId, turnId,
|
|
548
572
|
...timing, toolStatus: inferredToolStatus || raw.status || 'running' }
|
|
573
|
+
: kind === 'compaction' ? { providerId, threadId, turnId, ...timing,
|
|
574
|
+
compactionStatus: inferredStatus || raw.status || 'running' }
|
|
549
575
|
: { text, threadId, turnId, streaming: false, ...(completedAtMs ? { completedAtMs } : {}) };
|
|
550
576
|
if (existing) {
|
|
551
577
|
this.patch(existing.id, patch);
|
|
@@ -556,6 +582,10 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
556
582
|
} else {
|
|
557
583
|
this.append({ kind, providerId, ...(startedAtMs ? { createdAt: startedAtMs } : {}), ...patch });
|
|
558
584
|
}
|
|
585
|
+
if (kind === 'compaction' && (!threadId || threadId === this.threadId)) {
|
|
586
|
+
this.compacting = patch.compactionStatus === 'running';
|
|
587
|
+
this.emitControlState();
|
|
588
|
+
}
|
|
559
589
|
}
|
|
560
590
|
|
|
561
591
|
async refreshModels() {
|
|
@@ -625,8 +655,8 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
625
655
|
return items;
|
|
626
656
|
}
|
|
627
657
|
|
|
628
|
-
contextStatus() {
|
|
629
|
-
const usage =
|
|
658
|
+
contextStatus(tokenUsage = this.tokenUsage) {
|
|
659
|
+
const usage = tokenUsage || {};
|
|
630
660
|
const selectedModel = this.models.find(item => item.id === this.model);
|
|
631
661
|
const contextWindow = Number(usage.modelContextWindow || usage.model_context_window
|
|
632
662
|
|| usage.contextWindow || usage.context_window || selectedModel?.contextWindow || 0);
|
|
@@ -646,6 +676,16 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
646
676
|
};
|
|
647
677
|
}
|
|
648
678
|
|
|
679
|
+
recordTurnContext(turnId, tokenUsage = this.tokenUsage) {
|
|
680
|
+
const id = String(turnId || '').trim();
|
|
681
|
+
const context = this.contextStatus(tokenUsage);
|
|
682
|
+
if (!id || !context) return context;
|
|
683
|
+
this.turnContexts.set(id, context);
|
|
684
|
+
const turnEnd = this.messages.find(item => item.kind === 'turn-end' && String(item.turnId || '') === id);
|
|
685
|
+
if (turnEnd) this.patch(turnEnd.id, { context });
|
|
686
|
+
return context;
|
|
687
|
+
}
|
|
688
|
+
|
|
649
689
|
async showStatus() {
|
|
650
690
|
if (this.presentation !== 'structured') return false;
|
|
651
691
|
await this.ensureProcess();
|
|
@@ -759,6 +799,22 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
759
799
|
}
|
|
760
800
|
}
|
|
761
801
|
|
|
802
|
+
async compactContext() {
|
|
803
|
+
if (!this.threadId || this.presentation !== 'structured' || this.status !== 'idle') return false;
|
|
804
|
+
await this.ensureProcess();
|
|
805
|
+
this.compacting = true;
|
|
806
|
+
this.setStatus('running');
|
|
807
|
+
try {
|
|
808
|
+
await this.request('thread/compact/start', { threadId: this.threadId });
|
|
809
|
+
return true;
|
|
810
|
+
} catch (error) {
|
|
811
|
+
this.compacting = false;
|
|
812
|
+
this.setStatus('idle');
|
|
813
|
+
this.append({ kind: 'event', level: 'error', text: `Unable to compact context: ${error.message}` });
|
|
814
|
+
throw error;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
762
818
|
write(data) {
|
|
763
819
|
if (this.presentation === 'terminal') return this.terminalSession?.write(data) || false;
|
|
764
820
|
const text = String(data || '').replace(/\r/g, '\n');
|
|
@@ -848,6 +904,7 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
848
904
|
this.tokenUsage = thread?.tokenUsage || thread?.token_usage || this.tokenUsage;
|
|
849
905
|
this.messages = [];
|
|
850
906
|
this.completedPermissions = [];
|
|
907
|
+
this.turnContexts.clear();
|
|
851
908
|
this.providerItemContexts.clear();
|
|
852
909
|
for (const turn of thread?.turns || []) {
|
|
853
910
|
const status = turn.status === 'failed' ? 'failed' : turn.status === 'interrupted' ? 'cancelled' : 'completed';
|
package/lib/commands/web.js
CHANGED
|
@@ -374,6 +374,9 @@ async function webCommand(options) {
|
|
|
374
374
|
if (payload.type === 'codex-status') {
|
|
375
375
|
sessionManager.showCodexStatus(sessionId).catch(error => logger.error(`Codex status error: ${error.message}`));
|
|
376
376
|
}
|
|
377
|
+
if (payload.type === 'codex-compact') {
|
|
378
|
+
sessionManager.compactCodexContext(sessionId).catch(error => logger.error(`Codex compact error: ${error.message}`));
|
|
379
|
+
}
|
|
377
380
|
if (payload.type === 'codex-abort') {
|
|
378
381
|
sessionManager.abortCodex(sessionId);
|
|
379
382
|
}
|
|
@@ -436,6 +439,10 @@ async function webCommand(options) {
|
|
|
436
439
|
});
|
|
437
440
|
};
|
|
438
441
|
|
|
442
|
+
const sendDependencyAsset = assetPath => (req, res) => {
|
|
443
|
+
res.sendFile(path.basename(assetPath), { root: path.dirname(assetPath) });
|
|
444
|
+
};
|
|
445
|
+
|
|
439
446
|
const webAssets = [
|
|
440
447
|
'gitgraph.js',
|
|
441
448
|
'styles.css',
|
|
@@ -455,9 +462,9 @@ async function webCommand(options) {
|
|
|
455
462
|
app.get([`/${assetName}`, new RegExp(`.*\\/${escapedName}$`)], sendWebAsset(assetName));
|
|
456
463
|
}
|
|
457
464
|
|
|
458
|
-
app.get('/vendor/xterm.js', (
|
|
459
|
-
app.get('/vendor/xterm.css', (
|
|
460
|
-
app.get('/vendor/xterm-addon-fit.js', (
|
|
465
|
+
app.get('/vendor/xterm.js', sendDependencyAsset(xtermScript));
|
|
466
|
+
app.get('/vendor/xterm.css', sendDependencyAsset(xtermStyles));
|
|
467
|
+
app.get('/vendor/xterm-addon-fit.js', sendDependencyAsset(fitAddonScript));
|
|
461
468
|
|
|
462
469
|
app.get(['/logo.svg', /.*\/logo\.svg$/], sendLogo);
|
|
463
470
|
|
|
@@ -389,6 +389,12 @@ class SessionManager extends EventEmitter {
|
|
|
389
389
|
return session.showStatus();
|
|
390
390
|
}
|
|
391
391
|
|
|
392
|
+
compactCodexContext(id) {
|
|
393
|
+
const session = this.get(id);
|
|
394
|
+
if (!session || session.kind !== 'codex-structured') return Promise.resolve(false);
|
|
395
|
+
return session.compactContext();
|
|
396
|
+
}
|
|
397
|
+
|
|
392
398
|
abortCodex(id) {
|
|
393
399
|
const session = this.get(id);
|
|
394
400
|
return session && session.kind === 'codex-structured' ? session.abort('Aborted by user') : false;
|
package/lib/web/codex.js
CHANGED
|
@@ -94,6 +94,15 @@
|
|
|
94
94
|
${codexStatusItem('Context', contextLabel)}
|
|
95
95
|
</div></div>`;
|
|
96
96
|
}
|
|
97
|
+
function renderCodexCompaction(item) {
|
|
98
|
+
const running = item.compactionStatus === 'running';
|
|
99
|
+
const timestamp = Number(item.completedAtMs || item.updatedAt || item.createdAt || 0);
|
|
100
|
+
const date = timestamp > 0 ? new Date(timestamp) : null;
|
|
101
|
+
const time = date && !Number.isNaN(date.getTime())
|
|
102
|
+
? date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '';
|
|
103
|
+
const detail = running ? 'Compacting context…' : ['Completed', time].filter(Boolean).join(' · ');
|
|
104
|
+
return `<div class="codex-compaction-card${running ? ' running' : ''}" data-codex-key="compaction-${escapeHtml(item.id || item.providerId || '')}"><span class="codex-compaction-icon" aria-hidden="true">⇣</span><div><div class="codex-compaction-title">${running ? 'Compacting context' : 'Context compacted'}</div><div class="codex-compaction-meta">${escapeHtml(detail)}</div></div></div>`;
|
|
105
|
+
}
|
|
97
106
|
function renderCodexTool(item, permission = null) {
|
|
98
107
|
const status = codexToolStatus(item);
|
|
99
108
|
const isError = status === 'failed' || Boolean(item.error) || (item.exitCode != null && item.exitCode !== 0);
|
|
@@ -177,6 +186,21 @@
|
|
|
177
186
|
const label = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
178
187
|
return `<time class="codex-message-time" datetime="${escapeHtml(date.toISOString())}" title="${escapeHtml(date.toLocaleString())}">${escapeHtml(label)}</time>`;
|
|
179
188
|
}
|
|
189
|
+
function renderCodexContextMeter(context) {
|
|
190
|
+
const total = Number(context?.contextWindow || 0);
|
|
191
|
+
const remaining = Number(context?.remainingTokens || 0);
|
|
192
|
+
if (!(total > 0) || !Number.isFinite(remaining)) return '';
|
|
193
|
+
const reportedPercent = Number(context.remainingPercent ?? Math.round(remaining / total * 100));
|
|
194
|
+
const percent = Math.max(0, Math.min(100, Number.isFinite(reportedPercent) ? reportedPercent : 0));
|
|
195
|
+
const level = percent <= 20 ? ' danger' : percent <= 40 ? ' warn' : '';
|
|
196
|
+
const label = `${formatCodexTokens(remaining)} / ${formatCodexTokens(total)}(${Math.round(percent)}%)`;
|
|
197
|
+
return `<span class="codex-context-meter${level}" style="--context-remaining:${percent}%" title="${escapeHtml(`Context remaining: ${label}`)}" aria-label="${escapeHtml(`Context remaining: ${label}`)}">${escapeHtml(label)}</span>`;
|
|
198
|
+
}
|
|
199
|
+
function renderCodexMessageMeta(item, finalOnly = false, context = null) {
|
|
200
|
+
const time = renderCodexMessageTime(item, finalOnly);
|
|
201
|
+
const meter = renderCodexContextMeter(context);
|
|
202
|
+
return time || meter ? `<div class="codex-message-meta">${time}${meter}</div>` : '';
|
|
203
|
+
}
|
|
180
204
|
function syncCodexDom(current, next) {
|
|
181
205
|
if (!current || !next) return;
|
|
182
206
|
if (current.nodeType !== next.nodeType || current.nodeName !== next.nodeName) {
|
|
@@ -230,6 +254,12 @@
|
|
|
230
254
|
const usedPermissions = new Set();
|
|
231
255
|
const turnEndById = new Map(codexMessages.filter(item => item.kind === 'turn-end' && item.turnId)
|
|
232
256
|
.map(item => [String(item.turnId), item]));
|
|
257
|
+
const lastAssistantByTurn = new Map();
|
|
258
|
+
for (const item of codexMessages) {
|
|
259
|
+
if (item.kind === 'assistant' && item.turnId && !isCodexSubagentItem(item)) {
|
|
260
|
+
lastAssistantByTurn.set(String(item.turnId), item.id);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
233
263
|
const subagentItems = new Map();
|
|
234
264
|
for (const item of codexMessages) {
|
|
235
265
|
if (!isCodexSubagentItem(item)) continue;
|
|
@@ -258,8 +288,14 @@
|
|
|
258
288
|
turnEndById.get(String(turnId || ''))));
|
|
259
289
|
continue;
|
|
260
290
|
}
|
|
261
|
-
if (item.kind === 'assistant')
|
|
262
|
-
|
|
291
|
+
if (item.kind === 'assistant') {
|
|
292
|
+
const turnId = String(item.turnId || '');
|
|
293
|
+
const turnEnd = turnEndById.get(turnId);
|
|
294
|
+
const context = lastAssistantByTurn.get(turnId) === item.id ? turnEnd?.context : null;
|
|
295
|
+
parts.push(`<div class="codex-message-block assistant" data-codex-key="message-${escapeHtml(item.id || '')}"><div class="codex-message assistant claude-md">${renderMarkdown(item.text || '')}</div>${renderCodexMessageMeta(item, true, context)}</div>`);
|
|
296
|
+
}
|
|
297
|
+
else if (item.kind === 'user') parts.push(`<div class="codex-message-block user" data-codex-key="message-${escapeHtml(item.id || '')}"><div class="codex-message user claude-md">${renderMarkdown(item.text || '')}</div>${renderCodexMessageMeta(item)}</div>`);
|
|
298
|
+
else if (item.kind === 'compaction') parts.push(renderCodexCompaction(item));
|
|
263
299
|
else if (item.kind === 'status') parts.push(renderCodexStatus(item));
|
|
264
300
|
else parts.push(`<div class="codex-message ${escapeHtml(item.level === 'error' ? 'event error' : item.kind || 'event')}">${codexText(item.text)}</div>`);
|
|
265
301
|
i += 1;
|
|
@@ -300,6 +336,8 @@
|
|
|
300
336
|
if (modelButton) modelButton.textContent = 'Model';
|
|
301
337
|
const abort = document.getElementById('codex-abort-btn');
|
|
302
338
|
if (abort) abort.disabled = !codexState.canAbort;
|
|
339
|
+
const compact = document.getElementById('codex-compact-btn');
|
|
340
|
+
if (compact) { compact.disabled = !codexState.canCompact; compact.textContent = codexState.compacting ? 'Compacting' : 'Compact'; }
|
|
303
341
|
const fork = document.getElementById('codex-fork-btn');
|
|
304
342
|
if (fork) fork.disabled = !(codexState.presentation === 'structured' && codexState.status === 'idle');
|
|
305
343
|
const terminal = document.getElementById('codex-terminal-switch');
|
|
@@ -404,6 +442,12 @@
|
|
|
404
442
|
}
|
|
405
443
|
function abortCodexSession() { if (currentSocket?.readyState === 1) currentSocket.send(JSON.stringify({ type: 'codex-abort' })); }
|
|
406
444
|
function requestCodexStatus() { if (currentSocket?.readyState === 1) currentSocket.send(JSON.stringify({ type: 'codex-status' })); }
|
|
445
|
+
function compactCodexContext() {
|
|
446
|
+
if (!codexState.canCompact || currentSocket?.readyState !== 1) return false;
|
|
447
|
+
currentSocket.send(JSON.stringify({ type: 'codex-compact' }));
|
|
448
|
+
applyCodexState({ canCompact: false, compacting: true });
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
407
451
|
function respondCodexPermission(id, decision) { if (currentSocket?.readyState === 1) currentSocket.send(JSON.stringify({ type: 'codex-permission', id, decision })); }
|
|
408
452
|
async function toggleCodexResumePanel() {
|
|
409
453
|
codexResumePanelOpen = !codexResumePanelOpen;
|
package/lib/web/core.js
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
let claudeApprovalJumpIndex = 0;
|
|
33
33
|
let codexMessages = [];
|
|
34
34
|
let codexPendingPermissions = [];
|
|
35
|
-
let codexState = { permissionMode: 'default', sandboxMode: 'default', effectivePermissionMode: null, effectiveSandboxMode: null, model: null, effort: null, status: 'idle', threadId: null, presentation: 'structured', models: [], canAbort: false, canSwitchToTerminal: false, canSwitchToStructured: false };
|
|
35
|
+
let codexState = { permissionMode: 'default', sandboxMode: 'default', effectivePermissionMode: null, effectiveSandboxMode: null, model: null, effort: null, status: 'idle', threadId: null, presentation: 'structured', models: [], canAbort: false, canCompact: false, compacting: false, canSwitchToTerminal: false, canSwitchToStructured: false };
|
|
36
36
|
let codexModelPanelOpen = false;
|
|
37
37
|
let codexModelCandidate = null;
|
|
38
38
|
let codexResumePanelOpen = false;
|
package/lib/web/index.html
CHANGED
|
@@ -122,6 +122,7 @@
|
|
|
122
122
|
<button id="codex-fork-btn" class="claude-ctrl-btn primary" onclick="toggleCodexForkPanel()" title="Fork a Codex thread in this conversation">Fork</button>
|
|
123
123
|
</div>
|
|
124
124
|
<div class="codex-control-page">
|
|
125
|
+
<button id="codex-compact-btn" class="claude-ctrl-btn" onclick="compactCodexContext()" title="Compact the current Codex context">Compact</button>
|
|
125
126
|
<label class="codex-select-control" title="Sandbox mode">
|
|
126
127
|
<span class="codex-select-label" aria-hidden="true">Sandbox</span>
|
|
127
128
|
<select id="codex-sandbox-select" class="codex-select" aria-label="Sandbox mode" onchange="updateCodexSettingsFromControls()">
|
package/lib/web/session.js
CHANGED
|
@@ -134,7 +134,7 @@
|
|
|
134
134
|
document.getElementById('codex-resume-panel').classList.remove('active');
|
|
135
135
|
document.getElementById('codex-fork-panel').classList.remove('active');
|
|
136
136
|
document.getElementById('codex-control-rail').scrollLeft = 0;
|
|
137
|
-
codexState = { permissionMode: 'default', sandboxMode: 'default', effectivePermissionMode: null, effectiveSandboxMode: null, model: null, effort: null, status: 'idle', threadId: null, presentation: 'structured', models: [], canAbort: false, canSwitchToTerminal: false, canSwitchToStructured: false };
|
|
137
|
+
codexState = { permissionMode: 'default', sandboxMode: 'default', effectivePermissionMode: null, effectiveSandboxMode: null, model: null, effort: null, status: 'idle', threadId: null, presentation: 'structured', models: [], canAbort: false, canCompact: false, compacting: false, canSwitchToTerminal: false, canSwitchToStructured: false };
|
|
138
138
|
setClaudeModeEnabled(false);
|
|
139
139
|
applyCodexState(codexState);
|
|
140
140
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
package/lib/web/styles.css
CHANGED
|
@@ -93,8 +93,18 @@
|
|
|
93
93
|
.codex-message { max-width: 100%; margin: 0; color: #f5f5f7; font-size: 14px; line-height: 1.45; overflow-wrap: anywhere; }
|
|
94
94
|
.codex-message.user { padding: 8px 12px; border: 1px solid rgba(0,122,255,.32); border-radius: 12px; background: rgba(0,122,255,.24); }
|
|
95
95
|
.codex-message.event { color: var(--text-dim); text-align: center; font-size: 12px; }
|
|
96
|
-
.codex-message-
|
|
97
|
-
.codex-message-block.user .codex-message-
|
|
96
|
+
.codex-message-meta { display: flex; width: 100%; align-items: center; gap: 7px; margin-top: 4px; padding: 0 2px; box-sizing: border-box; }
|
|
97
|
+
.codex-message-block.user .codex-message-meta { justify-content: flex-end; }
|
|
98
|
+
.codex-message-time { display: block; flex: 0 0 auto; width: max-content; color: #8e8e93; font-size: 10px; font-weight: 500; line-height: 1; font-variant-numeric: tabular-nums; }
|
|
99
|
+
.codex-context-meter { --context-color: #30d158; position: relative; isolation: isolate; display: inline-flex; min-width: 128px; height: 16px; align-items: center; justify-content: center; overflow: hidden; border: 1px solid color-mix(in srgb, var(--context-color) 42%, transparent); border-radius: 8px; background: rgba(255,255,255,.055); color: #d9fbe2; font: 700 9px/1 ui-monospace, SFMono-Regular, Menlo, monospace; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
100
|
+
.codex-context-meter::before { content: ''; position: absolute; inset: 0 auto 0 0; z-index: -1; width: var(--context-remaining); background: color-mix(in srgb, var(--context-color) 28%, transparent); }
|
|
101
|
+
.codex-context-meter.warn { --context-color: #ffd60a; color: #fff3a6; }
|
|
102
|
+
.codex-context-meter.danger { --context-color: #ff453a; color: #ffd0cc; }
|
|
103
|
+
.codex-compaction-card { display: flex; align-items: center; gap: 10px; margin: 10px 0 14px; padding: 10px 12px; border: 1px solid rgba(191,90,242,.3); border-radius: 10px; background: linear-gradient(135deg, rgba(191,90,242,.13), rgba(94,92,230,.08)); color: #f5f5f7; }
|
|
104
|
+
.codex-compaction-icon { display: grid; width: 28px; height: 28px; flex: 0 0 28px; place-items: center; border-radius: 50%; background: rgba(191,90,242,.18); color: #d6a5ff; font-size: 15px; }
|
|
105
|
+
.codex-compaction-title { font-size: 12px; font-weight: 800; }
|
|
106
|
+
.codex-compaction-meta { margin-top: 3px; color: #a9a9b0; font-size: 10px; font-variant-numeric: tabular-nums; }
|
|
107
|
+
.codex-compaction-card.running .codex-compaction-icon { animation: codex-pulse 1.1s ease-in-out infinite alternate; }
|
|
98
108
|
.codex-status-card { margin: 10px 0 14px; border: 1px solid rgba(100,210,255,.2); border-radius: 10px; background: rgba(28,28,30,.72); padding: 11px; color: #f5f5f7; }
|
|
99
109
|
.codex-status-title { margin-bottom: 9px; color: #64d2ff; font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em; }
|
|
100
110
|
.codex-status-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px; }
|
|
@@ -151,6 +161,7 @@
|
|
|
151
161
|
.claude-inline-permission { margin-top: 10px; padding: 10px; border-top: 1px solid rgba(255,204,0,0.22); background: rgba(255,204,0,0.04); }
|
|
152
162
|
.claude-inline-permission-title { margin-bottom: 7px; color: #ffcc00; font-weight: 800; }
|
|
153
163
|
@keyframes codex-spin { to { transform: rotate(360deg); } }
|
|
164
|
+
@keyframes codex-pulse { to { box-shadow: 0 0 0 5px rgba(191,90,242,.08); transform: scale(.92); } }
|
|
154
165
|
#codex-control-panel { display: none; width: min(100%, var(--control-content-max)); margin: 0 auto; padding: 8px 14px 10px; border-bottom: 1px solid #222; background: #121212; box-sizing: border-box; }
|
|
155
166
|
.codex-control-rail { display: flex; gap: 14px; overflow-x: auto; overscroll-behavior-x: contain; scroll-snap-type: x mandatory; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
|
|
156
167
|
.codex-control-rail::-webkit-scrollbar { display: none; }
|