cc-viewer 1.6.311 → 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/assets/{App-T5FQTsBZ.js → App-DVWiEmB2.js} +1 -1
- package/dist/assets/{MdxEditorPanel-CEi81Xx8.js → MdxEditorPanel-DPZ3CV2s.js} +1 -1
- package/dist/assets/{Mobile-D0CPuF4p.js → Mobile-Dwue8DFB.js} +1 -1
- package/dist/assets/{index-DqUPEFNT.js → index-ByBJZi-l.js} +2 -2
- package/dist/assets/{seqResourceLoaders-Db3xRWgK.css → seqResourceLoaders-CX6xejM7.css} +1 -1
- package/dist/assets/seqResourceLoaders-CXTfIb_N.js +2 -0
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/server/lib/ansi-safe-slice.js +131 -0
- package/server/lib/pty-flood-coalescer.js +21 -1
- package/server/pty-manager.js +22 -49
- package/server/scratch-pty-manager.js +15 -26
- package/server/server.js +96 -34
- package/dist/assets/seqResourceLoaders-BE3I8wAc.js +0 -2
package/server/server.js
CHANGED
|
@@ -1166,6 +1166,9 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1166
1166
|
|
|
1167
1167
|
// resync nudge 冷却(CCV_RESYNC_NUDGE_COOLDOWN_MS,0 = 不冷却;详见 lib/resync-nudge-gate.js)
|
|
1168
1168
|
const RESYNC_NUDGE_COOLDOWN_MS = envIntAllowZero('CCV_RESYNC_NUDGE_COOLDOWN_MS', 3000);
|
|
1169
|
+
// 客户端 resync-request 的服务端冷却兜底(客户端 write-queue trim 后自带节流,这里防风暴;
|
|
1170
|
+
// 复用 nudge 门实现,0 = 不冷却)
|
|
1171
|
+
const RESYNC_REQ_COOLDOWN_MS = envIntAllowZero('CCV_RESYNC_REQ_COOLDOWN_MS', 1000);
|
|
1169
1172
|
|
|
1170
1173
|
// 洪泛限流器状态日志(与 makeBpLogger 同款 5s 节流,独立实例不共享计数)。
|
|
1171
1174
|
// Windows 实机排"切主题/大流量卡死"时据此确认 ConPTY 洪泛是否触发、几次、量级。
|
|
@@ -1228,6 +1231,13 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1228
1231
|
// floodGate 在 bpGate 之后构造(send 闭包依赖 bpGate),onBehind/onResume 经 let 前向引用 reset:
|
|
1229
1232
|
// resync 快照是唯一真相源,coalescer 残留 pending 不清会把早于快照的旧字节回灌导致画面回退。
|
|
1230
1233
|
let floodGate = null;
|
|
1234
|
+
// 权威快照对齐:bpGate.onResume / floodGate.onTruncate / 客户端 resync-request 三路共用。
|
|
1235
|
+
// 调用前须 floodGate.reset()(快照已含 pending 字节,残留会在快照后回灌重复)。
|
|
1236
|
+
const sendScratchResync = () => {
|
|
1237
|
+
if (ws.readyState !== 1) return;
|
|
1238
|
+
try { ws.send(JSON.stringify({ type: 'data-resync', data: getScratchOutputBuffer(id) })); } catch {}
|
|
1239
|
+
};
|
|
1240
|
+
const resyncReqGate = createResyncNudgeGate({ cooldownMs: RESYNC_REQ_COOLDOWN_MS });
|
|
1231
1241
|
const bpGate = createBackpressureGate({
|
|
1232
1242
|
getBufferedAmount: () => ws.bufferedAmount,
|
|
1233
1243
|
onBehind: (buffered) => {
|
|
@@ -1237,8 +1247,7 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1237
1247
|
onResume: (buffered) => {
|
|
1238
1248
|
_bpLog('resume', buffered);
|
|
1239
1249
|
floodGate?.reset();
|
|
1240
|
-
|
|
1241
|
-
try { ws.send(JSON.stringify({ type: 'data-resync', data: getScratchOutputBuffer(id) })); } catch {}
|
|
1250
|
+
sendScratchResync();
|
|
1242
1251
|
},
|
|
1243
1252
|
onTimeout: (buffered) => {
|
|
1244
1253
|
_bpLog('timeout', buffered);
|
|
@@ -1253,13 +1262,20 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1253
1262
|
floodGate = createFloodCoalescer({
|
|
1254
1263
|
send: (data) => {
|
|
1255
1264
|
if (ws.readyState === 1 && bpGate.offer()) {
|
|
1256
|
-
|
|
1265
|
+
// send 抛错 = 该条数据永久丢失(流中间挖洞且无路径补发)→ 立即快照对齐兜底
|
|
1266
|
+
try { ws.send(JSON.stringify({ type: 'data', data })); } catch { sendScratchResync(); return; }
|
|
1257
1267
|
_countMsg();
|
|
1258
1268
|
}
|
|
1259
1269
|
},
|
|
1260
1270
|
findSafeSliceStart,
|
|
1261
1271
|
onFloodStart: (bytes) => _floodLog('start', bytes),
|
|
1262
1272
|
onFloodEnd: () => _floodLog('end', 0),
|
|
1273
|
+
// 洪泛期丢过中段:增量输出流不会自愈,回落直通后用权威快照对齐一次
|
|
1274
|
+
onTruncate: (dropped) => {
|
|
1275
|
+
_floodLog('truncate', dropped);
|
|
1276
|
+
floodGate.reset();
|
|
1277
|
+
sendScratchResync();
|
|
1278
|
+
},
|
|
1263
1279
|
});
|
|
1264
1280
|
|
|
1265
1281
|
const removeDataListener = onScratchData(id, (data) => {
|
|
@@ -1283,6 +1299,12 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1283
1299
|
writeScratch(id, msg.data);
|
|
1284
1300
|
} else if (msg.type === 'resize') {
|
|
1285
1301
|
resizeScratch(id, msg.cols, msg.rows);
|
|
1302
|
+
} else if (msg.type === 'resync-request') {
|
|
1303
|
+
// 客户端 write-queue 积压丢弃后请求快照对齐(utils/terminalWriteQueue.js onTrim)
|
|
1304
|
+
if (resyncReqGate.shouldNudge()) {
|
|
1305
|
+
floodGate.reset();
|
|
1306
|
+
sendScratchResync();
|
|
1307
|
+
}
|
|
1286
1308
|
} else if (msg.type === 'kill') {
|
|
1287
1309
|
// 用户主动关闭 tab:杀 pty(killScratch 内部 ptys.delete 后配额自动释放);前端会随后 close ws
|
|
1288
1310
|
killScratch(id);
|
|
@@ -1338,6 +1360,26 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1338
1360
|
// 该 ws 收到第一条 resize 时(见 ws.on('message')),抖动 (rows+1) → (rows) 触发 SIGWINCH。
|
|
1339
1361
|
// 注:仅 PTY 已运行时才需要兜底;shell 不在 alternate-screen 不需要。
|
|
1340
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
|
+
};
|
|
1341
1383
|
|
|
1342
1384
|
// 反压闸门:写缓冲堆积时停发 data,恢复后用 outputBuffer 快照 resync 追赶;
|
|
1343
1385
|
// resync 后强制 claude TUI 全屏重绘,避免洪泛结束于 TUI 静止态时画面停在快照
|
|
@@ -1352,6 +1394,34 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1352
1394
|
// 走冷却——nudge 让 ConPTY 再吐全屏重绘 = 新洪泛燃料,紧 behind→resume 循环里反复
|
|
1353
1395
|
// nudge 会自我维持(客户端每轮 reset+重放快照 = 永久冻结表象)。详见 lib/resync-nudge-gate.js。
|
|
1354
1396
|
const nudgeGate = createResyncNudgeGate({ cooldownMs: RESYNC_NUDGE_COOLDOWN_MS });
|
|
1397
|
+
const resyncReqGate = createResyncNudgeGate({ cooldownMs: RESYNC_REQ_COOLDOWN_MS });
|
|
1398
|
+
// 权威快照对齐:data-resync 快照无条件发,全屏重绘 nudge 走 nudgeGate 冷却。
|
|
1399
|
+
// bpGate.onResume / floodGate.onTruncate / 客户端 resync-request 三路共用;
|
|
1400
|
+
// 调用前须 floodGate.reset()(快照已含 pending/微合并缓冲字节,残留会在快照后回灌重复)。
|
|
1401
|
+
const sendResync = (buffered = 0) => {
|
|
1402
|
+
if (ws.readyState !== 1) return;
|
|
1403
|
+
try { ws.send(JSON.stringify({ type: 'data-resync', data: getOutputBuffer() })); } catch {}
|
|
1404
|
+
if (!nudgeGate.shouldNudge()) { _bpLog('nudge-skip', buffered); return; }
|
|
1405
|
+
try {
|
|
1406
|
+
if (process.platform !== 'win32') {
|
|
1407
|
+
// POSIX:与下方 _needRedrawBootstrap 同款 SIGWINCH 兜底
|
|
1408
|
+
const pid = getClaudePid();
|
|
1409
|
+
if (pid && pid !== process.pid) process.kill(pid, 'SIGWINCH');
|
|
1410
|
+
} else {
|
|
1411
|
+
// Windows 无 SIGWINCH:resize 抖动经 ConPTY 通知重绘(恢复路径偶发,闪烁可接受)。
|
|
1412
|
+
// 尺寸仲裁与 resize 消息处理一致:移动端优先,否则活跃客户端(activeWs 为 null 时
|
|
1413
|
+
// 本 ws 视为所有者)——恢复的 ws 可能是非权威的慢后台 tab,用它自己的尺寸抖动会把
|
|
1414
|
+
// 共享 PTY 永久改成它的尺寸、挤掉活跃/移动端画面;无权威尺寸则跳过抖动。
|
|
1415
|
+
const mSize = getMobileSize();
|
|
1416
|
+
const size = mSize
|
|
1417
|
+
|| ((activeWs === ws || activeWs === null) ? clientSizes.get(ws) : clientSizes.get(activeWs));
|
|
1418
|
+
if (size) {
|
|
1419
|
+
resizePty(size.cols, size.rows + 1);
|
|
1420
|
+
resizePty(size.cols, size.rows);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
} catch {}
|
|
1424
|
+
};
|
|
1355
1425
|
const bpGate = createBackpressureGate({
|
|
1356
1426
|
getBufferedAmount: () => ws.bufferedAmount,
|
|
1357
1427
|
onBehind: (buffered) => {
|
|
@@ -1361,28 +1431,7 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1361
1431
|
onResume: (buffered) => {
|
|
1362
1432
|
_bpLog('resume', buffered);
|
|
1363
1433
|
floodGate?.reset();
|
|
1364
|
-
|
|
1365
|
-
try { ws.send(JSON.stringify({ type: 'data-resync', data: getOutputBuffer() })); } catch {}
|
|
1366
|
-
if (!nudgeGate.shouldNudge()) { _bpLog('nudge-skip', buffered); return; }
|
|
1367
|
-
try {
|
|
1368
|
-
if (process.platform !== 'win32') {
|
|
1369
|
-
// POSIX:与下方 _needRedrawBootstrap 同款 SIGWINCH 兜底
|
|
1370
|
-
const pid = getClaudePid();
|
|
1371
|
-
if (pid && pid !== process.pid) process.kill(pid, 'SIGWINCH');
|
|
1372
|
-
} else {
|
|
1373
|
-
// Windows 无 SIGWINCH:resize 抖动经 ConPTY 通知重绘(恢复路径偶发,闪烁可接受)。
|
|
1374
|
-
// 尺寸仲裁与 resize 消息处理一致:移动端优先,否则活跃客户端(activeWs 为 null 时
|
|
1375
|
-
// 本 ws 视为所有者)——恢复的 ws 可能是非权威的慢后台 tab,用它自己的尺寸抖动会把
|
|
1376
|
-
// 共享 PTY 永久改成它的尺寸、挤掉活跃/移动端画面;无权威尺寸则跳过抖动。
|
|
1377
|
-
const mSize = getMobileSize();
|
|
1378
|
-
const size = mSize
|
|
1379
|
-
|| ((activeWs === ws || activeWs === null) ? clientSizes.get(ws) : clientSizes.get(activeWs));
|
|
1380
|
-
if (size) {
|
|
1381
|
-
resizePty(size.cols, size.rows + 1);
|
|
1382
|
-
resizePty(size.cols, size.rows);
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
} catch {}
|
|
1434
|
+
sendResync(buffered);
|
|
1386
1435
|
},
|
|
1387
1436
|
onTimeout: (buffered) => {
|
|
1388
1437
|
_bpLog('timeout', buffered);
|
|
@@ -1397,17 +1446,26 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1397
1446
|
floodGate = createFloodCoalescer({
|
|
1398
1447
|
send: (data) => {
|
|
1399
1448
|
if (ws.readyState === 1 && bpGate.offer()) {
|
|
1400
|
-
|
|
1449
|
+
// send 抛错 = 该条数据永久丢失(流中间挖洞且无路径补发)→ 立即快照对齐兜底
|
|
1450
|
+
try { ws.send(JSON.stringify({ type: 'data', data })); } catch { sendResync(); return; }
|
|
1401
1451
|
_countMsg();
|
|
1402
1452
|
}
|
|
1403
1453
|
},
|
|
1404
1454
|
findSafeSliceStart,
|
|
1405
1455
|
onFloodStart: (bytes) => _floodLog('start', bytes),
|
|
1406
1456
|
onFloodEnd: () => _floodLog('end', 0),
|
|
1457
|
+
// 洪泛期丢过中段:安全切片只保证残片不上屏,被截掉的内容对增量 TUI 流
|
|
1458
|
+
// (macOS/Linux forkpty)不会自愈——回落直通后用权威快照对齐一次
|
|
1459
|
+
onTruncate: (dropped) => {
|
|
1460
|
+
_floodLog('truncate', dropped);
|
|
1461
|
+
floodGate.reset();
|
|
1462
|
+
sendResync();
|
|
1463
|
+
},
|
|
1407
1464
|
});
|
|
1408
1465
|
|
|
1409
1466
|
// PTY 输出 → WebSocket(合并 ws 后客户端自行按 msg.type 分发,server 端不再 role 过滤)
|
|
1410
1467
|
const removeDataListener = onPtyData((data) => {
|
|
1468
|
+
_ptyDataSeen = true;
|
|
1411
1469
|
floodGate.offer(data);
|
|
1412
1470
|
});
|
|
1413
1471
|
|
|
@@ -1488,6 +1546,12 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1488
1546
|
} else {
|
|
1489
1547
|
replyDone(false);
|
|
1490
1548
|
}
|
|
1549
|
+
} else if (msg.type === 'resync-request') {
|
|
1550
|
+
// 客户端 write-queue 积压丢弃后请求快照对齐(utils/terminalWriteQueue.js onTrim)
|
|
1551
|
+
if (resyncReqGate.shouldNudge()) {
|
|
1552
|
+
floodGate.reset();
|
|
1553
|
+
sendResync();
|
|
1554
|
+
}
|
|
1491
1555
|
} else if (msg.type === 'ask-hook-answer') {
|
|
1492
1556
|
// Client answered AskUserQuestion via hook bridge.
|
|
1493
1557
|
// New protocol: msg.id required to address one of multiple pending asks.
|
|
@@ -1778,20 +1842,18 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1778
1842
|
// 让 PTY 短暂处于错误尺寸再回滚(避免 50-100ms 闪烁)
|
|
1779
1843
|
if (_needRedrawBootstrap) {
|
|
1780
1844
|
_needRedrawBootstrap = false;
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
if (pid && pid !== process.pid) process.kill(pid, 'SIGWINCH');
|
|
1787
|
-
}
|
|
1788
|
-
} catch {}
|
|
1845
|
+
// Windows 无 SIGWINCH;ConPTY 在前面的 resizePty 调用里已经处理过 resize 通知,
|
|
1846
|
+
// _nudgeRedraw 仅是 POSIX 上的"等尺寸 noop 不发信号"兜底(内部跳过 win32)。
|
|
1847
|
+
_nudgeRedraw();
|
|
1848
|
+
// claude -c 加载期免疫 SIGWINCH → 零输出时延迟补发(见上方声明处注释)
|
|
1849
|
+
_armRedrawRetries();
|
|
1789
1850
|
}
|
|
1790
1851
|
}
|
|
1791
1852
|
} catch {}
|
|
1792
1853
|
});
|
|
1793
1854
|
|
|
1794
1855
|
ws.on('close', () => {
|
|
1856
|
+
for (const t of _redrawRetryTimers) clearTimeout(t);
|
|
1795
1857
|
bpGate.dispose();
|
|
1796
1858
|
floodGate.dispose();
|
|
1797
1859
|
removeDataListener();
|