cc-viewer 1.6.312 → 1.6.313

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.
package/dist/index.html CHANGED
@@ -21,7 +21,7 @@
21
21
  // 整体显示大小已弃用 CSS zoom:Electron 改用 webFrame.setZoomFactor(首屏抢占见
22
22
  // electron/tab-content-preload.js),纯浏览器交由用户用浏览器自带快捷键缩放,故此处不再设 zoom。
23
23
  </script>
24
- <script type="module" crossorigin src="/assets/index-4qAcuvOR.js"></script>
24
+ <script type="module" crossorigin src="/assets/index-ByBJZi-l.js"></script>
25
25
  <link rel="modulepreload" crossorigin href="/assets/vendor-antd-Bur5ZxWE.js">
26
26
  <link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-Si44UqBp.js">
27
27
  <link rel="modulepreload" crossorigin href="/assets/vendor-mdxeditor-Cco3AQJS.js">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-viewer",
3
- "version": "1.6.312",
3
+ "version": "1.6.313",
4
4
  "description": "Claude Code Logger visualization management tool",
5
5
  "license": "MIT",
6
6
  "main": "server.js",
@@ -1,5 +1,14 @@
1
1
  /**
2
- * 在输出缓冲区从头部截断时,找到安全的截断起点,
2
+ * ANSI 转义序列安全边界工具(纯函数、零依赖,乱码残片不变量的语义基准)。
3
+ * 两个导出:
4
+ * findSafeSliceStart(buf, rawStart) —— 缓冲区"掐头"裁剪的安全起点(锚点扫描)
5
+ * splitTrailingIncomplete(buf) —— 批边界"截尾"缓带:[安全前段, 半截序列尾巴]
6
+ * 共同保证:xterm 收到的字节流中,任何删除/分批的续点都是合法解析起点,
7
+ * 半截序列永不以字面渲染。端到端验证见 test/terminal-pipeline-oracle.test.js。
8
+ */
9
+
10
+ /**
11
+ * findSafeSliceStart:在输出缓冲区从头部截断时,找到安全的截断起点,
3
12
  * 避免切片落在 ANSI 转义序列中间——丢掉 ESC[ 前缀后剩余字节
4
13
  * 会被 xterm.js 当普通文本渲染(表现为 `[9m`、`?2026l`、`6;136;136m` 类乱码)。
5
14
  *
@@ -20,8 +29,54 @@
20
29
  * 孤儿化 END(?2026l,xterm 下无害 no-op),不可能产生无 END 的 BEGIN;
21
30
  * 跨 2026 块截断的配平由调用方负责(见 pty-flood-coalescer.js 的标记剥离)。
22
31
  */
32
+ // 前向锚点扫描窗:TUI 流 ESC 密集(几十字节内必中锚点),上限只在纯文本流生效——
33
+ // 多丢 ≤4KB 且发生在 ≥45KB 保留尾部的头部,不可感知。
23
34
  const DEFAULT_SCAN_WINDOW = 4096;
35
+ // 回看判定窗:CSI 实际长度有界(最长常见真彩 SGR ≈20 字节),64 足以覆盖
36
+ // "rawStart 是否落在某序列内部"的判定;OSC 超长场景由前向 ESC 锚兜住。
24
37
  const BACK_SCAN = 64;
