glad-web 1.0.34 → 1.0.35

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,6 +4,8 @@ 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
+
7
9
  const PERMISSION_MODES = new Set(['untrusted', 'on-request', 'never']);
8
10
  const SANDBOX_MODES = new Set(['read-only', 'workspace-write', 'danger-full-access']);
9
11
  const FALLBACK_EFFORTS = ['minimal', 'low', 'medium', 'high', 'xhigh', 'ultra'];
@@ -250,14 +252,60 @@ class CodexStructuredSession extends EventEmitter {
250
252
  }
251
253
 
252
254
  snapshot() {
255
+ const historyPage = this.getMessagePage();
256
+ const { messages, ...historyPageMeta } = historyPage;
253
257
  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,
258
+ status: this.status, state: this.getControlState(), messages, historyPage: historyPageMeta,
255
259
  pendingPermissions: [
256
260
  ...this.completedPermissions,
257
261
  ...Array.from(this.pendingPermissions.values()).map(item => item.public)
258
262
  ] };
259
263
  }
260
264
 
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 };
277
+
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);
283
+ }
284
+ }
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;
297
+ }
298
+
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
+ };
307
+ }
308
+
261
309
  getControlState() {
262
310
  const activeSubagentCount = Array.from(this.threadTurns.entries())
263
311
  .filter(([threadId, turn]) => threadId !== this.threadId && turn?.status === 'running').length;
@@ -1088,7 +1136,9 @@ class CodexStructuredSession extends EventEmitter {
1088
1136
  ...(completedAtMs ? { createdAt: completedAtMs } : {}) });
1089
1137
  }
1090
1138
  if (eventText) this.append({ kind: 'event', level: 'info', text: eventText });
1091
- this.emitEvent({ type: 'history-reset', messages: this.messages });
1139
+ const historyPage = this.getMessagePage();
1140
+ const { messages, ...historyPageMeta } = historyPage;
1141
+ this.emitEvent({ type: 'history-reset', messages, historyPage: historyPageMeta });
1092
1142
  this.emitEvent({ type: 'state', state: this.getControlState() });
1093
1143
  }
1094
1144
 
@@ -377,6 +377,12 @@ 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') {
381
+ const codex = sessionManager.get(sessionId);
382
+ if (codex && codex.kind === 'codex-structured' && codex.presentation === 'structured') {
383
+ ws.send(JSON.stringify({ type: 'codex-history-page', page: codex.getMessagePage(payload.beforeId) }));
384
+ }
385
+ }
380
386
  if (payload.type === 'codex-abort') {
381
387
  sessionManager.abortCodex(sessionId);
382
388
  }
package/lib/web/codex.js CHANGED
@@ -320,7 +320,46 @@
320
320
  const current = container.firstElementChild;
321
321
  if (!current) container.appendChild(next);
322
322
  else syncCodexDom(current, next);
323
- container.scrollTop = shouldStickToBottom ? container.scrollHeight : previousScrollTop;
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;
329
+ }
330
+ }
331
+
332
+ function applyCodexHistoryPageMeta(page = {}) {
333
+ codexHistoryBeforeId = page?.beforeId || codexMessages[0]?.id || null;
334
+ codexHistoryHasMore = Boolean(page?.hasMore);
335
+ codexHistoryLoading = false;
336
+ }
337
+
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 }));
347
+ }
348
+
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];
360
+ }
361
+ applyCodexHistoryPageMeta(page);
362
+ if (uniqueOlder.length) renderCodexChat();
324
363
  }
325
364
 
326
365
  function applyCodexState(state = {}) {
@@ -445,7 +484,12 @@
445
484
  if (!event) return;
446
485
  if (event.type === 'message' && event.message) codexMessages.push(event.message);
447
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); }
448
- else if (event.type === 'history-reset') codexMessages = event.messages || [];
487
+ else if (event.type === 'history-reset') {
488
+ codexMessages = event.messages || [];
489
+ codexHistoryUserScrolled = false;
490
+ codexHistoryPrependAnchor = null;
491
+ applyCodexHistoryPageMeta(event.historyPage);
492
+ }
449
493
  else if (event.type === 'permission-request' && event.request) { codexPendingPermissions = [...codexPendingPermissions.filter(item => item.id !== event.request.id), event.request]; }
450
494
  else if (event.type === 'permission-updated' && event.request) codexPendingPermissions = codexPendingPermissions.map(item => item.id === event.request.id ? event.request : item);
451
495
  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 codexHistoryBeforeId = null;
56
+ let codexHistoryHasMore = false;
57
+ let codexHistoryLoading = false;
58
+ let codexHistoryUserScrolled = false;
59
+ let codexHistoryPrependAnchor = null;
55
60
  const modifiers = { ctrl: false };
56
61
 
57
62
  function log(msg) {
@@ -143,6 +143,11 @@
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
151
  document.getElementById('codex-model-panel').classList.remove('active');
147
152
  document.getElementById('codex-resume-panel').classList.remove('active');
148
153
  document.getElementById('codex-fork-panel').classList.remove('active');
@@ -152,6 +157,7 @@
152
157
  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
158
  setClaudeModeEnabled(false);
154
159
  applyCodexState(codexState);
160
+ document.getElementById('codex-chat-container').onscroll = handleCodexHistoryScroll;
155
161
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
156
162
  currentSocket = new WebSocket(protocol + '//' + window.location.host + '?sessionId=' + sessionId);
157
163
  currentSocket.onmessage = (e) => {
@@ -159,8 +165,10 @@
159
165
  if (msg.type === 'codex-snapshot' && msg.snapshot) {
160
166
  codexMessages = msg.snapshot.messages || [];
161
167
  codexPendingPermissions = msg.snapshot.pendingPermissions || [];
168
+ applyCodexHistoryPageMeta(msg.snapshot.historyPage);
162
169
  applyCodexState(msg.snapshot.state || {});
163
170
  }
171
+ if (msg.type === 'codex-history-page' && msg.page) applyCodexHistoryPage(msg.page);
164
172
  if (msg.type === 'codex-event') {
165
173
  applyCodexEvent(msg.event);
166
174
  if (msg.event?.type === 'presentation' && msg.event.presentation === 'terminal') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glad-web",
3
- "version": "1.0.34",
3
+ "version": "1.0.35",
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"