glad-web 1.0.35 → 1.0.36

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.
@@ -4,8 +4,6 @@ const readline = require('readline');
4
4
  const crypto = require('crypto');
5
5
  const PTYManager = require('../session/pty-manager');
6
6
 
7
- const CODEX_MESSAGE_PAGE_BYTES = 200 * 1024;
8
-
9
7
  const PERMISSION_MODES = new Set(['untrusted', 'on-request', 'never']);
10
8
  const SANDBOX_MODES = new Set(['read-only', 'workspace-write', 'danger-full-access']);
11
9
  const FALLBACK_EFFORTS = ['minimal', 'low', 'medium', 'high', 'xhigh', 'ultra'];
@@ -199,6 +197,7 @@ class CodexStructuredSession extends EventEmitter {
199
197
  this.status = 'idle';
200
198
  this.presentation = 'structured';
201
199
  this.messages = [];
200
+ this.replayingHistory = false;
202
201
  this.pendingPermissions = new Map();
203
202
  this.completedPermissions = [];
204
203
  this.threadId = options.resume || null;
@@ -252,58 +251,51 @@ class CodexStructuredSession extends EventEmitter {
252
251
  }
253
252
 
254
253
  snapshot() {
255
- const historyPage = this.getMessagePage();
256
- const { messages, ...historyPageMeta } = historyPage;
257
254
  return { id: this.id, name: this.name, tool: this.tool.displayName, toolKey: this.tool.key,
258
- status: this.status, state: this.getControlState(), messages, historyPage: historyPageMeta,
255
+ status: this.status, state: this.getControlState(), messages: this.messages.map(item => this.toPublicMessage(item)),
259
256
  pendingPermissions: [
260
257
  ...this.completedPermissions,
261
258
  ...Array.from(this.pendingPermissions.values()).map(item => item.public)
262
259
  ] };
263
260
  }
264
261
 
265
- getMessagePage(beforeId = null, maxBytes = CODEX_MESSAGE_PAGE_BYTES) {
266
- const requestedBeforeId = beforeId == null ? '' : String(beforeId);
267
- let end = this.messages.length;
268
- if (requestedBeforeId) {
269
- const beforeIndex = this.messages.findIndex(item => String(item.id || '') === requestedBeforeId);
270
- if (beforeIndex < 0) {
271
- return { messages: [], hasMore: false, beforeId: null, bytes: 2, maxBytes };
272
- }
273
- end = beforeIndex;
274
- }
275
-
276
- if (end <= 0) return { messages: [], hasMore: false, beforeId: null, bytes: 2, maxBytes };
262
+ toPublicMessage(item) {
263
+ if (!item || typeof item !== 'object') return item;
264
+ const message = { ...item };
265
+ const isSubagent = Boolean(message.threadId && this.threadId && message.threadId !== this.threadId);
266
+ let hasDetail = false;
277
267
 
278
- const groupStarts = [0];
279
- for (let i = 1; i < end; i++) {
280
- const item = this.messages[i];
281
- if (item.kind === 'turn-start' && (!item.threadId || item.threadId === this.threadId)) {
282
- groupStarts.push(i);
268
+ if (message.kind === 'tool') {
269
+ for (const field of ['result', 'input', 'changes', 'error', 'agentsStates']) {
270
+ const value = message[field];
271
+ if (value != null && value !== '' && (!Array.isArray(value) || value.length)) hasDetail = true;
272
+ delete message[field];
283
273
  }
274
+ if (item.error) message.hasError = true;
284
275
  }
285
- groupStarts.push(end);
286
-
287
- let start = end;
288
- let selectedBytes = 2;
289
- for (let groupIndex = groupStarts.length - 2; groupIndex >= 0; groupIndex--) {
290
- const candidateStart = groupStarts[groupIndex];
291
- const candidate = this.messages.slice(candidateStart, end);
292
- const candidateBytes = Buffer.byteLength(JSON.stringify(candidate), 'utf8');
293
- if (start < end && candidateBytes > maxBytes) break;
294
- start = candidateStart;
295
- selectedBytes = candidateBytes;
296
- if (candidateBytes >= maxBytes) break;
276
+ if (isSubagent && ['user', 'assistant', 'reasoning', 'event'].includes(message.kind)) {
277
+ if (message.text) hasDetail = true;
278
+ delete message.text;
279
+ delete message.skills;
280
+ }
281
+ if (message.kind === 'reasoning') {
282
+ if (message.text) hasDetail = true;
283
+ delete message.text;
297
284
  }
285
+ message.hasDetail = hasDetail;
286
+ message.detailRevision = Number(item.updatedAt || item.createdAt || 0);
287
+ return message;
288
+ }
298
289
 
299
- const messages = this.messages.slice(start, end);
300
- return {
301
- messages,
302
- hasMore: start > 0,
303
- beforeId: messages[0]?.id || null,
304
- bytes: selectedBytes,
305
- maxBytes
306
- };
290
+ getMessageDetails({ ids = [], threadId = null } = {}) {
291
+ const requestedIds = new Set((Array.isArray(ids) ? ids : [])
292
+ .map(value => String(value || '')).filter(Boolean));
293
+ const requestedThreadId = threadId == null ? '' : String(threadId);
294
+ const messages = this.messages.filter(item => {
295
+ if (requestedThreadId && String(item.threadId || '') === requestedThreadId) return true;
296
+ return requestedIds.has(String(item.id || ''));
297
+ }).map(item => ({ ...item, detailLoaded: true }));
298
+ return { messages, threadId: requestedThreadId || null };
307
299
  }
308
300
 
309
301
  getControlState() {
@@ -340,12 +332,17 @@ class CodexStructuredSession extends EventEmitter {
340
332
  isRunning() { return this.running && (this.presentation !== 'terminal' || Boolean(this.terminalSession)); }
341
333
 
342
334
  createItem(item) { return { id: crypto.randomUUID(), createdAt: Date.now(), ...item }; }
343
- append(item) { const next = this.createItem(item); this.messages.push(next); this.emitEvent({ type: 'message', message: next }); return next; }
335
+ append(item) {
336
+ const next = this.createItem(item);
337
+ this.messages.push(next);
338
+ if (!this.replayingHistory) this.emitEvent({ type: 'message', message: this.toPublicMessage(next) });
339
+ return next;
340
+ }
344
341
  patch(id, patch) {
345
342
  const item = this.messages.find(message => message.id === id);
346
343
  if (!item) return null;
347
344
  Object.assign(item, { updatedAt: Date.now() }, patch);
348
- this.emitEvent({ type: 'message-updated', message: item });
345
+ if (!this.replayingHistory) this.emitEvent({ type: 'message-updated', message: this.toPublicMessage(item) });
349
346
  return item;
350
347
  }
351
348
  emitEvent(event) { this.emit('event', event); }
@@ -1112,10 +1109,12 @@ class CodexStructuredSession extends EventEmitter {
1112
1109
  if (!options.preserveEffort) this.effort = thread?.reasoningEffort || thread?.reasoning_effort || this.effort;
1113
1110
  this.tokenUsage = thread?.tokenUsage || thread?.token_usage || this.tokenUsage;
1114
1111
  this.messages = [];
1112
+ this.replayingHistory = true;
1115
1113
  this.completedPermissions = [];
1116
1114
  this.turnContexts.clear();
1117
1115
  this.providerItemContexts.clear();
1118
- for (const turn of thread?.turns || []) {
1116
+ try {
1117
+ for (const turn of thread?.turns || []) {
1119
1118
  const status = turn.status === 'failed' ? 'failed' : turn.status === 'interrupted' ? 'cancelled' : 'completed';
1120
1119
  const startedAt = Number(turn.startedAt || turn.createdAt || 0);
1121
1120
  const completedAt = Number(turn.completedAt || turn.updatedAt || 0);
@@ -1134,11 +1133,12 @@ class CodexStructuredSession extends EventEmitter {
1134
1133
  || (startedAtMs && completedAtMs && completedAtMs >= startedAtMs ? completedAtMs - startedAtMs : null);
1135
1134
  this.append({ kind: 'turn-end', turnId: turn.id, status, durationMs,
1136
1135
  ...(completedAtMs ? { createdAt: completedAtMs } : {}) });
1136
+ }
1137
+ if (eventText) this.append({ kind: 'event', level: 'info', text: eventText });
1138
+ } finally {
1139
+ this.replayingHistory = false;
1137
1140
  }
1138
- if (eventText) this.append({ kind: 'event', level: 'info', text: eventText });
1139
- const historyPage = this.getMessagePage();
1140
- const { messages, ...historyPageMeta } = historyPage;
1141
- this.emitEvent({ type: 'history-reset', messages, historyPage: historyPageMeta });
1141
+ this.emitEvent({ type: 'history-reset', messages: this.messages.map(item => this.toPublicMessage(item)) });
1142
1142
  this.emitEvent({ type: 'state', state: this.getControlState() });
1143
1143
  }
1144
1144
 
@@ -377,10 +377,14 @@ async function webCommand(options) {
377
377
  if (payload.type === 'codex-compact') {
378
378
  sessionManager.compactCodexContext(sessionId).catch(error => logger.error(`Codex compact error: ${error.message}`));
379
379
  }
380
- if (payload.type === 'codex-history-before') {
380
+ if (payload.type === 'codex-detail-request') {
381
381
  const codex = sessionManager.get(sessionId);
382
382
  if (codex && codex.kind === 'codex-structured' && codex.presentation === 'structured') {
383
- ws.send(JSON.stringify({ type: 'codex-history-page', page: codex.getMessagePage(payload.beforeId) }));
383
+ ws.send(JSON.stringify({
384
+ type: 'codex-detail-response',
385
+ requestId: payload.requestId || null,
386
+ detail: codex.getMessageDetails({ ids: payload.ids, threadId: payload.threadId })
387
+ }));
384
388
  }
385
389
  }
386
390
  if (payload.type === 'codex-abort') {
package/lib/web/codex.js CHANGED
@@ -10,6 +10,11 @@
10
10
  if (status === 'completed') return item.exitCode && item.exitCode !== 0 ? 'failed' : 'completed';
11
11
  return status;
12
12
  }
13
+ function codexMessageNeedsDetail(item) {
14
+ if (!item?.hasDetail) return false;
15
+ const loadedRevision = Number(codexDetailRevisions.get(String(item.id)) || 0);
16
+ return !item.detailLoaded || loadedRevision < Number(item.detailRevision || 0);
17
+ }
13
18
  function formatCodexDuration(durationMs) {
14
19
  const value = Number(durationMs || 0);
15
20
  if (!(value > 0)) return '';
@@ -108,7 +113,7 @@
108
113
  }
109
114
  function renderCodexTool(item, permission = null) {
110
115
  const status = codexToolStatus(item);
111
- const isError = status === 'failed' || Boolean(item.error) || (item.exitCode != null && item.exitCode !== 0);
116
+ const isError = status === 'failed' || Boolean(item.error) || Boolean(item.hasError) || (item.exitCode != null && item.exitCode !== 0);
112
117
  const runningClass = status === 'running' ? ' running' : '';
113
118
  if (item.name === 'CodexPatch') {
114
119
  return `<div class="codex-tool${isError ? ' error' : ''}">${renderCodexPatch(item)}${permission ? renderCodexPermission(permission, true) : ''}</div>`;
@@ -138,7 +143,11 @@
138
143
  }).join('');
139
144
  const label = running ? 'Working' : failed ? 'Work finished with errors' : duration ? `Worked for ${duration}` : 'Worked';
140
145
  const key = items.map(item => item.id || item.providerId || '').join('-');
141
- return `<details class="codex-work-group" data-codex-key="group-${escapeHtml(key)}"><summary>${label} · ${items.length} ${items.length === 1 ? 'tool' : 'tools'}</summary><div class="codex-work-group-body">${tools}</div></details>`;
146
+ const allDetailIds = items.filter(item => item.hasDetail).map(item => item.id).filter(Boolean);
147
+ const detailIds = items.filter(codexMessageNeedsDetail).map(item => item.id).filter(Boolean);
148
+ const lazy = detailIds.length
149
+ ? '<div class="codex-lazy-detail">Open to load tool details…</div>' : '';
150
+ return `<details class="codex-work-group" data-codex-key="group-${escapeHtml(key)}"${allDetailIds.length ? ` data-codex-detail-ids="${escapeHtml(allDetailIds.join(','))}"` : ''}><summary>${label} · ${items.length} ${items.length === 1 ? 'tool' : 'tools'}</summary><div class="codex-work-group-body">${tools}${lazy}</div></details>`;
142
151
  }
143
152
  function isCodexSubagentItem(item) {
144
153
  return Boolean(item?.threadId && codexState.threadId && item.threadId !== codexState.threadId);
@@ -159,8 +168,13 @@
159
168
  if (tools.length) counts.push(`${tools.length} ${tools.length === 1 ? 'tool' : 'tools'}`);
160
169
  if (messages.length) counts.push(`${messages.length} ${messages.length === 1 ? 'message' : 'messages'}`);
161
170
  const label = running ? 'Subagent working' : duration ? `Subagent worked for ${duration}` : 'Subagent worked';
171
+ const needsDetail = items.some(codexMessageNeedsDetail);
172
+ const deferContent = needsDetail && !items.some(item => item.detailLoaded);
162
173
  const body = [];
163
- for (let i = 0; i < content.length;) {
174
+ if (needsDetail) {
175
+ body.push(`<div class="codex-lazy-detail">${deferContent ? 'Open to load subagent details…' : 'Updating subagent details…'}</div>`);
176
+ }
177
+ for (let i = 0; !deferContent && i < content.length;) {
164
178
  const item = content[i];
165
179
  if (item.kind === 'tool') {
166
180
  const group = [];
@@ -171,14 +185,14 @@
171
185
  continue;
172
186
  }
173
187
  if (item.kind === 'assistant' || item.kind === 'user') {
174
- body.push(`<div class="codex-subagent-message${item.kind === 'user' ? ' task' : ''}">${renderMarkdown(item.text || '')}</div>`);
188
+ if (item.text) body.push(`<div class="codex-subagent-message${item.kind === 'user' ? ' task' : ''}">${renderMarkdown(item.text)}</div>`);
175
189
  } else if (item.text) {
176
190
  body.push(`<div class="codex-subagent-message">${codexText(item.text)}</div>`);
177
191
  }
178
192
  i += 1;
179
193
  }
180
194
  const suffix = counts.length ? ` · ${counts.join(' · ')}` : '';
181
- return `<details class="codex-work-group codex-subagent-group" data-codex-key="subagent-${escapeHtml(threadId)}"><summary>${escapeHtml(label + suffix)}</summary><div class="codex-work-group-body">${body.join('')}</div></details>`;
195
+ return `<details class="codex-work-group codex-subagent-group" data-codex-key="subagent-${escapeHtml(threadId)}" data-codex-thread-id="${escapeHtml(threadId)}"><summary>${escapeHtml(label + suffix)}</summary><div class="codex-work-group-body">${body.join('')}</div></details>`;
182
196
  }
183
197
  function renderCodexMessageTime(item, finalOnly = false) {
184
198
  if (!item || (finalOnly && item.streaming)) return '';
@@ -320,46 +334,89 @@
320
334
  const current = container.firstElementChild;
321
335
  if (!current) container.appendChild(next);
322
336
  else syncCodexDom(current, next);
323
- if (codexHistoryPrependAnchor) {
324
- const anchor = codexHistoryPrependAnchor;
325
- codexHistoryPrependAnchor = null;
326
- container.scrollTop = anchor.scrollTop + Math.max(0, container.scrollHeight - anchor.scrollHeight);
327
- } else {
328
- container.scrollTop = shouldStickToBottom ? container.scrollHeight : previousScrollTop;
337
+ container.scrollTop = shouldStickToBottom ? container.scrollHeight : previousScrollTop;
338
+ }
339
+
340
+ function installCodexLazyDetailHandler() {
341
+ const container = document.getElementById('codex-chat-container');
342
+ if (!container || container.dataset.lazyDetailHandler === 'true') return;
343
+ container.dataset.lazyDetailHandler = 'true';
344
+ container.addEventListener('toggle', handleCodexLazyToggle, true);
345
+ }
346
+
347
+ function handleCodexLazyToggle(event) {
348
+ const group = event.target;
349
+ if (!(group instanceof HTMLDetailsElement) || !group.open || !group.classList.contains('codex-work-group')) return;
350
+ const threadId = group.dataset.codexThreadId || '';
351
+ const ids = String(group.dataset.codexDetailIds || '').split(',').filter(Boolean)
352
+ .filter(id => codexMessageNeedsDetail(codexMessages.find(item => item.id === id)));
353
+ if (threadId && codexMessages.some(item => item.threadId === threadId && codexMessageNeedsDetail(item))) {
354
+ void requestCodexDetails({ threadId });
329
355
  }
356
+ else if (ids.length) void requestCodexDetails({ ids });
330
357
  }
331
358
 
332
- function applyCodexHistoryPageMeta(page = {}) {
333
- codexHistoryBeforeId = page?.beforeId || codexMessages[0]?.id || null;
334
- codexHistoryHasMore = Boolean(page?.hasMore);
335
- codexHistoryLoading = false;
359
+ function requestCodexDetails({ ids = [], threadId = '' } = {}) {
360
+ if (currentSocket?.readyState !== 1) return Promise.resolve(false);
361
+ const normalizedIds = Array.from(new Set(ids.map(value => String(value || '')).filter(Boolean))).sort();
362
+ const key = threadId ? `thread:${threadId}` : `ids:${normalizedIds.join(',')}`;
363
+ for (const pending of codexDetailRequests.values()) {
364
+ if (pending.key === key) return pending.promise;
365
+ }
366
+ const requestId = `detail-${++codexDetailRequestSeq}`;
367
+ let resolveRequest;
368
+ const promise = new Promise(resolve => { resolveRequest = resolve; });
369
+ const timer = setTimeout(() => {
370
+ const pending = codexDetailRequests.get(requestId);
371
+ if (!pending) return;
372
+ codexDetailRequests.delete(requestId);
373
+ pending.resolve(false);
374
+ }, 15000);
375
+ codexDetailRequests.set(requestId, { key, promise, resolve: resolveRequest, timer });
376
+ currentSocket.send(JSON.stringify({
377
+ type: 'codex-detail-request',
378
+ requestId,
379
+ ...(threadId ? { threadId } : { ids: normalizedIds })
380
+ }));
381
+ return promise;
336
382
  }
337
383
 
338
- function handleCodexHistoryScroll(event) {
339
- if (event?.isTrusted) codexHistoryUserScrolled = true;
340
- if (!codexHistoryUserScrolled || !codexHistoryHasMore || codexHistoryLoading) return;
341
- const container = event?.currentTarget || document.getElementById('codex-chat-container');
342
- const scrollRange = Math.max(0, container.scrollHeight - container.clientHeight);
343
- if (container.scrollTop > scrollRange * 0.5) return;
344
- if (!codexHistoryBeforeId || currentSocket?.readyState !== 1) return;
345
- codexHistoryLoading = true;
346
- currentSocket.send(JSON.stringify({ type: 'codex-history-before', beforeId: codexHistoryBeforeId }));
384
+ function applyCodexDetailResponse(response) {
385
+ const pending = codexDetailRequests.get(response.requestId);
386
+ if (!pending) return;
387
+ const detail = response.detail || {};
388
+ for (const message of detail.messages || []) {
389
+ const index = codexMessages.findIndex(item => item.id === message.id);
390
+ if (index >= 0) codexMessages[index] = { ...codexMessages[index], ...message, detailLoaded: true };
391
+ else codexMessages.push({ ...message, detailLoaded: true });
392
+ codexDetailRevisions.set(String(message.id), Number(message.updatedAt || message.createdAt || 0));
393
+ }
394
+ clearTimeout(pending.timer);
395
+ codexDetailRequests.delete(response.requestId);
396
+ pending.resolve(true);
397
+ renderCodexChat();
347
398
  }
348
399
 
349
- function applyCodexHistoryPage(page = {}) {
350
- const older = Array.isArray(page.messages) ? page.messages : [];
351
- const knownIds = new Set(codexMessages.map(item => String(item.id || '')));
352
- const uniqueOlder = older.filter(item => item?.id && !knownIds.has(String(item.id)));
353
- const container = document.getElementById('codex-chat-container');
354
- if (uniqueOlder.length && container) {
355
- codexHistoryPrependAnchor = {
356
- scrollTop: container.scrollTop,
357
- scrollHeight: container.scrollHeight
358
- };
359
- codexMessages = [...uniqueOlder, ...codexMessages];
400
+ function codexDetailIsOpen(message) {
401
+ if (!message?.id) return false;
402
+ if (isCodexSubagentItem(message)) {
403
+ return Array.from(document.querySelectorAll('.codex-subagent-group[open]'))
404
+ .some(group => group.dataset.codexKey === `subagent-${message.threadId}`);
360
405
  }
361
- applyCodexHistoryPageMeta(page);
362
- if (uniqueOlder.length) renderCodexChat();
406
+ return Array.from(document.querySelectorAll('.codex-work-group[open][data-codex-detail-ids]'))
407
+ .some(group => String(group.dataset.codexDetailIds || '').split(',').includes(String(message.id)));
408
+ }
409
+
410
+ function scheduleCodexDetailRefresh(message) {
411
+ if (!message || !codexDetailIsOpen(message)) return;
412
+ if (!message.id || (!message.detailLoaded && !isCodexSubagentItem(message))) return;
413
+ codexDetailRefreshIds.add(String(message.id));
414
+ clearTimeout(codexDetailRefreshTimer);
415
+ codexDetailRefreshTimer = setTimeout(() => {
416
+ const ids = Array.from(codexDetailRefreshIds);
417
+ codexDetailRefreshIds.clear();
418
+ if (ids.length) void requestCodexDetails({ ids });
419
+ }, 300);
363
420
  }
364
421
 
365
422
  function applyCodexState(state = {}) {
@@ -418,7 +475,18 @@
418
475
  if (!pending.length) return false;
419
476
  const request = pending[codexApprovalJumpIndex % pending.length];
420
477
  codexApprovalJumpIndex = (codexApprovalJumpIndex + 1) % pending.length;
421
- return focusCodexApproval(String(request.id || ''));
478
+ void loadAndFocusCodexApproval(request);
479
+ return true;
480
+ }
481
+
482
+ async function loadAndFocusCodexApproval(request) {
483
+ const permissionId = String(request?.id || '');
484
+ const message = codexMessages.find(item => String(item.providerId || item.id || '') === permissionId);
485
+ if (message && codexMessageNeedsDetail(message)) {
486
+ if (isCodexSubagentItem(message)) await requestCodexDetails({ threadId: String(message.threadId) });
487
+ else await requestCodexDetails({ ids: [String(message.id)] });
488
+ }
489
+ requestAnimationFrame(() => focusCodexApproval(permissionId));
422
490
  }
423
491
 
424
492
  function focusCodexApproval(permissionId, retry = true) {
@@ -482,13 +550,32 @@
482
550
 
483
551
  function applyCodexEvent(event) {
484
552
  if (!event) return;
485
- if (event.type === 'message' && event.message) codexMessages.push(event.message);
486
- else if (event.type === 'message-updated' && event.message) { const i = codexMessages.findIndex(item => item.id === event.message.id); if (i >= 0) codexMessages[i] = event.message; else codexMessages.push(event.message); }
553
+ if (event.type === 'message' && event.message) {
554
+ codexMessages.push(event.message);
555
+ scheduleCodexDetailRefresh(event.message);
556
+ }
557
+ else if (event.type === 'message-updated' && event.message) {
558
+ const i = codexMessages.findIndex(item => item.id === event.message.id);
559
+ if (i >= 0) {
560
+ const existing = codexMessages[i];
561
+ codexMessages[i] = existing.detailLoaded
562
+ ? { ...existing, ...event.message, detailLoaded: true }
563
+ : event.message;
564
+ scheduleCodexDetailRefresh(codexMessages[i]);
565
+ } else codexMessages.push(event.message);
566
+ }
487
567
  else if (event.type === 'history-reset') {
488
568
  codexMessages = event.messages || [];
489
- codexHistoryUserScrolled = false;
490
- codexHistoryPrependAnchor = null;
491
- applyCodexHistoryPageMeta(event.historyPage);
569
+ codexDetailRevisions.clear();
570
+ clearTimeout(codexDetailRefreshTimer);
571
+ codexDetailRefreshTimer = null;
572
+ codexDetailRefreshIds.clear();
573
+ for (const pending of codexDetailRequests.values()) {
574
+ clearTimeout(pending.timer);
575
+ pending.resolve(false);
576
+ }
577
+ codexDetailRequests.clear();
578
+ document.getElementById('codex-chat-container')?.replaceChildren();
492
579
  }
493
580
  else if (event.type === 'permission-request' && event.request) { codexPendingPermissions = [...codexPendingPermissions.filter(item => item.id !== event.request.id), event.request]; }
494
581
  else if (event.type === 'permission-updated' && event.request) codexPendingPermissions = codexPendingPermissions.map(item => item.id === event.request.id ? event.request : item);
package/lib/web/core.js CHANGED
@@ -52,11 +52,11 @@
52
52
  let selectedCodexSkill = null;
53
53
  let codexRenderFrame = null;
54
54
  let codexApprovalJumpIndex = 0;
55
- let codexHistoryBeforeId = null;
56
- let codexHistoryHasMore = false;
57
- let codexHistoryLoading = false;
58
- let codexHistoryUserScrolled = false;
59
- let codexHistoryPrependAnchor = null;
55
+ let codexDetailRequestSeq = 0;
56
+ let codexDetailRequests = new Map();
57
+ let codexDetailRevisions = new Map();
58
+ let codexDetailRefreshTimer = null;
59
+ let codexDetailRefreshIds = new Set();
60
60
  const modifiers = { ctrl: false };
61
61
 
62
62
  function log(msg) {
@@ -143,11 +143,12 @@
143
143
  codexSkillQuery = '';
144
144
  codexSkillError = '';
145
145
  selectedCodexSkill = null;
146
- codexHistoryBeforeId = null;
147
- codexHistoryHasMore = false;
148
- codexHistoryLoading = false;
149
- codexHistoryUserScrolled = false;
150
- codexHistoryPrependAnchor = null;
146
+ codexDetailRequestSeq = 0;
147
+ codexDetailRequests = new Map();
148
+ codexDetailRevisions = new Map();
149
+ clearTimeout(codexDetailRefreshTimer);
150
+ codexDetailRefreshTimer = null;
151
+ codexDetailRefreshIds = new Set();
151
152
  document.getElementById('codex-model-panel').classList.remove('active');
152
153
  document.getElementById('codex-resume-panel').classList.remove('active');
153
154
  document.getElementById('codex-fork-panel').classList.remove('active');
@@ -157,7 +158,7 @@
157
158
  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 };
158
159
  setClaudeModeEnabled(false);
159
160
  applyCodexState(codexState);
160
- document.getElementById('codex-chat-container').onscroll = handleCodexHistoryScroll;
161
+ installCodexLazyDetailHandler();
161
162
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
162
163
  currentSocket = new WebSocket(protocol + '//' + window.location.host + '?sessionId=' + sessionId);
163
164
  currentSocket.onmessage = (e) => {
@@ -165,10 +166,9 @@
165
166
  if (msg.type === 'codex-snapshot' && msg.snapshot) {
166
167
  codexMessages = msg.snapshot.messages || [];
167
168
  codexPendingPermissions = msg.snapshot.pendingPermissions || [];
168
- applyCodexHistoryPageMeta(msg.snapshot.historyPage);
169
169
  applyCodexState(msg.snapshot.state || {});
170
170
  }
171
- if (msg.type === 'codex-history-page' && msg.page) applyCodexHistoryPage(msg.page);
171
+ if (msg.type === 'codex-detail-response' && msg.detail) applyCodexDetailResponse(msg);
172
172
  if (msg.type === 'codex-event') {
173
173
  applyCodexEvent(msg.event);
174
174
  if (msg.event?.type === 'presentation' && msg.event.presentation === 'terminal') {
@@ -85,6 +85,7 @@
85
85
  #claude-chat-container { display: none; flex: 1; min-height: 0; overflow-y: auto; background: #050505; padding: 12px 12px 24px 12px; box-sizing: border-box; -webkit-overflow-scrolling: touch; }
86
86
  .claude-conversation { width: min(100%, var(--chat-content-max)); min-height: 100%; margin: 0 auto; }
87
87
  #codex-chat-container { display: none; flex: 1; min-height: 0; overflow-y: auto; background: #050505; padding: 12px 12px 24px 12px; box-sizing: border-box; -webkit-overflow-scrolling: touch; }
88
+ .codex-lazy-detail { padding: 10px 12px; color: var(--text-dim); font-size: 12px; }
88
89
  .codex-conversation { width: min(100%, var(--chat-content-max)); margin: 0 auto; }
89
90
  .codex-working-indicator, .claude-working-indicator { position: sticky; top: 0; z-index: 4; width: 28px; height: 28px; margin: 0 0 -28px auto; border: 1px solid rgba(255,255,255,.1); border-radius: 50%; background: rgba(28,28,30,.68); box-shadow: 0 5px 16px rgba(0,0,0,.24); backdrop-filter: blur(8px); pointer-events: none; }
90
91
  .codex-working-indicator::after, .claude-working-indicator::after { content: ''; position: absolute; inset: 8px; border: 1.5px solid rgba(255,255,255,.7); border-top-color: transparent; border-radius: 50%; animation: codex-spin .8s linear infinite; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glad-web",
3
- "version": "1.0.35",
3
+ "version": "1.0.36",
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"