38
+ // 缓带上限:正常序列远小于此;超限视为畸形流(永不终结的 OSC/DCS),
39
+ // 放弃缓带按原样发出,防半截尾巴无界滞留内存/延迟。
40
+ const DEFAULT_MAX_CARRY = 4096;
41
+
42
+ /**
43
+ * 把 buf 切成 [可安全发出的前段, 尾部未终结的半截转义序列]。
44
+ * 用途:flushBatch 给每批输出包裹 DEC 2026 SYNC 标记——若批边界劈开一条序列,
45
+ * 注入的标记会吃掉它的 ESC,让后半段以字面渲染(`[9m`/`8;2;102m` 类残片的总根源)。
46
+ * 半截尾巴缓带到下一批(PTY 续写必然补全)即可保证每个被包裹的批序列完整。
47
+ * 尾部孤立高代理同样缓带(不劈 emoji 码点)。超 maxCarry 的悬挂(畸形流如
48
+ * 永不终结的 OSC)放弃缓带按原样发出,防止无界延迟/内存。
49
+ */
50
+ export function splitTrailingIncomplete(buf, maxCarry = DEFAULT_MAX_CARRY) {
51
+ if (!buf) return ['', ''];
52
+ const k = buf.lastIndexOf('\x1b');
53
+ if (k !== -1 && buf.length - k <= maxCarry) {
54
+ const intro = k + 1 < buf.length ? buf.charCodeAt(k + 1) : -1;
55
+ let complete;
56
+ if (intro === -1) {
57
+ complete = false; // 裸 ESC 收尾
58
+ } else if (intro === 0x5b) { // CSI:任何 0x40-0x7e 终字节即终结
59
+ complete = false;
60
+ for (let j = k + 2; j < buf.length; j++) {
61
+ const c = buf.charCodeAt(j);
62
+ if (c >= 0x40 && c <= 0x7e) { complete = true; break; }
63
+ }
64
+ } else if (intro === 0x5d) { // OSC:k 是最后一个 ESC → ST 不可能,只看 BEL
65
+ complete = buf.indexOf('\x07', k + 2) !== -1;
66
+ } else if (intro === 0x50) { // DCS:ST 终结需要 ESC,而 k 已是最后一个
67
+ complete = false;
68
+ } else { // 短转义:ESC + 中间字节(0x20-0x2f)* + 终字节
69
+ let j = k + 1;
70
+ while (j < buf.length && buf.charCodeAt(j) >= 0x20 && buf.charCodeAt(j) <= 0x2f) j++;
71
+ complete = j < buf.length;
72
+ }
73
+ if (!complete) return [buf.slice(0, k), buf.slice(k)];
74
+ }
75
+ // 尾部孤立高代理:配对的低代理还在路上
76
+ const last = buf.charCodeAt(buf.length - 1);
77
+ if (last >= 0xd800 && last <= 0xdbff) return [buf.slice(0, -1), buf.slice(-1)];
78
+ return [buf, ''];
79
+ }
25
80
 
26
81
  export function findSafeSliceStart(buf, rawStart, scanWindow = DEFAULT_SCAN_WINDOW) {
27
82
  if (rawStart <= 0) return 0;
@@ -6,7 +6,7 @@ import { platform, arch, homedir } from 'node:os';
6
6
  import { createRequire } from 'node:module';
7
7
  import { prepareEmbeddedShellSpawn, stripClaudeNoFlickerUnlessOptedIn } from './lib/terminal-env.js';
8
8
  import { killPtyTree } from './lib/term-signals.js';
9
- import { findSafeSliceStart } from './lib/ansi-safe-slice.js';
9
+ import { findSafeSliceStart, splitTrailingIncomplete } from './lib/ansi-safe-slice.js';
10
10
 
11
11
  const __filename = fileURLToPath(import.meta.url);
12
12
  const __dirname = dirname(__filename);
@@ -65,11 +65,18 @@ export { findSafeSliceStart };
65
65
  const SYNC_BEGIN = '\x1b[?2026h';
66
66
  const SYNC_END = '\x1b[?2026l';
67
67
 
68
- function flushBatch() {
68
+ function flushBatch(force = false) {
69
69
  batchScheduled = false;
70
70
  if (!batchBuffer) return;
71
- const chunk = SYNC_BEGIN + batchBuffer + SYNC_END;
72
- batchBuffer = '';
71
+ // 批边界半截序列缓带:每批都包 SYNC 标记,若批边界劈开一条转义序列,注入的标记
72
+ // 会吃掉它的 ESC、让后半段以字面渲染(`[9m`/`8;2;102m` 类残片的总根源)。半截尾巴
73
+ // 留到下一批(PTY 续写必然补全);force=true(进程退出)时不缓带,冲洗全部残余。
74
+ let safe = batchBuffer;
75
+ let carry = '';
76
+ if (!force) [safe, carry] = splitTrailingIncomplete(batchBuffer);
77
+ batchBuffer = carry;
78
+ if (!safe) return;
79
+ const chunk = SYNC_BEGIN + safe + SYNC_END;
73
80
  for (const cb of dataListeners) {
74
81
  try { cb(chunk); } catch { }
75
82
  }
@@ -275,7 +282,7 @@ async function _spawnClaudeImpl(proxyPort, cwd, extraArgs = [], claudePath = nul
275
282
  });
276
283
 
277
284
  ptyProcess.onExit(({ exitCode }) => {
278
- flushBatch();
285
+ flushBatch(true);
279
286
  lastExitCode = exitCode;
280
287
  ptyProcess = null;
281
288
  ptyKind = null;
@@ -455,7 +462,7 @@ async function _spawnShellImpl() {
455
462
  });
456
463
 
457
464
  ptyProcess.onExit(({ exitCode }) => {
458
- flushBatch();
465
+ flushBatch(true);
459
466
  lastExitCode = exitCode;
460
467
  ptyProcess = null;
461
468
  ptyKind = null;
@@ -488,7 +495,7 @@ export function resizePty(cols, rows) {
488
495
 
489
496
  export function killPty() {
490
497
  if (ptyProcess) {
491
- flushBatch();
498
+ flushBatch(true);
492
499
  batchBuffer = '';
493
500
  batchScheduled = false;
494
501
  // Windows:node-pty 的 ConPTY kill 有已知同步挂起问题(microsoft/node-pty#454),
@@ -5,7 +5,7 @@ import { platform, arch, homedir } from 'node:os';
5
5
  import { createRequire } from 'node:module';
6
6
  import { prepareEmbeddedShellSpawn } from './lib/terminal-env.js';
7
7
  import { killPtyTree } from './lib/term-signals.js';
8
- import { findSafeSliceStart } from './lib/ansi-safe-slice.js';
8
+ import { findSafeSliceStart, splitTrailingIncomplete } from './lib/ansi-safe-slice.js';
9
9
 
10
10
  const __filename = fileURLToPath(import.meta.url);
11
11
  const __dirname = dirname(__filename);
@@ -74,11 +74,17 @@ function maybeReap(id, s) {
74
74
  }
75
75
  }
76
76
 
77
- function flushBatch(s) {
77
+ function flushBatch(s, force = false) {
78
78
  s.batchScheduled = false;
79
79
  if (!s.batchBuffer) return;
80
- const chunk = s.batchBuffer;
81
- s.batchBuffer = '';
80
+ // 批边界半截序列缓带(同 pty-manager.flushBatch):scratch chunk 本身不包 SYNC 标记,
81
+ // 但洪泛限流器会按 flush 包裹——chunk 尾部序列完整才能保证 flush 边界不撕裂。
82
+ // force=true(进程退出)时不缓带,冲洗全部残余。
83
+ let chunk = s.batchBuffer;
84
+ let carry = '';
85
+ if (!force) [chunk, carry] = splitTrailingIncomplete(s.batchBuffer);
86
+ s.batchBuffer = carry;
87
+ if (!chunk) return;
82
88
  // snapshot 防 listener 在迭代中卸载产生跳号
83
89
  for (const cb of [...s.dataListeners]) {
84
90
  try { cb(chunk); } catch { }
@@ -159,7 +165,7 @@ export async function spawnScratch(id) {
159
165
  });
160
166
 
161
167
  s.ptyProcess.onExit(({ exitCode }) => {
162
- flushBatch(s);
168
+ flushBatch(s, true);
163
169
  s.lastExitCode = exitCode;
164
170
  s.ptyProcess = null;
165
171
  for (const cb of [...s.exitListeners]) {
@@ -208,7 +214,7 @@ export function killScratch(id) {
208
214
  const s = ptys.get(id);
209
215
  if (!s) return;
210
216
  if (s.ptyProcess) {
211
- flushBatch(s);
217
+ flushBatch(s, true);
212
218
  s.batchBuffer = '';
213
219
  s.batchScheduled = false;
214
220
  // 与 pty-manager.killPty 同款:win32 用 taskkill /T 收割 ConPTY 树,
package/server/server.js CHANGED
@@ -1262,7 +1262,8 @@ async function setupTerminalWebSocket(httpServer) {
1262
1262
  floodGate = createFloodCoalescer({
1263
1263
  send: (data) => {
1264
1264
  if (ws.readyState === 1 && bpGate.offer()) {
1265
- try { ws.send(JSON.stringify({ type: 'data', data })); } catch {}
1265
+ // send 抛错 = 该条数据永久丢失(流中间挖洞且无路径补发)→ 立即快照对齐兜底
1266
+ try { ws.send(JSON.stringify({ type: 'data', data })); } catch { sendScratchResync(); return; }
1266
1267
  _countMsg();
1267
1268
  }
1268
1269
  },
@@ -1359,6 +1360,26 @@ async function setupTerminalWebSocket(httpServer) {
1359
1360
  // 该 ws 收到第一条 resize 时(见 ws.on('message')),抖动 (rows+1) → (rows) 触发 SIGWINCH。
1360
1361
  // 注:仅 PTY 已运行时才需要兜底;shell 不在 alternate-screen 不需要。
1361
1362
  let _needRedrawBootstrap = state.running === true;
1363
+ // 加载期免疫补救:claude -c 大会话有数秒加载期,首发 SIGWINCH 若打在 TUI 渲染器
1364
+ // 挂载前会被忽略且兜底是一次性的 → 画面空白持续到用户敲键盘。数据感知延迟重试:
1365
+ // 连接以来 PTY 零输出(= 画面必然空白)才补发,有数据流过即作罢;ws close 清理。
1366
+ let _ptyDataSeen = false;
1367
+ const _redrawRetryTimers = [];
1368
+ const _nudgeRedraw = () => {
1369
+ try {
1370
+ if (process.platform !== 'win32') {
1371
+ const pid = getClaudePid();
1372
+ if (pid && pid !== process.pid) process.kill(pid, 'SIGWINCH');
1373
+ }
1374
+ } catch {}
1375
+ };
1376
+ const _armRedrawRetries = () => {
1377
+ for (const delay of [2000, 6000]) {
1378
+ const t = setTimeout(() => { if (!_ptyDataSeen) _nudgeRedraw(); }, delay);
1379
+ t.unref?.();
1380
+ _redrawRetryTimers.push(t);
1381
+ }
1382
+ };
1362
1383
 
1363
1384
  // 反压闸门:写缓冲堆积时停发 data,恢复后用 outputBuffer 快照 resync 追赶;
1364
1385
  // resync 后强制 claude TUI 全屏重绘,避免洪泛结束于 TUI 静止态时画面停在快照
@@ -1425,7 +1446,8 @@ async function setupTerminalWebSocket(httpServer) {
1425
1446
  floodGate = createFloodCoalescer({
1426
1447
  send: (data) => {
1427
1448
  if (ws.readyState === 1 && bpGate.offer()) {
1428
- try { ws.send(JSON.stringify({ type: 'data', data })); } catch {}
1449
+ // send 抛错 = 该条数据永久丢失(流中间挖洞且无路径补发)→ 立即快照对齐兜底
1450
+ try { ws.send(JSON.stringify({ type: 'data', data })); } catch { sendResync(); return; }
1429
1451
  _countMsg();
1430
1452
  }
1431
1453
  },
@@ -1443,6 +1465,7 @@ async function setupTerminalWebSocket(httpServer) {
1443
1465
 
1444
1466
  // PTY 输出 → WebSocket(合并 ws 后客户端自行按 msg.type 分发,server 端不再 role 过滤)
1445
1467
  const removeDataListener = onPtyData((data) => {
1468
+ _ptyDataSeen = true;
1446
1469
  floodGate.offer(data);
1447
1470
  });
1448
1471
 
@@ -1819,20 +1842,18 @@ async function setupTerminalWebSocket(httpServer) {
1819
1842
  // 让 PTY 短暂处于错误尺寸再回滚(避免 50-100ms 闪烁)
1820
1843
  if (_needRedrawBootstrap) {
1821
1844
  _needRedrawBootstrap = false;
1822
- try {
1823
- // Windows SIGWINCH;ConPTY 在前面的 resizePty 调用里已经处理过 resize 通知,
1824
- // 这里仅是 POSIX 上的"等尺寸 noop 不发信号"兜底,Win 上跳过避免抛异常。
1825
- if (process.platform !== 'win32') {
1826
- const pid = getClaudePid();
1827
- if (pid && pid !== process.pid) process.kill(pid, 'SIGWINCH');
1828
- }
1829
- } catch {}
1845
+ // Windows 无 SIGWINCH;ConPTY 在前面的 resizePty 调用里已经处理过 resize 通知,
1846
+ // _nudgeRedraw 仅是 POSIX 上的"等尺寸 noop 不发信号"兜底(内部跳过 win32)。
1847
+ _nudgeRedraw();
1848
+ // claude -c 加载期免疫 SIGWINCH → 零输出时延迟补发(见上方声明处注释)
1849
+ _armRedrawRetries();
1830
1850
  }
1831
1851
  }
1832
1852
  } catch {}
1833
1853
  });
1834
1854
 
1835
1855
  ws.on('close', () => {
1856
+ for (const t of _redrawRetryTimers) clearTimeout(t);
1836
1857
  bpGate.dispose();
1837
1858
  floodGate.dispose();
1838
1859
  removeDataListener();