glad-web 1.0.30 → 1.0.31

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