glad-web 1.0.30 → 1.0.32
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 +189 -22
- package/lib/commands/web.js +10 -3
- package/lib/server/routes/providers.js +13 -0
- package/lib/session/session-manager.js +12 -0
- package/lib/web/codex.js +141 -2
- package/lib/web/core.js +8 -1
- package/lib/web/index.html +3 -0
- package/lib/web/session.js +9 -1
- package/lib/web/styles.css +30 -4
- package/package.json +1 -1
|
@@ -85,6 +85,27 @@ function recentUserQuestions(thread, limit = 2) {
|
|
|
85
85
|
return questions;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
function userPromptsFromThread(thread, fallbackTimestamp = null) {
|
|
89
|
+
const prompts = [];
|
|
90
|
+
const threadId = String(thread?.id || '');
|
|
91
|
+
for (const turn of Array.isArray(thread?.turns) ? thread.turns : []) {
|
|
92
|
+
const turnTimestamp = toTimestampMs(turn.startedAt || turn.createdAt || turn.completedAt || turn.updatedAt)
|
|
93
|
+
|| fallbackTimestamp;
|
|
94
|
+
for (const item of Array.isArray(turn?.items) ? turn.items : []) {
|
|
95
|
+
if (item?.type !== 'userMessage') continue;
|
|
96
|
+
const prompt = (textFromInputItems(item.content) || item.text || '').trim();
|
|
97
|
+
if (!prompt) continue;
|
|
98
|
+
prompts.push({
|
|
99
|
+
id: String(item.id || `${threadId}:${turn.id || 'turn'}:${prompts.length}`),
|
|
100
|
+
threadId,
|
|
101
|
+
text: prompt,
|
|
102
|
+
createdAt: toTimestampMs(item.createdAt || item.updatedAt) || turnTimestamp || null
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return prompts;
|
|
107
|
+
}
|
|
108
|
+
|
|
88
109
|
function toolDetails(raw) {
|
|
89
110
|
if (raw.type === 'commandExecution') {
|
|
90
111
|
return {
|
|
@@ -167,8 +188,10 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
167
188
|
this.currentTurnId = null;
|
|
168
189
|
this.currentTurnStartedAt = null;
|
|
169
190
|
this.threadTurns = new Map();
|
|
191
|
+
this.turnContexts = new Map();
|
|
170
192
|
this.providerItemContexts = new Map();
|
|
171
193
|
this.tokenUsage = null;
|
|
194
|
+
this.compacting = false;
|
|
172
195
|
this.permissionMode = normalizePermissionMode(options.permissionMode);
|
|
173
196
|
this.sandboxMode = normalizeSandboxMode(options.sandboxMode);
|
|
174
197
|
this.effectivePermissionMode = null;
|
|
@@ -193,6 +216,8 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
193
216
|
this.inputSeq = 0;
|
|
194
217
|
this.completionReadInputSeq = 0;
|
|
195
218
|
this.timedInputs = new Map();
|
|
219
|
+
this.promptHistoryCache = null;
|
|
220
|
+
this.deferredWarnings = null;
|
|
196
221
|
this.ptyManager = {
|
|
197
222
|
workingDir,
|
|
198
223
|
isRunning: () => this.isRunning(),
|
|
@@ -226,6 +251,8 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
226
251
|
model: this.model, effort: this.effort,
|
|
227
252
|
status: this.status, threadId: this.threadId, presentation: this.presentation,
|
|
228
253
|
canAbort: this.presentation === 'structured' && this.status !== 'idle',
|
|
254
|
+
canCompact: this.presentation === 'structured' && this.status === 'idle' && !this.compacting && Boolean(this.threadId),
|
|
255
|
+
compacting: this.compacting,
|
|
229
256
|
canSwitchToTerminal: this.presentation === 'structured' && this.status === 'idle' && Boolean(this.threadId),
|
|
230
257
|
canSwitchToStructured: this.presentation === 'terminal',
|
|
231
258
|
pendingPermissionCount: this.pendingPermissions.size, activeSubagentCount, models: this.models };
|
|
@@ -289,6 +316,7 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
289
316
|
for (const request of this.pendingRequests.values()) request.reject(new Error(`Codex app-server exited (${code})`));
|
|
290
317
|
this.pendingRequests.clear();
|
|
291
318
|
if (this.running && this.presentation === 'structured') {
|
|
319
|
+
this.compacting = false;
|
|
292
320
|
this.append({ kind: 'event', level: 'error', text: 'Codex app-server exited.' });
|
|
293
321
|
this.setStatus('idle');
|
|
294
322
|
}
|
|
@@ -382,6 +410,7 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
382
410
|
handleNotification(method, params) {
|
|
383
411
|
if (method === 'thread/tokenUsage/updated') {
|
|
384
412
|
this.tokenUsage = params.tokenUsage || params.usage || params;
|
|
413
|
+
this.recordTurnContext(params.turnId, this.tokenUsage);
|
|
385
414
|
return;
|
|
386
415
|
}
|
|
387
416
|
if (method === 'turn/started') {
|
|
@@ -411,8 +440,9 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
411
440
|
const startedAtMs = trackedTurn?.startedAt || ((!threadId || threadId === this.threadId) ? this.currentTurnStartedAt : null);
|
|
412
441
|
const durationMs = Number(params.turn?.durationMs || 0)
|
|
413
442
|
|| (startedAtMs ? Math.max(0, completedAtMs - startedAtMs) : null);
|
|
443
|
+
const context = this.turnContexts.get(String(completedTurnId || ''));
|
|
414
444
|
this.append({ kind: 'turn-end', threadId, turnId: completedTurnId, status: turnStatus,
|
|
415
|
-
durationMs, createdAt: completedAtMs });
|
|
445
|
+
durationMs, createdAt: completedAtMs, ...(context ? { context } : {}) });
|
|
416
446
|
const observedNow = Date.now();
|
|
417
447
|
const observedCompletedAtMs = Math.abs(observedNow - completedAtMs) < 5000
|
|
418
448
|
? Math.max(completedAtMs, observedNow) : completedAtMs;
|
|
@@ -424,8 +454,13 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
424
454
|
this.patch(item.id, { toolStatus,
|
|
425
455
|
completedAtMs: observedCompletedAtMs, ...(toolDurationMs != null ? { durationMs: toolDurationMs } : {}) });
|
|
426
456
|
}
|
|
457
|
+
for (const item of this.messages.filter(message => message.kind === 'compaction'
|
|
458
|
+
&& message.turnId === completedTurnId && message.compactionStatus === 'running')) {
|
|
459
|
+
this.patch(item.id, { compactionStatus: 'completed', completedAtMs: observedCompletedAtMs });
|
|
460
|
+
}
|
|
427
461
|
if (threadId) this.threadTurns.delete(threadId);
|
|
428
462
|
if (!threadId || threadId === this.threadId) {
|
|
463
|
+
this.compacting = false;
|
|
429
464
|
for (const pending of this.pendingPermissions.values()) {
|
|
430
465
|
this.recordPermission(pending.public, 'denied', 'abort');
|
|
431
466
|
}
|
|
@@ -442,6 +477,14 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
442
477
|
}
|
|
443
478
|
return;
|
|
444
479
|
}
|
|
480
|
+
if (method === 'thread/compacted') {
|
|
481
|
+
const threadId = params.threadId || this.threadId;
|
|
482
|
+
const turnId = params.turnId || (threadId ? this.threadTurns.get(threadId)?.turnId : null) || this.currentTurnId;
|
|
483
|
+
this.applyProviderItem({ id: `compaction-${turnId || Date.now()}`, type: 'contextCompaction', threadId, turnId }, 'completed', {
|
|
484
|
+
threadId, turnId, completedAtMs: Date.now()
|
|
485
|
+
});
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
445
488
|
if (method === 'thread/started' || method === 'thread/resumed') {
|
|
446
489
|
const threadId = params.thread?.id || params.threadId;
|
|
447
490
|
if (threadId && !this.threadId) { this.threadId = threadId; this.emitControlState(); }
|
|
@@ -451,7 +494,7 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
451
494
|
const threadId = params.threadId || this.threadId;
|
|
452
495
|
const status = params.status?.type || params.status;
|
|
453
496
|
if (!threadId || threadId === this.threadId) {
|
|
454
|
-
if (status === 'idle' && !this.currentTurnId) this.setStatus('idle');
|
|
497
|
+
if (status === 'idle' && !this.currentTurnId) { this.compacting = false; this.setStatus('idle'); }
|
|
455
498
|
if (status === 'active') this.setStatus('running');
|
|
456
499
|
}
|
|
457
500
|
return;
|
|
@@ -467,11 +510,13 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
467
510
|
}
|
|
468
511
|
if (method === 'error') {
|
|
469
512
|
this.append({ kind: 'event', level: 'error', text: params.error?.message || 'Codex reported an error.' });
|
|
470
|
-
if (!params.willRetry) this.setStatus('idle');
|
|
513
|
+
if (!params.willRetry) { this.compacting = false; this.setStatus('idle'); }
|
|
471
514
|
return;
|
|
472
515
|
}
|
|
473
516
|
if (method === 'warning' || method === 'guardianWarning') {
|
|
474
|
-
|
|
517
|
+
const warning = { kind: 'event', level: 'warning', text: params.message || params.warning || 'Codex warning.' };
|
|
518
|
+
if (this.deferredWarnings) this.deferredWarnings.push(warning);
|
|
519
|
+
else this.append(warning);
|
|
475
520
|
return;
|
|
476
521
|
}
|
|
477
522
|
if (method === 'item/commandExecution/outputDelta' || method === 'item/fileChange/outputDelta') {
|
|
@@ -519,9 +564,10 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
519
564
|
applyProviderItem(raw, inferredStatus = null, context = {}) {
|
|
520
565
|
if (!raw || typeof raw !== 'object') return;
|
|
521
566
|
const providerId = String(raw.id || '');
|
|
522
|
-
|
|
567
|
+
let existing = providerId && this.messages.find(item => item.providerId === providerId);
|
|
523
568
|
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'
|
|
569
|
+
: ['commandExecution', 'fileChange', 'mcpToolCall', 'dynamicToolCall', 'webSearch', 'collabAgentToolCall'].includes(raw.type) ? 'tool'
|
|
570
|
+
: raw.type === 'contextCompaction' ? 'compaction' : null;
|
|
525
571
|
if (!kind) return;
|
|
526
572
|
const text = kind === 'user' ? textFromInputItems(raw.content) : kind === 'assistant' ? String(raw.text || '')
|
|
527
573
|
: kind === 'reasoning' ? (raw.text || (Array.isArray(raw.summary) ? raw.summary.join('\n') : Array.isArray(raw.content) ? raw.content.join('\n') : '')) : '';
|
|
@@ -530,6 +576,9 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
530
576
|
const threadId = raw.threadId || context.threadId || null;
|
|
531
577
|
const trackedTurnId = threadId ? this.threadTurns.get(threadId)?.turnId : null;
|
|
532
578
|
const turnId = raw.turnId || context.turnId || trackedTurnId || this.currentTurnId;
|
|
579
|
+
if (!existing && kind === 'compaction' && turnId) {
|
|
580
|
+
existing = this.messages.find(item => item.kind === 'compaction' && item.turnId === turnId);
|
|
581
|
+
}
|
|
533
582
|
if (providerId && (threadId || turnId)) this.providerItemContexts.set(providerId, { threadId, turnId });
|
|
534
583
|
const existingStartedAtMs = existing?.startedAtMs || existing?.createdAt || null;
|
|
535
584
|
const startedAtMs = Number(context.startedAtMs || raw.startedAtMs || 0)
|
|
@@ -546,6 +595,8 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
546
595
|
};
|
|
547
596
|
const patch = kind === 'tool' ? { ...toolDetails(raw), threadId, turnId,
|
|
548
597
|
...timing, toolStatus: inferredToolStatus || raw.status || 'running' }
|
|
598
|
+
: kind === 'compaction' ? { providerId, threadId, turnId, ...timing,
|
|
599
|
+
compactionStatus: inferredStatus || raw.status || 'running' }
|
|
549
600
|
: { text, threadId, turnId, streaming: false, ...(completedAtMs ? { completedAtMs } : {}) };
|
|
550
601
|
if (existing) {
|
|
551
602
|
this.patch(existing.id, patch);
|
|
@@ -556,6 +607,10 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
556
607
|
} else {
|
|
557
608
|
this.append({ kind, providerId, ...(startedAtMs ? { createdAt: startedAtMs } : {}), ...patch });
|
|
558
609
|
}
|
|
610
|
+
if (kind === 'compaction' && (!threadId || threadId === this.threadId)) {
|
|
611
|
+
this.compacting = patch.compactionStatus === 'running';
|
|
612
|
+
this.emitControlState();
|
|
613
|
+
}
|
|
559
614
|
}
|
|
560
615
|
|
|
561
616
|
async refreshModels() {
|
|
@@ -625,8 +680,70 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
625
680
|
return items;
|
|
626
681
|
}
|
|
627
682
|
|
|
628
|
-
|
|
629
|
-
|
|
683
|
+
async listPromptHistory({ offset = 0, limit = 30 } = {}) {
|
|
684
|
+
await this.ensureProcess();
|
|
685
|
+
const safeOffset = Math.max(0, Math.min(199, Number(offset) || 0));
|
|
686
|
+
const safeLimit = Math.max(1, Math.min(30, Number(limit) || 30));
|
|
687
|
+
const cacheFresh = this.promptHistoryCache
|
|
688
|
+
&& Date.now() - this.promptHistoryCache.loadedAt < 15000;
|
|
689
|
+
|
|
690
|
+
if (!cacheFresh) {
|
|
691
|
+
const prompts = [];
|
|
692
|
+
let cursor = null;
|
|
693
|
+
let pageCount = 0;
|
|
694
|
+
let capped = false;
|
|
695
|
+
do {
|
|
696
|
+
const result = await this.request('thread/list', {
|
|
697
|
+
cursor,
|
|
698
|
+
limit: 20,
|
|
699
|
+
sortKey: 'updated_at',
|
|
700
|
+
sortDirection: 'desc',
|
|
701
|
+
archived: false,
|
|
702
|
+
cwd: this.workingDir
|
|
703
|
+
});
|
|
704
|
+
const threads = (result?.data || []).filter(item => !item.parentThreadId);
|
|
705
|
+
const histories = await Promise.all(threads.map(async item => {
|
|
706
|
+
try {
|
|
707
|
+
const history = await this.request('thread/read', { threadId: item.id, includeTurns: true });
|
|
708
|
+
const fallbackTimestamp = toTimestampMs(item.updatedAt || item.createdAt);
|
|
709
|
+
return userPromptsFromThread(history?.thread || { id: item.id, turns: [] }, fallbackTimestamp)
|
|
710
|
+
.map(prompt => ({ ...prompt, threadId: prompt.threadId || item.id }));
|
|
711
|
+
} catch (error) {
|
|
712
|
+
this.logger.debugInfo?.(`[codex-app-server] unable to read prompt history for ${item.id}: ${error.message}`);
|
|
713
|
+
return [];
|
|
714
|
+
}
|
|
715
|
+
}));
|
|
716
|
+
prompts.push(...histories.flat());
|
|
717
|
+
cursor = result?.nextCursor || null;
|
|
718
|
+
pageCount += 1;
|
|
719
|
+
if (prompts.length >= 200 || pageCount >= 5) {
|
|
720
|
+
capped = Boolean(cursor) || prompts.length > 200;
|
|
721
|
+
break;
|
|
722
|
+
}
|
|
723
|
+
} while (cursor);
|
|
724
|
+
|
|
725
|
+
prompts.sort((a, b) => Number(b.createdAt || 0) - Number(a.createdAt || 0));
|
|
726
|
+
this.promptHistoryCache = {
|
|
727
|
+
loadedAt: Date.now(),
|
|
728
|
+
items: prompts.slice(0, 200),
|
|
729
|
+
capped
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const items = this.promptHistoryCache.items.slice(safeOffset, safeOffset + safeLimit);
|
|
734
|
+
const nextOffset = safeOffset + items.length;
|
|
735
|
+
return {
|
|
736
|
+
items,
|
|
737
|
+
offset: safeOffset,
|
|
738
|
+
nextOffset,
|
|
739
|
+
total: this.promptHistoryCache.items.length,
|
|
740
|
+
hasMore: nextOffset < this.promptHistoryCache.items.length,
|
|
741
|
+
capped: this.promptHistoryCache.capped
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
contextStatus(tokenUsage = this.tokenUsage) {
|
|
746
|
+
const usage = tokenUsage || {};
|
|
630
747
|
const selectedModel = this.models.find(item => item.id === this.model);
|
|
631
748
|
const contextWindow = Number(usage.modelContextWindow || usage.model_context_window
|
|
632
749
|
|| usage.contextWindow || usage.context_window || selectedModel?.contextWindow || 0);
|
|
@@ -646,6 +763,16 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
646
763
|
};
|
|
647
764
|
}
|
|
648
765
|
|
|
766
|
+
recordTurnContext(turnId, tokenUsage = this.tokenUsage) {
|
|
767
|
+
const id = String(turnId || '').trim();
|
|
768
|
+
const context = this.contextStatus(tokenUsage);
|
|
769
|
+
if (!id || !context) return context;
|
|
770
|
+
this.turnContexts.set(id, context);
|
|
771
|
+
const turnEnd = this.messages.find(item => item.kind === 'turn-end' && String(item.turnId || '') === id);
|
|
772
|
+
if (turnEnd) this.patch(turnEnd.id, { context });
|
|
773
|
+
return context;
|
|
774
|
+
}
|
|
775
|
+
|
|
649
776
|
async showStatus() {
|
|
650
777
|
if (this.presentation !== 'structured') return false;
|
|
651
778
|
await this.ensureProcess();
|
|
@@ -717,6 +844,7 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
717
844
|
.filter(item => item && typeof item.path === 'string' && item.path);
|
|
718
845
|
if ((!prompt && images.length === 0) || this.presentation !== 'structured' || this.status !== 'idle') return false;
|
|
719
846
|
this.hasUnreadCompletion = false;
|
|
847
|
+
this.promptHistoryCache = null;
|
|
720
848
|
this.append({
|
|
721
849
|
kind: 'user',
|
|
722
850
|
text: prompt || '📷 Image attachment',
|
|
@@ -759,6 +887,22 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
759
887
|
}
|
|
760
888
|
}
|
|
761
889
|
|
|
890
|
+
async compactContext() {
|
|
891
|
+
if (!this.threadId || this.presentation !== 'structured' || this.status !== 'idle') return false;
|
|
892
|
+
await this.ensureProcess();
|
|
893
|
+
this.compacting = true;
|
|
894
|
+
this.setStatus('running');
|
|
895
|
+
try {
|
|
896
|
+
await this.request('thread/compact/start', { threadId: this.threadId });
|
|
897
|
+
return true;
|
|
898
|
+
} catch (error) {
|
|
899
|
+
this.compacting = false;
|
|
900
|
+
this.setStatus('idle');
|
|
901
|
+
this.append({ kind: 'event', level: 'error', text: `Unable to compact context: ${error.message}` });
|
|
902
|
+
throw error;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
762
906
|
write(data) {
|
|
763
907
|
if (this.presentation === 'terminal') return this.terminalSession?.write(data) || false;
|
|
764
908
|
const text = String(data || '').replace(/\r/g, '\n');
|
|
@@ -826,28 +970,50 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
826
970
|
const target = String(threadId || this.threadId || '').trim();
|
|
827
971
|
if (!target || this.presentation !== 'structured' || this.status !== 'idle') return false;
|
|
828
972
|
await this.ensureProcess();
|
|
973
|
+
const selectedModel = this.model;
|
|
974
|
+
const selectedEffort = this.effort;
|
|
975
|
+
const resumeWithModelOverride = Boolean(this.hasModelOverride && selectedModel);
|
|
976
|
+
const resumeWithEffortOverride = Boolean(this.hasEffortOverride && selectedEffort);
|
|
829
977
|
const params = { threadId: target, cwd: this.workingDir };
|
|
830
978
|
if (this.permissionMode) params.approvalPolicy = this.permissionMode;
|
|
831
979
|
if (this.sandboxMode) params.sandbox = this.sandboxMode;
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
this.
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
980
|
+
if (resumeWithModelOverride) params.model = selectedModel;
|
|
981
|
+
if (resumeWithEffortOverride) params.config = { model_reasoning_effort: selectedEffort };
|
|
982
|
+
this.deferredWarnings = [];
|
|
983
|
+
try {
|
|
984
|
+
const result = await this.request('thread/resume', params);
|
|
985
|
+
this.threadId = result.thread?.id || target;
|
|
986
|
+
this.hasModelOverride = resumeWithModelOverride;
|
|
987
|
+
this.hasEffortOverride = resumeWithEffortOverride;
|
|
988
|
+
this.model = result.model || result.thread?.model || selectedModel;
|
|
989
|
+
this.effort = result.reasoningEffort || result.thread?.reasoningEffort || selectedEffort;
|
|
990
|
+
this.effectivePermissionMode = result.approvalPolicy || this.effectivePermissionMode;
|
|
991
|
+
this.effectiveSandboxMode = sandboxModeFromPolicy(result.sandbox) || this.effectiveSandboxMode;
|
|
992
|
+
const history = await this.request('thread/read', { threadId: this.threadId, includeTurns: true });
|
|
993
|
+
this.restoreThreadHistory(history?.thread, `Resumed Codex thread ${this.threadId}`, {
|
|
994
|
+
preserveModel: resumeWithModelOverride,
|
|
995
|
+
preserveEffort: resumeWithEffortOverride
|
|
996
|
+
});
|
|
997
|
+
const warnings = this.deferredWarnings;
|
|
998
|
+
this.deferredWarnings = null;
|
|
999
|
+
for (const warning of warnings) this.append(warning);
|
|
1000
|
+
this.promptHistoryCache = null;
|
|
1001
|
+
return true;
|
|
1002
|
+
} catch (error) {
|
|
1003
|
+
const warnings = this.deferredWarnings || [];
|
|
1004
|
+
this.deferredWarnings = null;
|
|
1005
|
+
for (const warning of warnings) this.append(warning);
|
|
1006
|
+
throw error;
|
|
1007
|
+
}
|
|
843
1008
|
}
|
|
844
1009
|
|
|
845
|
-
restoreThreadHistory(thread, eventText = '') {
|
|
846
|
-
this.model = thread?.model || this.model;
|
|
847
|
-
this.effort = thread?.reasoningEffort || thread?.reasoning_effort || this.effort;
|
|
1010
|
+
restoreThreadHistory(thread, eventText = '', options = {}) {
|
|
1011
|
+
if (!options.preserveModel) this.model = thread?.model || this.model;
|
|
1012
|
+
if (!options.preserveEffort) this.effort = thread?.reasoningEffort || thread?.reasoning_effort || this.effort;
|
|
848
1013
|
this.tokenUsage = thread?.tokenUsage || thread?.token_usage || this.tokenUsage;
|
|
849
1014
|
this.messages = [];
|
|
850
1015
|
this.completedPermissions = [];
|
|
1016
|
+
this.turnContexts.clear();
|
|
851
1017
|
this.providerItemContexts.clear();
|
|
852
1018
|
for (const turn of thread?.turns || []) {
|
|
853
1019
|
const status = turn.status === 'failed' ? 'failed' : turn.status === 'interrupted' ? 'cancelled' : 'completed';
|
|
@@ -891,6 +1057,7 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
891
1057
|
this.effectivePermissionMode = result.approvalPolicy || this.effectivePermissionMode;
|
|
892
1058
|
this.effectiveSandboxMode = sandboxModeFromPolicy(result.sandbox) || this.effectiveSandboxMode;
|
|
893
1059
|
this.restoreThreadHistory(forkedThread, `Forked from Codex thread ${sourceThreadId}`);
|
|
1060
|
+
this.promptHistoryCache = null;
|
|
894
1061
|
return { threadId: this.threadId };
|
|
895
1062
|
}
|
|
896
1063
|
|
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
|
|
|
@@ -55,6 +55,19 @@ function registerProviderRoutes(app, { sessionManager }) {
|
|
|
55
55
|
}
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
+
app.get('/api/sessions/:id/codex-prompts', async (req, res) => {
|
|
59
|
+
try {
|
|
60
|
+
const result = await sessionManager.listCodexPrompts(req.params.id, {
|
|
61
|
+
offset: req.query?.offset,
|
|
62
|
+
limit: req.query?.limit
|
|
63
|
+
});
|
|
64
|
+
if (!result) return res.status(404).json({ error: 'Codex session not found' });
|
|
65
|
+
res.json({ success: true, ...result });
|
|
66
|
+
} catch (error) {
|
|
67
|
+
res.status(400).json({ error: error.message });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
58
71
|
app.post('/api/sessions/:id/codex-abort', (req, res) => {
|
|
59
72
|
const success = sessionManager.abortCodex(req.params.id);
|
|
60
73
|
if (!success) return res.status(409).json({ error: 'Codex session is idle or unavailable' });
|
|
@@ -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;
|
|
@@ -425,6 +431,12 @@ class SessionManager extends EventEmitter {
|
|
|
425
431
|
return session.listResumeThreads();
|
|
426
432
|
}
|
|
427
433
|
|
|
434
|
+
listCodexPrompts(id, options) {
|
|
435
|
+
const session = this.get(id);
|
|
436
|
+
if (!session || session.kind !== 'codex-structured') return null;
|
|
437
|
+
return session.listPromptHistory(options);
|
|
438
|
+
}
|
|
439
|
+
|
|
428
440
|
switchCodexPresentation(id, presentation) {
|
|
429
441
|
const session = this.get(id);
|
|
430
442
|
if (!session || session.kind !== 'codex-structured') return Promise.resolve(false);
|
package/lib/web/codex.js
CHANGED
|
@@ -94,6 +94,18 @@
|
|
|
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
|
+
}
|
|
106
|
+
function renderCodexWarning(item) {
|
|
107
|
+
return `<div class="codex-warning-card" data-codex-key="warning-${escapeHtml(item.id || '')}" role="status"><span class="codex-warning-icon" aria-hidden="true">!</span><div><div class="codex-warning-title">Codex warning</div><div class="codex-warning-text">${codexText(item.text || 'Codex reported a warning.')}</div></div></div>`;
|
|
108
|
+
}
|
|
97
109
|
function renderCodexTool(item, permission = null) {
|
|
98
110
|
const status = codexToolStatus(item);
|
|
99
111
|
const isError = status === 'failed' || Boolean(item.error) || (item.exitCode != null && item.exitCode !== 0);
|
|
@@ -177,6 +189,21 @@
|
|
|
177
189
|
const label = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
178
190
|
return `<time class="codex-message-time" datetime="${escapeHtml(date.toISOString())}" title="${escapeHtml(date.toLocaleString())}">${escapeHtml(label)}</time>`;
|
|
179
191
|
}
|
|
192
|
+
function renderCodexContextMeter(context) {
|
|
193
|
+
const total = Number(context?.contextWindow || 0);
|
|
194
|
+
const remaining = Number(context?.remainingTokens || 0);
|
|
195
|
+
if (!(total > 0) || !Number.isFinite(remaining)) return '';
|
|
196
|
+
const reportedPercent = Number(context.remainingPercent ?? Math.round(remaining / total * 100));
|
|
197
|
+
const percent = Math.max(0, Math.min(100, Number.isFinite(reportedPercent) ? reportedPercent : 0));
|
|
198
|
+
const level = percent <= 20 ? ' danger' : percent <= 40 ? ' warn' : '';
|
|
199
|
+
const label = `${formatCodexTokens(remaining)} / ${formatCodexTokens(total)}(${Math.round(percent)}%)`;
|
|
200
|
+
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>`;
|
|
201
|
+
}
|
|
202
|
+
function renderCodexMessageMeta(item, finalOnly = false, context = null) {
|
|
203
|
+
const time = renderCodexMessageTime(item, finalOnly);
|
|
204
|
+
const meter = renderCodexContextMeter(context);
|
|
205
|
+
return time || meter ? `<div class="codex-message-meta">${time}${meter}</div>` : '';
|
|
206
|
+
}
|
|
180
207
|
function syncCodexDom(current, next) {
|
|
181
208
|
if (!current || !next) return;
|
|
182
209
|
if (current.nodeType !== next.nodeType || current.nodeName !== next.nodeName) {
|
|
@@ -230,6 +257,12 @@
|
|
|
230
257
|
const usedPermissions = new Set();
|
|
231
258
|
const turnEndById = new Map(codexMessages.filter(item => item.kind === 'turn-end' && item.turnId)
|
|
232
259
|
.map(item => [String(item.turnId), item]));
|
|
260
|
+
const lastAssistantByTurn = new Map();
|
|
261
|
+
for (const item of codexMessages) {
|
|
262
|
+
if (item.kind === 'assistant' && item.turnId && !isCodexSubagentItem(item)) {
|
|
263
|
+
lastAssistantByTurn.set(String(item.turnId), item.id);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
233
266
|
const subagentItems = new Map();
|
|
234
267
|
for (const item of codexMessages) {
|
|
235
268
|
if (!isCodexSubagentItem(item)) continue;
|
|
@@ -258,9 +291,16 @@
|
|
|
258
291
|
turnEndById.get(String(turnId || ''))));
|
|
259
292
|
continue;
|
|
260
293
|
}
|
|
261
|
-
if (item.kind === 'assistant')
|
|
262
|
-
|
|
294
|
+
if (item.kind === 'assistant') {
|
|
295
|
+
const turnId = String(item.turnId || '');
|
|
296
|
+
const turnEnd = turnEndById.get(turnId);
|
|
297
|
+
const context = lastAssistantByTurn.get(turnId) === item.id ? turnEnd?.context : null;
|
|
298
|
+
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>`);
|
|
299
|
+
}
|
|
300
|
+
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>`);
|
|
301
|
+
else if (item.kind === 'compaction') parts.push(renderCodexCompaction(item));
|
|
263
302
|
else if (item.kind === 'status') parts.push(renderCodexStatus(item));
|
|
303
|
+
else if (item.kind === 'event' && item.level === 'warning') parts.push(renderCodexWarning(item));
|
|
264
304
|
else parts.push(`<div class="codex-message ${escapeHtml(item.level === 'error' ? 'event error' : item.kind || 'event')}">${codexText(item.text)}</div>`);
|
|
265
305
|
i += 1;
|
|
266
306
|
}
|
|
@@ -300,6 +340,8 @@
|
|
|
300
340
|
if (modelButton) modelButton.textContent = 'Model';
|
|
301
341
|
const abort = document.getElementById('codex-abort-btn');
|
|
302
342
|
if (abort) abort.disabled = !codexState.canAbort;
|
|
343
|
+
const compact = document.getElementById('codex-compact-btn');
|
|
344
|
+
if (compact) { compact.disabled = !codexState.canCompact; compact.textContent = codexState.compacting ? 'Compacting' : 'Compact'; }
|
|
303
345
|
const fork = document.getElementById('codex-fork-btn');
|
|
304
346
|
if (fork) fork.disabled = !(codexState.presentation === 'structured' && codexState.status === 'idle');
|
|
305
347
|
const terminal = document.getElementById('codex-terminal-switch');
|
|
@@ -357,9 +399,11 @@
|
|
|
357
399
|
codexModelPanelOpen = !codexModelPanelOpen;
|
|
358
400
|
codexResumePanelOpen = false;
|
|
359
401
|
codexForkPanelOpen = false;
|
|
402
|
+
codexPromptPanelOpen = false;
|
|
360
403
|
codexModelCandidate = codexState.model || codexState.models?.[0]?.id || null;
|
|
361
404
|
document.getElementById('codex-resume-panel').classList.remove('active');
|
|
362
405
|
document.getElementById('codex-fork-panel').classList.remove('active');
|
|
406
|
+
document.getElementById('codex-prompt-panel').classList.remove('active');
|
|
363
407
|
renderCodexModelPanel();
|
|
364
408
|
updateTerminalControlsHeight();
|
|
365
409
|
}
|
|
@@ -404,13 +448,21 @@
|
|
|
404
448
|
}
|
|
405
449
|
function abortCodexSession() { if (currentSocket?.readyState === 1) currentSocket.send(JSON.stringify({ type: 'codex-abort' })); }
|
|
406
450
|
function requestCodexStatus() { if (currentSocket?.readyState === 1) currentSocket.send(JSON.stringify({ type: 'codex-status' })); }
|
|
451
|
+
function compactCodexContext() {
|
|
452
|
+
if (!codexState.canCompact || currentSocket?.readyState !== 1) return false;
|
|
453
|
+
currentSocket.send(JSON.stringify({ type: 'codex-compact' }));
|
|
454
|
+
applyCodexState({ canCompact: false, compacting: true });
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
407
457
|
function respondCodexPermission(id, decision) { if (currentSocket?.readyState === 1) currentSocket.send(JSON.stringify({ type: 'codex-permission', id, decision })); }
|
|
408
458
|
async function toggleCodexResumePanel() {
|
|
409
459
|
codexResumePanelOpen = !codexResumePanelOpen;
|
|
410
460
|
codexModelPanelOpen = false;
|
|
411
461
|
codexForkPanelOpen = false;
|
|
462
|
+
codexPromptPanelOpen = false;
|
|
412
463
|
document.getElementById('codex-model-panel').classList.remove('active');
|
|
413
464
|
document.getElementById('codex-fork-panel').classList.remove('active');
|
|
465
|
+
document.getElementById('codex-prompt-panel').classList.remove('active');
|
|
414
466
|
const panel = document.getElementById('codex-resume-panel');
|
|
415
467
|
panel.classList.toggle('active', codexResumePanelOpen);
|
|
416
468
|
updateTerminalControlsHeight();
|
|
@@ -422,8 +474,10 @@
|
|
|
422
474
|
codexForkPanelOpen = !codexForkPanelOpen;
|
|
423
475
|
codexModelPanelOpen = false;
|
|
424
476
|
codexResumePanelOpen = false;
|
|
477
|
+
codexPromptPanelOpen = false;
|
|
425
478
|
document.getElementById('codex-model-panel').classList.remove('active');
|
|
426
479
|
document.getElementById('codex-resume-panel').classList.remove('active');
|
|
480
|
+
document.getElementById('codex-prompt-panel').classList.remove('active');
|
|
427
481
|
const panel = document.getElementById('codex-fork-panel');
|
|
428
482
|
panel.classList.toggle('active', codexForkPanelOpen);
|
|
429
483
|
updateTerminalControlsHeight();
|
|
@@ -445,6 +499,91 @@
|
|
|
445
499
|
} catch (e) { panel.innerHTML = `<div class="claude-resume-meta" style="padding:12px;color:#ff6b61;">${escapeHtml(e.message)}</div>`; }
|
|
446
500
|
updateTerminalControlsHeight();
|
|
447
501
|
}
|
|
502
|
+
function formatCodexPromptTime(timestamp) {
|
|
503
|
+
const value = Number(timestamp || 0);
|
|
504
|
+
if (!value) return '';
|
|
505
|
+
const date = new Date(value);
|
|
506
|
+
return Number.isNaN(date.getTime()) ? '' : date.toLocaleString([], {
|
|
507
|
+
month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
function renderCodexPromptPanel(error = '') {
|
|
511
|
+
const panel = document.getElementById('codex-prompt-panel');
|
|
512
|
+
if (!panel) return;
|
|
513
|
+
const previousScrollTop = panel.scrollTop;
|
|
514
|
+
panel.classList.toggle('active', codexPromptPanelOpen);
|
|
515
|
+
if (!codexPromptPanelOpen) return;
|
|
516
|
+
const countLabel = codexPromptTotal ? `${codexPromptItems.length} / ${codexPromptTotal}${codexPromptTotal >= 200 ? ' max' : ''}` : '';
|
|
517
|
+
const items = codexPromptItems.map((item, index) => {
|
|
518
|
+
const expanded = codexExpandedPrompts.has(index);
|
|
519
|
+
return `<div class="codex-prompt-item"><button type="button" class="codex-prompt-text${expanded ? ' expanded' : ''}" onclick="toggleCodexPromptExpanded(${index})" title="${expanded ? 'Collapse prompt' : 'Expand prompt'}">${escapeHtml(item.text || '')}</button><div class="codex-prompt-actions"><span class="codex-prompt-time">${escapeHtml(formatCodexPromptTime(item.createdAt))}</span><button type="button" class="small-btn codex-prompt-copy" data-codex-prompt-copy="${index}" onclick="copyCodexPrompt(${index})">Copy</button></div></div>`;
|
|
520
|
+
}).join('');
|
|
521
|
+
const empty = !items && !codexPromptLoading && !error
|
|
522
|
+
? '<div class="claude-resume-meta" style="padding:12px;">No text prompts found for this folder.</div>' : '';
|
|
523
|
+
const footer = codexPromptHasMore || codexPromptLoading
|
|
524
|
+
? `<div class="codex-prompt-footer"><button type="button" class="small-btn primary" onclick="loadMoreCodexPrompts()"${codexPromptLoading ? ' disabled' : ''}>${codexPromptLoading ? 'Loading…' : 'Load more'}</button></div>` : '';
|
|
525
|
+
panel.innerHTML = `<div class="codex-prompt-header"><span>Prompt history</span><span>${escapeHtml(countLabel)}</span></div>${error ? `<div class="claude-resume-meta" style="padding:12px;color:#ff6b61;">${escapeHtml(error)}</div>` : ''}${items}${empty}${footer}`;
|
|
526
|
+
panel.scrollTop = previousScrollTop;
|
|
527
|
+
updateTerminalControlsHeight();
|
|
528
|
+
}
|
|
529
|
+
async function toggleCodexPromptPanel() {
|
|
530
|
+
codexPromptPanelOpen = !codexPromptPanelOpen;
|
|
531
|
+
codexModelPanelOpen = false;
|
|
532
|
+
codexResumePanelOpen = false;
|
|
533
|
+
codexForkPanelOpen = false;
|
|
534
|
+
document.getElementById('codex-model-panel').classList.remove('active');
|
|
535
|
+
document.getElementById('codex-resume-panel').classList.remove('active');
|
|
536
|
+
document.getElementById('codex-fork-panel').classList.remove('active');
|
|
537
|
+
if (!codexPromptPanelOpen) {
|
|
538
|
+
document.getElementById('codex-prompt-panel').classList.remove('active');
|
|
539
|
+
updateTerminalControlsHeight();
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
codexPromptItems = [];
|
|
543
|
+
codexPromptNextOffset = 0;
|
|
544
|
+
codexPromptHasMore = false;
|
|
545
|
+
codexPromptTotal = 0;
|
|
546
|
+
codexExpandedPrompts = new Set();
|
|
547
|
+
renderCodexPromptPanel();
|
|
548
|
+
await loadMoreCodexPrompts();
|
|
549
|
+
}
|
|
550
|
+
async function loadMoreCodexPrompts() {
|
|
551
|
+
if (codexPromptLoading || !codexPromptPanelOpen || codexPromptNextOffset >= 200) return;
|
|
552
|
+
codexPromptLoading = true;
|
|
553
|
+
renderCodexPromptPanel();
|
|
554
|
+
try {
|
|
555
|
+
const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-prompts?offset=${codexPromptNextOffset}&limit=30`, {}, 60000);
|
|
556
|
+
const data = await res.json();
|
|
557
|
+
if (!res.ok || !data.success) throw new Error(data.error || 'Failed to load prompt history');
|
|
558
|
+
codexPromptItems.push(...(data.items || []));
|
|
559
|
+
codexPromptNextOffset = Number(data.nextOffset || codexPromptItems.length);
|
|
560
|
+
codexPromptHasMore = Boolean(data.hasMore) && codexPromptNextOffset < 200;
|
|
561
|
+
codexPromptTotal = Math.min(200, Number(data.total || codexPromptItems.length));
|
|
562
|
+
codexPromptLoading = false;
|
|
563
|
+
renderCodexPromptPanel();
|
|
564
|
+
} catch (error) {
|
|
565
|
+
codexPromptLoading = false;
|
|
566
|
+
renderCodexPromptPanel(error.message);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function toggleCodexPromptExpanded(index) {
|
|
570
|
+
if (codexExpandedPrompts.has(index)) codexExpandedPrompts.delete(index);
|
|
571
|
+
else codexExpandedPrompts.add(index);
|
|
572
|
+
renderCodexPromptPanel();
|
|
573
|
+
}
|
|
574
|
+
async function copyCodexPrompt(index) {
|
|
575
|
+
const prompt = codexPromptItems[index]?.text;
|
|
576
|
+
if (!prompt) return;
|
|
577
|
+
try {
|
|
578
|
+
await copyTextToClipboard(prompt);
|
|
579
|
+
const button = document.querySelector(`[data-codex-prompt-copy="${index}"]`);
|
|
580
|
+
if (!button) return;
|
|
581
|
+
button.textContent = 'Copied';
|
|
582
|
+
setTimeout(() => { if (button.isConnected) button.textContent = 'Copy'; }, 1200);
|
|
583
|
+
} catch (_) {
|
|
584
|
+
alert('Copy failed.');
|
|
585
|
+
}
|
|
586
|
+
}
|
|
448
587
|
async function selectCodexResumeThread(threadId) {
|
|
449
588
|
const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-resume`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ threadId }) }, 30000);
|
|
450
589
|
if (!res.ok) { alert((await res.json()).error || 'Unable to resume Codex thread'); return; }
|
package/lib/web/core.js
CHANGED
|
@@ -32,11 +32,18 @@
|
|
|
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;
|
|
39
39
|
let codexForkPanelOpen = false;
|
|
40
|
+
let codexPromptPanelOpen = false;
|
|
41
|
+
let codexPromptItems = [];
|
|
42
|
+
let codexPromptNextOffset = 0;
|
|
43
|
+
let codexPromptHasMore = false;
|
|
44
|
+
let codexPromptTotal = 0;
|
|
45
|
+
let codexPromptLoading = false;
|
|
46
|
+
let codexExpandedPrompts = new Set();
|
|
40
47
|
let codexRenderFrame = null;
|
|
41
48
|
let codexApprovalJumpIndex = 0;
|
|
42
49
|
const modifiers = { ctrl: false };
|
package/lib/web/index.html
CHANGED
|
@@ -122,6 +122,8 @@
|
|
|
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-prompts-btn" class="claude-ctrl-btn" onclick="toggleCodexPromptPanel()" title="Browse and copy recent prompts">Prompts</button>
|
|
126
|
+
<button id="codex-compact-btn" class="claude-ctrl-btn" onclick="compactCodexContext()" title="Compact the current Codex context">Compact</button>
|
|
125
127
|
<label class="codex-select-control" title="Sandbox mode">
|
|
126
128
|
<span class="codex-select-label" aria-hidden="true">Sandbox</span>
|
|
127
129
|
<select id="codex-sandbox-select" class="codex-select" aria-label="Sandbox mode" onchange="updateCodexSettingsFromControls()">
|
|
@@ -140,6 +142,7 @@
|
|
|
140
142
|
<div id="codex-model-panel"></div>
|
|
141
143
|
<div id="codex-resume-panel"></div>
|
|
142
144
|
<div id="codex-fork-panel"></div>
|
|
145
|
+
<div id="codex-prompt-panel"></div>
|
|
143
146
|
</div>
|
|
144
147
|
<div id="timed-send-panel">
|
|
145
148
|
<div class="timed-row">
|
package/lib/web/session.js
CHANGED
|
@@ -130,11 +130,19 @@
|
|
|
130
130
|
codexModelCandidate = null;
|
|
131
131
|
codexResumePanelOpen = false;
|
|
132
132
|
codexForkPanelOpen = false;
|
|
133
|
+
codexPromptPanelOpen = false;
|
|
134
|
+
codexPromptItems = [];
|
|
135
|
+
codexPromptNextOffset = 0;
|
|
136
|
+
codexPromptHasMore = false;
|
|
137
|
+
codexPromptTotal = 0;
|
|
138
|
+
codexPromptLoading = false;
|
|
139
|
+
codexExpandedPrompts = new Set();
|
|
133
140
|
document.getElementById('codex-model-panel').classList.remove('active');
|
|
134
141
|
document.getElementById('codex-resume-panel').classList.remove('active');
|
|
135
142
|
document.getElementById('codex-fork-panel').classList.remove('active');
|
|
143
|
+
document.getElementById('codex-prompt-panel').classList.remove('active');
|
|
136
144
|
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 };
|
|
145
|
+
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
146
|
setClaudeModeEnabled(false);
|
|
139
147
|
applyCodexState(codexState);
|
|
140
148
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
package/lib/web/styles.css
CHANGED
|
@@ -93,8 +93,22 @@
|
|
|
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; }
|
|
108
|
+
.codex-warning-card { display: flex; align-items: flex-start; gap: 10px; margin: 10px 0 14px; padding: 10px 12px; border: 1px solid rgba(255,204,0,.34); border-radius: 10px; background: linear-gradient(135deg, rgba(255,204,0,.13), rgba(255,159,10,.07)); color: #f5f5f7; }
|
|
109
|
+
.codex-warning-icon { display: grid; width: 28px; height: 28px; flex: 0 0 28px; place-items: center; border-radius: 50%; background: rgba(255,204,0,.16); color: #ffd60a; font-size: 15px; font-weight: 900; }
|
|
110
|
+
.codex-warning-title { color: #ffe680; font-size: 12px; font-weight: 800; }
|
|
111
|
+
.codex-warning-text { margin-top: 3px; color: #d8d3bf; font-size: 11px; line-height: 1.4; overflow-wrap: anywhere; }
|
|
98
112
|
.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
113
|
.codex-status-title { margin-bottom: 9px; color: #64d2ff; font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em; }
|
|
100
114
|
.codex-status-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px; }
|
|
@@ -151,6 +165,7 @@
|
|
|
151
165
|
.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
166
|
.claude-inline-permission-title { margin-bottom: 7px; color: #ffcc00; font-weight: 800; }
|
|
153
167
|
@keyframes codex-spin { to { transform: rotate(360deg); } }
|
|
168
|
+
@keyframes codex-pulse { to { box-shadow: 0 0 0 5px rgba(191,90,242,.08); transform: scale(.92); } }
|
|
154
169
|
#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
170
|
.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
171
|
.codex-control-rail::-webkit-scrollbar { display: none; }
|
|
@@ -162,8 +177,8 @@
|
|
|
162
177
|
.codex-select-control:focus-within .codex-select-label { background: rgba(255,255,255,.14); border-color: rgba(0,122,255,.45); }
|
|
163
178
|
.codex-select { appearance: none; position: absolute; inset: 0; width: 100%; min-width: 0; height: 100%; opacity: 0; cursor: pointer; }
|
|
164
179
|
.codex-select option { background: #1c1c1e; color: #fff; }
|
|
165
|
-
#codex-model-panel, #codex-resume-panel, #codex-fork-panel { display: none; margin-top: 8px; border: 1px solid rgba(255,255,255,.1); background: rgba(28,28,30,.99); border-radius: 8px; box-shadow: 0 16px 36px rgba(0,0,0,.34); overflow: hidden; }
|
|
166
|
-
#codex-model-panel.active, #codex-resume-panel.active, #codex-fork-panel.active { display: block; }
|
|
180
|
+
#codex-model-panel, #codex-resume-panel, #codex-fork-panel, #codex-prompt-panel { display: none; margin-top: 8px; border: 1px solid rgba(255,255,255,.1); background: rgba(28,28,30,.99); border-radius: 8px; box-shadow: 0 16px 36px rgba(0,0,0,.34); overflow: hidden; }
|
|
181
|
+
#codex-model-panel.active, #codex-resume-panel.active, #codex-fork-panel.active, #codex-prompt-panel.active { display: block; }
|
|
167
182
|
#codex-model-panel { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); max-height: min(300px, 42dvh); }
|
|
168
183
|
#codex-model-panel.active { display: grid; }
|
|
169
184
|
.codex-picker-column { min-width: 0; overflow-y: auto; }
|
|
@@ -173,6 +188,17 @@
|
|
|
173
188
|
#codex-state-bar { display: flex; align-items: center; gap: 7px; min-height: 24px; margin-top: 8px; color: var(--text-dim); font-size: 11px; overflow-x: auto; scrollbar-width: none; white-space: nowrap; }
|
|
174
189
|
#codex-state-bar::-webkit-scrollbar { display: none; }
|
|
175
190
|
#codex-resume-panel, #codex-fork-panel { max-height: min(280px, 38dvh); overflow-y: auto; }
|
|
191
|
+
#codex-prompt-panel { max-height: min(440px, 52dvh); overflow-y: auto; }
|
|
192
|
+
.codex-prompt-header { position: sticky; top: 0; z-index: 2; display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 11px; border-bottom: 1px solid rgba(255,255,255,.08); background: rgba(28,28,30,.97); color: var(--text-dim); font-size: 11px; font-weight: 800; }
|
|
193
|
+
.codex-prompt-item { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; align-items: end; padding: 10px 11px; border-bottom: 1px solid rgba(255,255,255,.06); }
|
|
194
|
+
.codex-prompt-item:last-of-type { border-bottom: 0; }
|
|
195
|
+
.codex-prompt-text { display: -webkit-box; width: 100%; min-width: 0; max-height: calc(1.42em * 4); overflow: hidden; padding: 0; border: 0; background: transparent; color: #f5f5f7; font: 12px/1.42 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; text-align: left; white-space: pre-wrap; overflow-wrap: anywhere; -webkit-box-orient: vertical; -webkit-line-clamp: 4; cursor: pointer; }
|
|
196
|
+
.codex-prompt-text.expanded { display: block; max-height: none; -webkit-line-clamp: initial; }
|
|
197
|
+
.codex-prompt-actions { display: flex; min-width: 64px; flex-direction: column; align-items: flex-end; gap: 6px; }
|
|
198
|
+
.codex-prompt-time { color: #77777e; font-size: 10px; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
199
|
+
.codex-prompt-copy { min-width: 54px; min-height: 28px; padding: 0 9px; }
|
|
200
|
+
.codex-prompt-footer { padding: 9px 11px; text-align: center; }
|
|
201
|
+
.codex-prompt-footer .small-btn { min-width: 112px; }
|
|
176
202
|
.claude-message { max-width: 92%; margin: 0 0 10px 0; padding: 10px 12px; border-radius: 8px; overflow-wrap: anywhere; line-height: 1.45; font-size: 14px; }
|
|
177
203
|
.claude-message-block { max-width: 100%; margin: 0 0 12px; }
|
|
178
204
|
.claude-message-block > .claude-message { margin-bottom: 0; }
|