claude-simple 0.2.35 → 0.2.37

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.
Files changed (62) hide show
  1. package/README.md +0 -0
  2. package/client.js +1 -1
  3. package/dist/addon-fit.min.js +0 -0
  4. package/dist/{app.0.2.35.min.js → app.0.2.37.min.js} +147 -147
  5. package/dist/index.html +2 -2
  6. package/dist/{style.0.2.35.min.css → style.0.2.37.min.css} +23 -0
  7. package/dist/xterm.min.css +0 -0
  8. package/dist/xterm.min.js +0 -0
  9. package/gateway.js +0 -0
  10. package/package.json +1 -1
  11. package/public/addon-fit.min.js +0 -0
  12. package/public/agent-panel.js +0 -0
  13. package/public/api.js +0 -0
  14. package/public/app.js +2 -2
  15. package/public/base.css +0 -0
  16. package/public/cache.js +0 -0
  17. package/public/chat.js +0 -0
  18. package/public/download-status.js +25 -0
  19. package/public/files.js +71 -38
  20. package/public/git.js +0 -0
  21. package/public/icons/icon-180.png +0 -0
  22. package/public/icons/icon-192.png +0 -0
  23. package/public/icons/icon-512.png +0 -0
  24. package/public/icons/icon-maskable-512.png +0 -0
  25. package/public/index.html +2 -2
  26. package/public/lib.js +0 -0
  27. package/public/manifest.webmanifest +0 -0
  28. package/public/mobile.js +0 -0
  29. package/public/notifications.js +0 -0
  30. package/public/offline.html +0 -0
  31. package/public/pwa.js +1 -1
  32. package/public/router.js +0 -0
  33. package/public/settings.js +0 -0
  34. package/public/shell/boot.js +0 -0
  35. package/public/shell/dashboard.js +0 -0
  36. package/public/shell/hub.js +0 -0
  37. package/public/shell/layout.js +0 -0
  38. package/public/shell/notes.js +0 -0
  39. package/public/shell/session-context.js +0 -0
  40. package/public/shell/session-list.js +0 -0
  41. package/public/shell/session-nav.js +0 -0
  42. package/public/shell/templates.js +0 -0
  43. package/public/shell/util.js +0 -0
  44. package/public/shell.js +0 -0
  45. package/public/state.js +0 -0
  46. package/public/style.css +23 -0
  47. package/public/sw.js +3 -3
  48. package/public/terminal.js +0 -0
  49. package/public/wake-lock.js +0 -0
  50. package/public/xterm.min.css +0 -0
  51. package/public/xterm.min.js +0 -0
  52. package/src/agents/claude.js +0 -0
  53. package/src/agents/codex.js +5 -1
  54. package/src/agents/meta-agent.js +0 -0
  55. package/src/agents/models.js +0 -0
  56. package/src/agents/sessions.js +54 -25
  57. package/src/agents/stream-batch.js +0 -0
  58. package/src/app.js +0 -0
  59. package/src/auto-update.js +0 -0
  60. package/src/runtime/approvals.js +0 -0
  61. package/src/runtime/outbox.js +0 -0
  62. package/src/runtime/protocol.js +0 -0
package/dist/index.html CHANGED
@@ -4,13 +4,13 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
6
6
  <title>Claude Code</title>
7
- <link rel="stylesheet" href="/style.0.2.35.min.css">
7
+ <link rel="stylesheet" href="/style.0.2.37.min.css">
8
8
  <link rel="stylesheet" href="/highlight.min.css">
9
9
  <script src="/marked.min.js"></script>
10
10
  <script src="/highlight.min.js"></script>
11
11
  </head>
12
12
  <body class="h-dvh overflow-hidden flex flex-col bg-base-100 text-base-content">
13
13
  <div id="root" class="flex flex-col flex-1 overflow-hidden"></div>
14
- <script type="module" src="/app.0.2.35.min.js"></script>
14
+ <script type="module" src="/app.0.2.37.min.js"></script>
15
15
  </body>
16
16
  </html>
@@ -140,6 +140,29 @@
140
140
  }
141
141
  }
142
142
 
