glad-web 1.0.34 → 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.
@@ -197,6 +197,7 @@ class CodexStructuredSession extends EventEmitter {
197
197
  this.status = 'idle';
198
198
  this.presentation = 'structured';
199
199
  this.messages = [];
200
+ this.replayingHistory = false;
200
201
  this.pendingPermissions = new Map();
201
202
  this.completedPermissions = [];
202
203
  this.threadId = options.resume || null;
@@ -251,13 +252,52 @@ class CodexStructuredSession extends EventEmitter {
251
252
 
252
253
  snapshot() {
253
254
  return { id: this.id, name: this.name, tool: this.tool.displayName, toolKey: this.tool.key,
254
- status: this.status, state: this.getControlState(), messages: this.messages,
255
+ status: this.status, state: this.getControlState(), messages: this.messages.map(item => this.toPublicMessage(item)),
255
256
  pendingPermissions: [
256
257
  ...this.completedPermissions,
257
258
  ...Array.from(this.pendingPermissions.values()).map(item => item.public)
258
259
  ] };
259
260
  }
260
261
 
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;
267
+
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];
273
+ }
274
+ if (item.error) message.hasError = true;
275
+ }
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;
284
+ }
285
+ message.hasDetail = hasDetail;
286
+ message.detailRevision = Number(item.updatedAt || item.createdAt || 0);
287
+ return message;
288
+ }
289
+
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 };
299
+ }
300
+
261
301
  getControlState() {
262
302
  const activeSubagentCount = Array.from(this.threadTurns.entries())
263
303
  .filter(([threadId, turn]) => threadId !== this.threadId && turn?.status === 'running').length;
@@ -292,12 +332,17 @@ class CodexStructuredSession extends EventEmitter {
292
332
  isRunning() { return this.running && (this.presentation !== 'terminal' || Boolean(this.terminalSession)); }
293
333
 
294
334
  createItem(item) { return { id: crypto.randomUUID(), createdAt: Date.now(), ...item }; }
295
- 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
+ }
296
341
  patch(id, patch) {
297
342
  const item = this.messages.find(message => message.id === id);
298
343
  if (!item) return null;
299
344
  Object.assign(item, { updatedAt: Date.now() }, patch);
300
- this.emitEvent({ type: 'message-updated', message: item });
345
+ if (!this.replayingHistory) this.emitEvent({ type: 'message-updated', message: this.toPublicMessage(item) });
301
346
  return item;
302
347
  }
303
348
  emitEvent(event) { this.emit('event', event); }
@@ -1064,10 +1109,12 @@ class CodexStructuredSession extends EventEmitter {
1064
1109
  if (!options.preserveEffort) this.effort = thread?.reasoningEffort || thread?.reasoning_effort || this.effort;
1065
1110
  this.tokenUsage = thread?.tokenUsage || thread?.token_usage || this.tokenUsage;
1066
1111
  this.messages = [];
1112
+ this.replayingHistory = true;
1067
1113
  this.completedPermissions = [];
1068
1114
  this.turnContexts.clear();
1069
1115
  this.providerItemContexts.clear();
1070
- for (const turn of thread?.turns || []) {
1116
+ try {
1117
+ for (const turn of thread?.turns || []) {
1071
1118
  const status = turn.status === 'failed' ? 'failed' : turn.status === 'interrupted' ? 'cancelled' : 'completed';
1072
1119
  const startedAt = Number(turn.startedAt || turn.createdAt || 0);
1073
1120
  const completedAt = Number(turn.completedAt || turn.updatedAt || 0);
@@ -1086,9 +1133,12 @@ class CodexStructuredSession extends EventEmitter {
1086
1133
  || (startedAtMs && completedAtMs && completedAtMs >= startedAtMs ? completedAtMs - startedAtMs : null);
1087
1134
  this.append({ kind: 'turn-end', turnId: turn.id, status, durationMs,
1088
1135
  ...(completedAtMs ? { createdAt: completedAtMs } : {}) });
1136
+ }
1137
+ if (eventText) this.append({ kind: 'event', level: 'info', text: eventText });
1138
+ } finally {
1139
+ this.replayingHistory = false;
1089
1140
  }
1090
- if (eventText) this.append({ kind: 'event', level: 'info', text: eventText });
1091
- this.emitEvent({ type: 'history-reset', messages: this.messages });
1141
+ this.emitEvent({ type: 'history-reset', messages: this.messages.map(item => this.toPublicMessage(item)) });
1092
1142
  this.emitEvent({ type: 'state', state: this.getControlState() });
1093
1143
  }
1094
1144
 
@@ -377,6 +377,16 @@ 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-detail-request') {
381
+ const codex = sessionManager.get(sessionId);
382
+ if (codex && codex.kind === 'codex-structured' && codex.presentation === 'structured') {
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
+ }));
388
+ }
389
+ }
380
390
  if (payload.type === 'codex-abort') {
381
391
  sessionManager.abortCodex(sessionId);
382
392
  }
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 '';
@@ -323,6 +337,88 @@
323
337
  container.scrollTop = shouldStickToBottom ? container.scrollHeight : previousScrollTop;
324
338
  }
325
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 });
355
+ }
356
+ else if (ids.length) void requestCodexDetails({ ids });
357
+ }
358
+
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;
382
+ }
383
+
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();
398
+ }
399
+
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}`);
405
+ }
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);
420
+ }
421
+
326
422
  function applyCodexState(state = {}) {
327
423
  const presentationChanged = state.presentation !== undefined && state.presentation !== codexState.presentation;
328
424
  codexState = { ...codexState, ...state };
@@ -379,7 +475,18 @@
379
475
  if (!pending.length) return false;
380
476
  const request = pending[codexApprovalJumpIndex % pending.length];
381
477
  codexApprovalJumpIndex = (codexApprovalJumpIndex + 1) % pending.length;
382
- 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));
383
490
  }
384
491
 
385
492
  function focusCodexApproval(permissionId, retry = true) {
@@ -443,9 +550,33 @@
443
550
 
444
551
  function applyCodexEvent(event) {
445
552
  if (!event) return;
446
- if (event.type === 'message' && event.message) codexMessages.push(event.message);
447
- 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); }
448
- else if (event.type === 'history-reset') codexMessages = event.messages || [];
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
+ }
567
+ else if (event.type === 'history-reset') {
568
+ codexMessages = event.messages || [];
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();
579
+ }
449
580
  else if (event.type === 'permission-request' && event.request) { codexPendingPermissions = [...codexPendingPermissions.filter(item => item.id !== event.request.id), event.request]; }
450
581
  else if (event.type === 'permission-updated' && event.request) codexPendingPermissions = codexPendingPermissions.map(item => item.id === event.request.id ? event.request : item);
451
582
  if (event.state) applyCodexState(event.state); else renderCodexChat();
package/lib/web/core.js CHANGED
@@ -52,6 +52,11 @@
52
52
  let selectedCodexSkill = null;
53
53
  let codexRenderFrame = null;
54
54
  let codexApprovalJumpIndex = 0;
55
+ let codexDetailRequestSeq = 0;
56
+ let codexDetailRequests = new Map();
57
+ let codexDetailRevisions = new Map();
58
+ let codexDetailRefreshTimer = null;
59
+ let codexDetailRefreshIds = new Set();
55
60
  const modifiers = { ctrl: false };
56
61
 
57
62
  function log(msg) {
@@ -143,6 +143,12 @@
143
143
  codexSkillQuery = '';
144
144
  codexSkillError = '';
145
145
  selectedCodexSkill = null;
146
+ codexDetailRequestSeq = 0;
147
+ codexDetailRequests = new Map();
148
+ codexDetailRevisions = new Map();
149
+ clearTimeout(codexDetailRefreshTimer);
150
+ codexDetailRefreshTimer = null;
151
+ codexDetailRefreshIds = new Set();
146
152
  document.getElementById('codex-model-panel').classList.remove('active');
147
153
  document.getElementById('codex-resume-panel').classList.remove('active');
148
154
  document.getElementById('codex-fork-panel').classList.remove('active');
@@ -152,6 +158,7 @@
152
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 };
153
159
  setClaudeModeEnabled(false);
154
160
  applyCodexState(codexState);
161
+ installCodexLazyDetailHandler();
155
162
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
156
163
  currentSocket = new WebSocket(protocol + '//' + window.location.host + '?sessionId=' + sessionId);
157
164
  currentSocket.onmessage = (e) => {
@@ -161,6 +168,7 @@
161
168
  codexPendingPermissions = msg.snapshot.pendingPermissions || [];
162
169
  applyCodexState(msg.snapshot.state || {});
163
170
  }
171
+ if (msg.type === 'codex-detail-response' && msg.detail) applyCodexDetailResponse(msg);
164
172
  if (msg.type === 'codex-event') {
165
173
  applyCodexEvent(msg.event);
166
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.34",
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"