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.
@@ -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
- this.append({ kind: 'event', level: 'warning', text: params.message || params.warning || 'Codex warning.' });
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
- const existing = providerId && this.messages.find(item => item.providerId === providerId);
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' : null;
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
- contextStatus() {
629
- const usage = this.tokenUsage || {};
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
- const result = await this.request('thread/resume', params);
833
- this.threadId = result.thread?.id || target;
834
- this.hasModelOverride = false;
835
- this.hasEffortOverride = false;
836
- this.model = result.model || result.thread?.model || this.model;
837
- this.effort = result.reasoningEffort || result.thread?.reasoningEffort || this.effort;
838
- this.effectivePermissionMode = result.approvalPolicy || this.effectivePermissionMode;
839
- this.effectiveSandboxMode = sandboxModeFromPolicy(result.sandbox) || this.effectiveSandboxMode;
840
- const history = await this.request('thread/read', { threadId: this.threadId, includeTurns: true });
841
- this.restoreThreadHistory(history?.thread, `Resumed Codex thread ${this.threadId}`);
842
- return true;
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
 
@@ -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', (req, res) => res.sendFile(xtermScript));
459
- app.get('/vendor/xterm.css', (req, res) => res.sendFile(xtermStyles));
460
- app.get('/vendor/xterm-addon-fit.js', (req, res) => res.sendFile(fitAddonScript));
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') 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>${renderCodexMessageTime(item, true)}</div>`);
262
- 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>${renderCodexMessageTime(item)}</div>`);
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 };
@@ -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">
@@ -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:';
@@ -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-time { display: block; width: max-content; margin-top: 4px; padding: 0 2px; color: #8e8e93; font-size: 10px; font-weight: 500; line-height: 1; font-variant-numeric: tabular-nums; }
97
- .codex-message-block.user .codex-message-time { margin-left: auto; }
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; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glad-web",
3
- "version": "1.0.30",
3
+ "version": "1.0.32",
4
4
  "description": "Glad transforms terminal-based AI coding tools into a polished, mobile-friendly local Web interface.",
5
5
  "bin": {
6
6
  "glad": "bin/cli.js"