143
+ .file-download-toast {
144
+ position: fixed;
145
+ left: 50%;
146
+ bottom: calc(1.25rem + env(safe-area-inset-bottom, 0px));
147
+ transform: translateX(-50%) translateY(8px);
148
+ z-index: 80;
149
+ max-width: min(92vw, 28rem);
150
+ padding: 0.7rem 1rem;
151
+ border-radius: 0.7rem;
152
+ background: var(--color-primary, #3b82f6);
153
+ color: var(--color-primary-content, #fff);
154
+ font-size: 0.85rem;
155
+ line-height: 1.35;
156
+ box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35);
157
+ opacity: 0;
158
+ pointer-events: none;
159
+ transition: opacity .18s ease, transform .18s ease;
160
+ }
161
+ .file-download-toast.visible {
162
+ opacity: 1;
163
+ transform: translateX(-50%) translateY(0);
164
+ }
165
+
143
166
  /* File row actions: hover-reveal on desktop only */
144
167
  @media (min-width: 641px) {
145
168
  .file-row-action {
File without changes
package/dist/xterm.min.js CHANGED
File without changes
package/gateway.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-simple",
3
- "version": "0.2.35",
3
+ "version": "0.2.37",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "claude-simple": "server.js",
File without changes
File without changes
package/public/api.js CHANGED
File without changes
package/public/app.js CHANGED
@@ -3,7 +3,7 @@ import { ctx } from './state.js';
3
3
  import { probeHub } from './api.js?v=ui-24';
4
4
  import { initShell, showApp, initAuth, resumeSession, switchTab, shouldRestoreSessionOnBoot } from './shell.js';
5
5
  import { initChat } from './chat.js?v=ui-24';
6
- import { initFilesTab } from './files.js?v=ui-24';
6
+ import { initFilesTab } from './files.js?v=ui-29';
7
7
  import { initGitTab } from './git.js';
8
8
  import { initSettings } from './settings.js';
9
9
  import { initNotes } from './notes.js';
@@ -11,7 +11,7 @@ import { initTerminal } from './terminal.js';
11
11
  import { initImagePaste } from './chat.js?v=ui-24';
12
12
  import { initMetaAgent } from './agent-panel.js';
13
13
  import { initRouter, applyInitialRoute, parseHashPath } from './router.js';
14
- import { initPwa } from './pwa.js?v=ui-24';
14
+ import { initPwa } from './pwa.js?v=ui-29';
15
15
  import { initMobileChrome } from './mobile.js';
16
16
 
17
17
  function markBootReady() {
package/public/base.css CHANGED
File without changes
package/public/cache.js CHANGED
File without changes
package/public/chat.js CHANGED
File without changes
@@ -0,0 +1,25 @@
1
+ /** Shared download status copy for the Files tab toast. */
2
+ export function sizeLabel(size) {
3
+ if (size == null) return '';
4
+ if (size > 1024 * 1024) return `${(size / (1024 * 1024)).toFixed(1)} MB`;
5
+ if (size > 1024) return `${(size / 1024).toFixed(1)} KB`;
6
+ return `${size} B`;
7
+ }
8
+
9
+ export function formatDownloadStatus(name, received, total) {
10
+ const n = name || 'file';
11
+ if (total > 0 && received != null) {
12
+ const pct = Math.min(100, Math.round((received / total) * 100));
13
+ return `正在下载 ${n}(${pct}%)`;
14
+ }
15
+ if (received > 0) return `正在下载 ${n}(${sizeLabel(received)})`;
16
+ return `正在下载 ${n}…`;
17
+ }
18
+
19
+ export function formatDownloadDone(name) {
20
+ return `已保存 ${name || 'file'},请查看浏览器下载栏`;
21
+ }
22
+
23
+ export function formatDownloadHandedOff(name) {
24
+ return `已交给浏览器下载 ${name || 'file'}`;
25
+ }
package/public/files.js CHANGED
@@ -6,6 +6,12 @@ import { resolveWorkspaceFile } from './workspace-path.js?v=ui-23';
6
6
  import { renderMermaid } from './mermaid.js';
7
7
  import { renderMath } from './math.js';
8
8
  import { parseMarkdownWithMath } from './markdown.js';
9
+ import {
10
+ sizeLabel,
11
+ formatDownloadStatus,
12
+ formatDownloadDone,
13
+ formatDownloadHandedOff,
14
+ } from './download-status.js?v=ui-29';
9
15
 
10
16
  // ── Helpers ───────────────────────────────────────────────────────────────────
11
17
  function fileIcon(name) {
@@ -33,13 +39,6 @@ function isMarkdownFile(path) {
33
39
  return ext === 'md' || ext === 'markdown' || ext === 'mdx';
34
40
  }
35
41
 
36
- function sizeLabel(size) {
37
- if (size == null) return '';
38
- if (size > 1024 * 1024) return `${(size / (1024 * 1024)).toFixed(1)} MB`;
39
- if (size > 1024) return `${(size / 1024).toFixed(1)} KB`;
40
- return `${size} B`;
41
- }
42
-
43
42
  function base64ToArrayBuffer(b64) {
44
43
  const bin = atob(b64);
45
44
  const bytes = new Uint8Array(bin.length);
@@ -381,35 +380,75 @@ function triggerUpload() {
381
380
  input.click();
382
381
  }
383
382
 
383
+ let downloadBusy = false;
384
+ let downloadToastTimer = 0;
385
+
386
+ function downloadToastEl() {
387
+ let el = document.getElementById('file-download-toast');
388
+ if (el) return el;
389
+ el = document.createElement('div');
390
+ el.id = 'file-download-toast';
391
+ el.className = 'file-download-toast';
392
+ el.setAttribute('role', 'status');
393
+ el.setAttribute('aria-live', 'polite');
394
+ el.hidden = true;
395
+ document.body.appendChild(el);
396
+ return el;
397
+ }
398
+
399
+ function showDownloadToast(text, { sticky = false } = {}) {
400
+ const el = downloadToastEl();
401
+ el.textContent = text;
402
+ el.hidden = false;
403
+ el.classList.add('visible');
404
+ clearTimeout(downloadToastTimer);
405
+ if (!sticky) {
406
+ downloadToastTimer = setTimeout(() => {
407
+ el.classList.remove('visible');
408
+ }, 5000);
409
+ }
410
+ }
411
+
412
+ function triggerBrowserDownload(href, name) {
413
+ const a = document.createElement('a');
414
+ a.href = href;
415
+ a.download = name;
416
+ a.rel = 'noopener';
417
+ a.style.display = 'none';
418
+ document.body.appendChild(a);
419
+ a.click();
420
+ a.remove();
421
+ }
422
+
384
423
  async function downloadFile(proj, filePath) {
385
424
  const root = filesRoot.peek() || proj?.path || '';
386
425
  if (!root) {
387
426
  alert('No path selected — set root first');
388
427
  return;
389
428
  }
429
+ if (downloadBusy) {
430
+ showDownloadToast('已有文件正在下载');
431
+ return;
432
+ }
390
433
  const id = '_';
391
434
  const name = (filePath || '').split('/').pop() || 'download';
392
- // Prefer Authorization header (no token in URL / logs). authQuery kept as
393
- // fallback for environments that only support plain navigations.
394
435
  const qs = new URLSearchParams({ root, path: filePath, download: 'true' });
395
436
  const url = `/api/projects/${id}/file?${qs}`;
437
+ downloadBusy = true;
438
+ showDownloadToast(formatDownloadStatus(name), { sticky: true });
396
439
  try {
397
- const handle = typeof window.showSaveFilePicker === 'function'
398
- ? await window.showSaveFilePicker({ suggestedName: name }) : null;
399
440
  const res = await fetch(url, { headers: authHeaders(), credentials: 'same-origin' });
400
441
  if (!res.ok) {
401
442
  const d = await res.json().catch(() => ({}));
402
443
  throw new Error(d.error || `HTTP ${res.status}`);
403
444
  }
404
- if (handle && res.body) {
405
- await res.body.pipeTo(await handle.createWritable());
406
- return;
407
- }
408
- // Browsers without a file sink hand large downloads to their download manager.
409
445
  const maxBlob = 16 * 1024 * 1024;
410
- if (Number(res.headers.get('content-length')) > maxBlob) {
446
+ const total = Number(res.headers.get('content-length')) || 0;
447
+ if (total > maxBlob || !res.body) {
411
448
  await res.body?.cancel();
412
- throw new Error('Use native download');
449
+ triggerBrowserDownload(`/api/projects/${id}/file?${authQuery({ root, path: filePath, download: 'true' })}`, name);
450
+ showDownloadToast(formatDownloadHandedOff(name));
451
+ return;
413
452
  }
414
453
  const reader = res.body.getReader();
415
454
  const chunks = []; let bytes = 0;
@@ -417,37 +456,31 @@ async function downloadFile(proj, filePath) {
417
456
  const { done, value } = await reader.read();
418
457
  if (done) break;
419
458
  bytes += value.byteLength;
420
- if (bytes > maxBlob) { await reader.cancel(); throw new Error('Use native download'); }
459
+ if (bytes > maxBlob) {
460
+ await reader.cancel();
461
+ triggerBrowserDownload(`/api/projects/${id}/file?${authQuery({ root, path: filePath, download: 'true' })}`, name);
462
+ showDownloadToast(formatDownloadHandedOff(name));
463
+ return;
464
+ }
421
465
  chunks.push(value);
466
+ showDownloadToast(formatDownloadStatus(name, bytes, total), { sticky: true });
422
467
  }
423
468
  const blob = new Blob(chunks, { type: res.headers.get('content-type') || 'application/octet-stream' });
424
469
  const objectUrl = URL.createObjectURL(blob);
425
- const a = document.createElement('a');
426
- a.href = objectUrl;
427
- a.download = name;
428
- a.rel = 'noopener';
429
- a.style.display = 'none';
430
- document.body.appendChild(a);
431
- a.click();
432
- a.remove();
433
- // iOS may need the URL briefly; revoke after a beat.
470
+ triggerBrowserDownload(objectUrl, name);
434
471
  setTimeout(() => URL.revokeObjectURL(objectUrl), 60_000);
472
+ showDownloadToast(formatDownloadDone(name));
435
473
  } catch (e) {
436
474
  if (e.name === 'AbortError') return;
437
- // Last resort: open with token query (works when fetch is blocked, e.g. some WebViews).
438
475
  try {
439
- const fallback = `/api/projects/${id}/file?${authQuery({ root, path: filePath, download: 'true' })}`;
440
- const a = document.createElement('a');
441
- a.href = fallback;
442
- a.download = name;
443
- a.rel = 'noopener';
444
- a.target = '_blank';
445
- document.body.appendChild(a);
446
- a.click();
447
- a.remove();
476
+ triggerBrowserDownload(`/api/projects/${id}/file?${authQuery({ root, path: filePath, download: 'true' })}`, name);
477
+ showDownloadToast(formatDownloadHandedOff(name));
448
478
  } catch {
479
+ showDownloadToast(`下载失败:${e.message}`);
449
480
  alert(`Download failed: ${e.message}`);
450
481
  }
482
+ } finally {
483
+ downloadBusy = false;
451
484
  }
452
485
  }
453
486
 
package/public/git.js CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
package/public/index.html CHANGED
@@ -85,7 +85,7 @@
85
85
  <script src="/vendor/mermaid.min.js"></script>
86
86
  <link rel="stylesheet" href="/vendor/katex.min.css">
87
87
  <script src="/vendor/katex.min.js"></script>
88
- <link rel="stylesheet" href="style.css?v=ui-28">
88
+ <link rel="stylesheet" href="style.css?v=ui-29">
89
89
  </head>
90
90
  <body class="h-dvh overflow-hidden flex flex-col bg-base-100 text-base-content">
91
91
  <div id="root" class="flex flex-col flex-1 overflow-hidden"></div>
@@ -101,6 +101,6 @@
101
101
  }
102
102
  }, 10000);
103
103
  </script>
104
- <script type="module" src="app.js?v=ui-28"></script>
104
+ <script type="module" src="app.js?v=ui-29"></script>
105
105
  </body>
106
106
  </html>
package/public/lib.js CHANGED
File without changes
File without changes
package/public/mobile.js CHANGED
File without changes
File without changes
File without changes
package/public/pwa.js CHANGED
@@ -69,7 +69,7 @@ export function initPwa() {
69
69
 
70
70
  // Register after first paint so install doesn't compete with boot.
71
71
  const register = () => {
72
- navigator.serviceWorker.register('/sw.js?v=33', { scope: '/' })
72
+ navigator.serviceWorker.register('/sw.js?v=38', { scope: '/' })
73
73
  .then((reg) => {
74
74
  reg.addEventListener('updatefound', () => {
75
75
  const sw = reg.installing;
package/public/router.js CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/public/shell.js CHANGED
File without changes
package/public/state.js CHANGED
File without changes
package/public/style.css CHANGED
@@ -138,6 +138,29 @@
138
138
  }
139
139
  }
140
140
 
141
+ .file-download-toast {
142
+ position: fixed;
143
+ left: 50%;
144
+ bottom: calc(1.25rem + env(safe-area-inset-bottom, 0px));
145
+ transform: translateX(-50%) translateY(8px);
146
+ z-index: 80;
147
+ max-width: min(92vw, 28rem);
148
+ padding: 0.7rem 1rem;
149
+ border-radius: 0.7rem;
150
+ background: var(--color-primary, #3b82f6);
151
+ color: var(--color-primary-content, #fff);
152
+ font-size: 0.85rem;
153
+ line-height: 1.35;
154
+ box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35);
155
+ opacity: 0;
156
+ pointer-events: none;
157
+ transition: opacity .18s ease, transform .18s ease;
158
+ }
159
+ .file-download-toast.visible {
160
+ opacity: 1;
161
+ transform: translateX(-50%) translateY(0);
162
+ }
163
+
141
164
  /* File row actions: hover-reveal on desktop only */
142
165
  @media (min-width: 641px) {
143
166
  .file-row-action {
package/public/sw.js CHANGED
@@ -1,12 +1,12 @@
1
1
  /* Agent UI service worker — cache shell assets only.
2
2
  * Never intercepts WebSocket or mutates API semantics beyond offline fallback.
3
3
  * Bump CACHE when shipping UI that must invalidate old shells. */
4
- const CACHE = 'agent-ui-shell-v37';
4
+ const CACHE = 'agent-ui-shell-v38';
5
5
  const PRECACHE = [
6
6
  '/',
7
7
  '/index.html',
8
8
  '/manifest.webmanifest',
9
- '/style.css?v=ui-28',
9
+ '/style.css?v=ui-29',
10
10
  '/style.css',
11
11
  '/ui.min.css',
12
12
  '/vendor/mini-react.min.js',
@@ -16,7 +16,7 @@ const PRECACHE = [
16
16
  '/vendor/mermaid.min.js',
17
17
  '/vendor/katex.min.js',
18
18
  '/vendor/katex.min.css',
19
- '/app.js?v=ui-28',
19
+ '/app.js?v=ui-29',
20
20
  '/icons/icon-192.png',
21
21
  '/icons/icon-512.png',
22
22
  '/icons/icon-maskable-512.png',
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -12,6 +12,10 @@ import crypto from 'crypto';
12
12
  * result (usage), complete, error
13
13
  */
14
14
 
15
+ export function isTransientCodexNotice(msg) {
16
+ return /Reconnecting|Falling back|remote compact task/i.test(String(msg || ''));
17
+ }
18
+
15
19
  function mapPermissionToMode(permissionMode) {
16
20
  if (permissionMode == null) return 'yolo';
17
21
  switch (permissionMode) {
@@ -244,7 +248,7 @@ export function runCodex(command, options, send, { cliPath, activeSessions }) {
244
248
  }
245
249
  case 'error': {
246
250
  const msg = raw.message || 'codex error';
247
- if (!/Reconnecting|Falling back/i.test(msg)) {
251
+ if (!isTransientCodexNotice(msg)) {
248
252
  send({ type: 'error', message: msg, agent: 'codex' });
249
253
  }
250
254
  break;
File without changes
File without changes
@@ -313,36 +313,63 @@ async function findCodexRollouts(codexHome) {
313
313
  return files;
314
314
  }
315
315
 
316
+ const CODEX_CATALOG_FULL_SCAN_MAX = 2 * 1024 * 1024;
317
+ const CODEX_CATALOG_HEAD_BYTES = 256 * 1024;
318
+ const CODEX_LINE_PARSE_MAX = 16 * 1024;
319
+
316
320
  const codexMetadata = new Map();
317
321
  async function readCodexMeta(filePath) {
318
322
  const stat = await fs.stat(filePath);
319
323
  const cached = codexMetadata.get(filePath);
320
324
  if (cached?.size === stat.size && cached?.mtimeMs === stat.mtimeMs) return cached.meta;
321
325
  let id = null, cwd = null, ts = null, turnCount = 0;
326
+ // Native rollouts keep every tool payload. AgentUI / Hub never show that
327
+ // body, so catalog only reads a short head (session_meta is line 0).
328
+ const limit = stat.size > CODEX_CATALOG_FULL_SCAN_MAX
329
+ ? Math.min(stat.size, CODEX_CATALOG_HEAD_BYTES)
330
+ : stat.size;
322
331
  try {
323
- // Full scan: meta is early; turnCount needs whole file
324
- const stream = fsSync.createReadStream(filePath);
325
- const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
326
- for await (const line of rl) {
327
- const head = line.slice(0, 400);
328
- if (!/"type"\s*:\s*"(?:session_meta|turn_context)"/.test(head) &&
329
- !/"(?:role|type)"\s*:\s*"(?:user|user_message)"/.test(head)) continue;
330
- try {
331
- const d = JSON.parse(line);
332
- if (d.type === 'session_meta' && d.payload) {
333
- id = d.payload.id || id;
334
- cwd = d.payload.cwd || cwd;
335
- ts = d.payload.timestamp || d.timestamp || ts;
332
+ if (limit > 0) {
333
+ const stream = fsSync.createReadStream(filePath, { start: 0, end: limit - 1 });
334
+ let pending = Buffer.alloc(0);
335
+ let skip = false;
336
+ for await (const chunk of stream) {
337
+ pending = pending.length ? Buffer.concat([pending, chunk]) : chunk;
338
+ let start = 0;
339
+ let end;
340
+ while ((end = pending.indexOf(10, start)) !== -1) {
341
+ const len = end - start;
342
+ const overflow = skip || len > CODEX_LINE_PARSE_MAX;
343
+ skip = false;
344
+ if (!overflow) {
345
+ const line = pending.subarray(start, end).toString('utf8');
346
+ const head = line.slice(0, 400);
347
+ if (/"type"\s*:\s*"(?:session_meta|turn_context)"/.test(head) ||
348
+ /"(?:role|type)"\s*:\s*"(?:user|user_message)"/.test(head)) {
349
+ try {
350
+ const d = JSON.parse(line);
351
+ if (d.type === 'session_meta' && d.payload) {
352
+ id = d.payload.id || id;
353
+ cwd = d.payload.cwd || cwd;
354
+ ts = d.payload.timestamp || d.timestamp || ts;
355
+ }
356
+ if (d.type === 'event_msg' && d.payload?.type === 'user_message') turnCount += 1;
357
+ else if (d.type === 'response_item' && d.payload?.role === 'user') turnCount += 1;
358
+ else if (d.type === 'turn_context' && d.payload?.turn_index != null) {
359
+ const idx = Number(d.payload.turn_index) + 1;
360
+ if (idx > turnCount) turnCount = idx;
361
+ }
362
+ } catch {}
363
+ }
364
+ }
365
+ start = end + 1;
336
366
  }
337
- // Codex user turns appear in a few event shapes across versions
338
- if (d.type === 'event_msg' && d.payload?.type === 'user_message') turnCount += 1;
339
- else if (d.type === 'response_item' && d.payload?.role === 'user') turnCount += 1;
340
- else if (d.type === 'turn_context' && d.payload?.turn_index != null) {
341
- // last turn_index + 1 is a better total; bump max
342
- const idx = Number(d.payload.turn_index) + 1;
343
- if (idx > turnCount) turnCount = idx;
367
+ pending = pending.subarray(start);
368
+ if (pending.length > CODEX_LINE_PARSE_MAX) {
369
+ skip = true;
370
+ pending = Buffer.alloc(0);
344
371
  }
345
- } catch {}
372
+ }
346
373
  }
347
374
  } catch {}
348
375
  // Fallback: parse id from filename rollout-...-<uuid>.jsonl
@@ -472,11 +499,13 @@ async function updateCodexTranscript(filePath, entry, stat) {
472
499
  let start = 0;
473
500
  let end;
474
501
  while ((end = pending.indexOf(10, start)) !== -1) {
475
- const line = pending.subarray(start, end).toString('utf8');
476
- entry.offset += end - start + 1;
502
+ const lineBytes = end - start;
503
+ entry.offset += lineBytes + 1;
477
504
  start = end + 1;
478
- // Reasoning and tool outputs dominate large rollouts. Do not JSON-parse
479
- // their multi-megabyte payloads when building the visible transcript.
505
+ // Tool outputs are multi-MB JSONL lines. AgentUI does not render them
506
+ // and Hub must not receive them — skip without materializing the body.
507
+ if (lineBytes > 64 * 1024) continue;
508
+ const line = pending.subarray(start - lineBytes - 1, end).toString('utf8');
480
509
  const head = line.slice(0, 400);
481
510
  if (!/"type"\s*:\s*"(?:response_item|event_msg)"/.test(head) ||
482
511
  !/"type"\s*:\s*"(?:message|agent_message|function_call|command_execution|custom_tool_call|task_started|task_complete|turn_aborted)"/.test(head)) continue;
File without changes
package/src/app.js CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes