cc-viewer 1.6.310 → 1.6.312
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-DXhyaS3P.css +1 -0
- package/dist/assets/App-DtRb6L04.js +2 -0
- package/dist/assets/{MdxEditorPanel-Dfn62V7N.js → MdxEditorPanel-CpcelHyU.js} +1 -1
- package/dist/assets/Mobile-DwiM0Dm6.js +1 -0
- package/dist/assets/{index-DgGJTx-b.js → index-4qAcuvOR.js} +2 -2
- package/dist/assets/seqResourceLoaders-BVRDP3Yd.js +2 -0
- package/dist/assets/seqResourceLoaders-CX6xejM7.css +41 -0
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/server/lib/ansi-safe-slice.js +76 -0
- package/server/lib/pty-flood-coalescer.js +21 -1
- package/server/pty-manager.js +9 -43
- package/server/scratch-pty-manager.js +4 -21
- package/server/server.js +65 -24
- package/dist/assets/App-JIy7ipem.js +0 -2
- package/dist/assets/App-LcZ7ezR-.css +0 -1
- package/dist/assets/Mobile-Dbhv9o_l.js +0 -1
- package/dist/assets/seqResourceLoaders-CC0PRWqk.js +0 -2
- package/dist/assets/seqResourceLoaders-H8fAJXE5.css +0 -41
|
@@ -81,6 +81,12 @@ const DEFAULT_PT_COALESCE_MS = envIntAllowZero('CCV_FLOOD_PT_COALESCE_MS', 16);
|
|
|
81
81
|
* @param {(buf: string, rawStart: number) => number} opts.findSafeSliceStart - ANSI 安全截断(pty-manager 导出)
|
|
82
82
|
* @param {(buffered: number) => void} [opts.onFloodStart] - 进入限流态(observability 埋点)
|
|
83
83
|
* @param {() => void} [opts.onFloodEnd] - 回落直通态
|
|
84
|
+
* @param {(droppedChars: number) => void} [opts.onTruncate] - 本轮洪泛**实际丢过字节**且已回落直通时触发
|
|
85
|
+
* (紧随 onFloodEnd 之后,每轮洪泛至多一次,携带累计丢弃量)。安全切片只保证残片不上屏,
|
|
86
|
+
* 被截掉的中段对增量 TUI 流(macOS/Linux forkpty)不会自愈——调用方应在此用权威快照
|
|
87
|
+
* (getOutputBuffer → data-resync)对齐客户端画面。洪泛进行中不触发:中途 resync 快照
|
|
88
|
+
* 立刻过期且加重负载,回落时一次终态对齐即可。reset()/dispose() 清零累计(resync 路径
|
|
89
|
+
* 自身就是快照对齐,无需重复触发)。
|
|
84
90
|
* @param {number} [opts.flushMs]
|
|
85
91
|
* @param {number} [opts.floodThresholdBytesPerWin]
|
|
86
92
|
* @param {number} [opts.fallbackWins]
|
|
@@ -97,6 +103,7 @@ export function createFloodCoalescer({
|
|
|
97
103
|
findSafeSliceStart,
|
|
98
104
|
onFloodStart,
|
|
99
105
|
onFloodEnd,
|
|
106
|
+
onTruncate,
|
|
100
107
|
flushMs = DEFAULT_FLUSH_MS,
|
|
101
108
|
floodThresholdBytesPerWin = DEFAULT_FLOOD_THRESHOLD,
|
|
102
109
|
fallbackWins = DEFAULT_FALLBACK_WINS,
|
|
@@ -111,6 +118,7 @@ export function createFloodCoalescer({
|
|
|
111
118
|
let pending = '';
|
|
112
119
|
let winBytes = 0; // 当前桶累计字节(直通态由 offer 累计,限流态由 flush 结算)
|
|
113
120
|
let calmWins = 0; // 连续低于阈值的桶数
|
|
121
|
+
let droppedChars = 0; // 本轮洪泛累计实际丢弃的字符数(>0 时回落触发 onTruncate)
|
|
114
122
|
let flushTimer = null; // 限流态周期 flush;直通态下亦作为桶边界 timer(见 offer)
|
|
115
123
|
let ptBuffer = ''; // 直通态微合并缓冲(窗口开启期间到达的后续 chunk)
|
|
116
124
|
let ptTimer = null; // 直通态微合并窗口 timer(16ms),与 flushTimer(33ms 字节桶)并存、职责正交
|
|
@@ -157,7 +165,9 @@ export function createFloodCoalescer({
|
|
|
157
165
|
// 保证洪泛期送达客户端的字节率 ≤ flushBudgetBytes/flushMs,与前端消化速率同量级。
|
|
158
166
|
if (pending.length > flushBudgetBytes) {
|
|
159
167
|
const rawStart = pending.length - flushBudgetBytes;
|
|
160
|
-
|
|
168
|
+
const safeStart = findSafeSliceStart(pending, rawStart);
|
|
169
|
+
droppedChars += safeStart;
|
|
170
|
+
pending = pending.slice(safeStart);
|
|
161
171
|
}
|
|
162
172
|
const merged = SYNC_BEGIN + pending + SYNC_END;
|
|
163
173
|
pending = '';
|
|
@@ -179,6 +189,13 @@ export function createFloodCoalescer({
|
|
|
179
189
|
flooding = false;
|
|
180
190
|
calmWins = 0;
|
|
181
191
|
try { onFloodEnd?.(); } catch { }
|
|
192
|
+
// 本轮洪泛丢过字节 → 回落后通知一次终态对齐(见 opts.onTruncate doc)。
|
|
193
|
+
// 置零在调用**前**:回调内若同步 reset()/re-offer 不会重复触发。
|
|
194
|
+
if (droppedChars > 0) {
|
|
195
|
+
const dropped = droppedChars;
|
|
196
|
+
droppedChars = 0;
|
|
197
|
+
try { onTruncate?.(dropped); } catch { }
|
|
198
|
+
}
|
|
182
199
|
return; // 不再续约 timer,回直通
|
|
183
200
|
}
|
|
184
201
|
flushTimer = setTimer(onFloodTick, flushMs);
|
|
@@ -227,6 +244,7 @@ export function createFloodCoalescer({
|
|
|
227
244
|
if (pending.length > pendingCap) {
|
|
228
245
|
const rawStart = pending.length - trimTo;
|
|
229
246
|
const safeStart = findSafeSliceStart(pending, rawStart);
|
|
247
|
+
droppedChars += safeStart;
|
|
230
248
|
pending = pending.slice(safeStart);
|
|
231
249
|
}
|
|
232
250
|
},
|
|
@@ -239,6 +257,7 @@ export function createFloodCoalescer({
|
|
|
239
257
|
winBytes = 0;
|
|
240
258
|
calmWins = 0;
|
|
241
259
|
flooding = false;
|
|
260
|
+
droppedChars = 0; // resync 路径调用本方法,快照即对齐,不再补发 onTruncate
|
|
242
261
|
},
|
|
243
262
|
isFlooding() {
|
|
244
263
|
return flooding;
|
|
@@ -250,6 +269,7 @@ export function createFloodCoalescer({
|
|
|
250
269
|
stopPtTimer();
|
|
251
270
|
pending = '';
|
|
252
271
|
ptBuffer = '';
|
|
272
|
+
droppedChars = 0;
|
|
253
273
|
},
|
|
254
274
|
};
|
|
255
275
|
}
|
package/server/pty-manager.js
CHANGED
|
@@ -6,6 +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
10
|
|
|
10
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
12
|
const __dirname = dirname(__filename);
|
|
@@ -35,6 +36,9 @@ let _spawnInflight = null;
|
|
|
35
36
|
const PTY_COLS_MIN = 2, PTY_COLS_MAX = 1000;
|
|
36
37
|
const PTY_ROWS_MIN = 1, PTY_ROWS_MAX = 1000;
|
|
37
38
|
const MAX_BUFFER = 200000;
|
|
39
|
+
// 裁剪滞回:超 MAX_BUFFER 触发后一次裁到 TRIM_TO,而非每个 chunk 都裁到 MAX_BUFFER——
|
|
40
|
+
// 把 ~200KB slice 重分配的频率从每 chunk 一次降到每 ~20KB 新输出一次。
|
|
41
|
+
const BUFFER_TRIM_TO = 180000;
|
|
38
42
|
let batchBuffer = '';
|
|
39
43
|
let batchScheduled = false;
|
|
40
44
|
let _ptyImportForTests = null;
|
|
@@ -51,47 +55,9 @@ async function getPty() {
|
|
|
51
55
|
return ptyMod.default || ptyMod;
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
* 策略:从截断点向后扫描,跳过可能被截断的不完整转义序列。
|
|
58
|
-
* 注意:只保 ANSI 序列边界,不保 DEC 2026 同步标记的配对——
|
|
59
|
-
* 跨 2026 块截断的配平由调用方负责(见 lib/pty-flood-coalescer.js)。
|
|
60
|
-
* export 供洪泛限流器复用同一截断语义。
|
|
61
|
-
*/
|
|
62
|
-
export function findSafeSliceStart(buf, rawStart) {
|
|
63
|
-
// 从 rawStart 开始,向后最多扫描 64 字节寻找安全起点
|
|
64
|
-
const scanLimit = Math.min(rawStart + 64, buf.length);
|
|
65
|
-
let i = rawStart;
|
|
66
|
-
while (i < scanLimit) {
|
|
67
|
-
const ch = buf.charCodeAt(i);
|
|
68
|
-
// 如果当前字符是 ESC (0x1b),可能是新转义序列的开头,
|
|
69
|
-
// 但也可能是被截断的序列的中间部分,跳过整个序列
|
|
70
|
-
if (ch === 0x1b) {
|
|
71
|
-
// 找到 ESC,向后寻找序列结束符(字母字符)
|
|
72
|
-
let j = i + 1;
|
|
73
|
-
while (j < scanLimit && !((buf.charCodeAt(j) >= 0x40 && buf.charCodeAt(j) <= 0x7e) && j > i + 1)) {
|
|
74
|
-
j++;
|
|
75
|
-
}
|
|
76
|
-
if (j < scanLimit) {
|
|
77
|
-
// 找到完整序列末尾,从下一个字符开始是安全的
|
|
78
|
-
return j + 1;
|
|
79
|
-
}
|
|
80
|
-
// 序列不完整,继续扫描
|
|
81
|
-
i = j;
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
// 如果字符是 CSI 参数字符 (0x30-0x3f) 或中间字符 (0x20-0x2f),
|
|
85
|
-
// 说明我们在转义序列中间,继续向后
|
|
86
|
-
if ((ch >= 0x20 && ch <= 0x3f)) {
|
|
87
|
-
i++;
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
// 普通可见字符或控制字符(非转义相关),这是安全位置
|
|
91
|
-
break;
|
|
92
|
-
}
|
|
93
|
-
return i < buf.length ? i : rawStart;
|
|
94
|
-
}
|
|
58
|
+
// ANSI 安全截断起点:实现迁至 lib/ansi-safe-slice.js(锚点扫描算法,见该文件 doc)。
|
|
59
|
+
// 保持从本模块 export——server.js 解构注入洪泛限流器、单测均从这里导入。
|
|
60
|
+
export { findSafeSliceStart };
|
|
95
61
|
|
|
96
62
|
// DEC Private Mode 2026 (Synchronized Output) markers.
|
|
97
63
|
// xterm.js 6.0+ 原生支持:收到 BEGIN 后缓存所有写入,收到 END 后一次性渲染,
|
|
@@ -297,7 +263,7 @@ async function _spawnClaudeImpl(proxyPort, cwd, extraArgs = [], claudePath = nul
|
|
|
297
263
|
ptyProcess.onData((data) => {
|
|
298
264
|
outputBuffer += data;
|
|
299
265
|
if (outputBuffer.length > MAX_BUFFER) {
|
|
300
|
-
const rawStart = outputBuffer.length -
|
|
266
|
+
const rawStart = outputBuffer.length - BUFFER_TRIM_TO;
|
|
301
267
|
const safeStart = findSafeSliceStart(outputBuffer, rawStart);
|
|
302
268
|
outputBuffer = outputBuffer.slice(safeStart);
|
|
303
269
|
}
|
|
@@ -477,7 +443,7 @@ async function _spawnShellImpl() {
|
|
|
477
443
|
ptyProcess.onData((data) => {
|
|
478
444
|
outputBuffer += data;
|
|
479
445
|
if (outputBuffer.length > MAX_BUFFER) {
|
|
480
|
-
const rawStart = outputBuffer.length -
|
|
446
|
+
const rawStart = outputBuffer.length - BUFFER_TRIM_TO;
|
|
481
447
|
const safeStart = findSafeSliceStart(outputBuffer, rawStart);
|
|
482
448
|
outputBuffer = outputBuffer.slice(safeStart);
|
|
483
449
|
}
|
|
@@ -5,6 +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
9
|
|
|
9
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
11
|
const __dirname = dirname(__filename);
|
|
@@ -21,6 +22,8 @@ const STARTUP_CWD = (() => {
|
|
|
21
22
|
})();
|
|
22
23
|
|
|
23
24
|
const MAX_BUFFER = 50000;
|
|
25
|
+
// 裁剪滞回:超 MAX_BUFFER 后一次裁到 TRIM_TO,降低每 chunk slice 重分配频率(同 pty-manager.js)
|
|
26
|
+
const BUFFER_TRIM_TO = 45000;
|
|
24
27
|
|
|
25
28
|
// id -> { ptyProcess, dataListeners, exitListeners, lastExitCode, outputBuffer, lastCols, lastRows, batchBuffer, batchScheduled }
|
|
26
29
|
const ptys = new Map();
|
|
@@ -71,26 +74,6 @@ function maybeReap(id, s) {
|
|
|
71
74
|
}
|
|
72
75
|
}
|
|
73
76
|
|
|
74
|
-
function findSafeSliceStart(buf, rawStart) {
|
|
75
|
-
const scanLimit = Math.min(rawStart + 64, buf.length);
|
|
76
|
-
let i = rawStart;
|
|
77
|
-
while (i < scanLimit) {
|
|
78
|
-
const ch = buf.charCodeAt(i);
|
|
79
|
-
if (ch === 0x1b) {
|
|
80
|
-
let j = i + 1;
|
|
81
|
-
while (j < scanLimit && !((buf.charCodeAt(j) >= 0x40 && buf.charCodeAt(j) <= 0x7e) && j > i + 1)) {
|
|
82
|
-
j++;
|
|
83
|
-
}
|
|
84
|
-
if (j < scanLimit) return j + 1;
|
|
85
|
-
i = j;
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
if (ch >= 0x20 && ch <= 0x3f) { i++; continue; }
|
|
89
|
-
break;
|
|
90
|
-
}
|
|
91
|
-
return i < buf.length ? i : rawStart;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
77
|
function flushBatch(s) {
|
|
95
78
|
s.batchScheduled = false;
|
|
96
79
|
if (!s.batchBuffer) return;
|
|
@@ -164,7 +147,7 @@ export async function spawnScratch(id) {
|
|
|
164
147
|
s.ptyProcess.onData((data) => {
|
|
165
148
|
s.outputBuffer += data;
|
|
166
149
|
if (s.outputBuffer.length > MAX_BUFFER) {
|
|
167
|
-
const rawStart = s.outputBuffer.length -
|
|
150
|
+
const rawStart = s.outputBuffer.length - BUFFER_TRIM_TO;
|
|
168
151
|
const safeStart = findSafeSliceStart(s.outputBuffer, rawStart);
|
|
169
152
|
s.outputBuffer = s.outputBuffer.slice(safeStart);
|
|
170
153
|
}
|
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);
|
|
@@ -1260,6 +1269,12 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1260
1269
|
findSafeSliceStart,
|
|
1261
1270
|
onFloodStart: (bytes) => _floodLog('start', bytes),
|
|
1262
1271
|
onFloodEnd: () => _floodLog('end', 0),
|
|
1272
|
+
// 洪泛期丢过中段:增量输出流不会自愈,回落直通后用权威快照对齐一次
|
|
1273
|
+
onTruncate: (dropped) => {
|
|
1274
|
+
_floodLog('truncate', dropped);
|
|
1275
|
+
floodGate.reset();
|
|
1276
|
+
sendScratchResync();
|
|
1277
|
+
},
|
|
1263
1278
|
});
|
|
1264
1279
|
|
|
1265
1280
|
const removeDataListener = onScratchData(id, (data) => {
|
|
@@ -1283,6 +1298,12 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1283
1298
|
writeScratch(id, msg.data);
|
|
1284
1299
|
} else if (msg.type === 'resize') {
|
|
1285
1300
|
resizeScratch(id, msg.cols, msg.rows);
|
|
1301
|
+
} else if (msg.type === 'resync-request') {
|
|
1302
|
+
// 客户端 write-queue 积压丢弃后请求快照对齐(utils/terminalWriteQueue.js onTrim)
|
|
1303
|
+
if (resyncReqGate.shouldNudge()) {
|
|
1304
|
+
floodGate.reset();
|
|
1305
|
+
sendScratchResync();
|
|
1306
|
+
}
|
|
1286
1307
|
} else if (msg.type === 'kill') {
|
|
1287
1308
|
// 用户主动关闭 tab:杀 pty(killScratch 内部 ptys.delete 后配额自动释放);前端会随后 close ws
|
|
1288
1309
|
killScratch(id);
|
|
@@ -1352,6 +1373,34 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1352
1373
|
// 走冷却——nudge 让 ConPTY 再吐全屏重绘 = 新洪泛燃料,紧 behind→resume 循环里反复
|
|
1353
1374
|
// nudge 会自我维持(客户端每轮 reset+重放快照 = 永久冻结表象)。详见 lib/resync-nudge-gate.js。
|
|
1354
1375
|
const nudgeGate = createResyncNudgeGate({ cooldownMs: RESYNC_NUDGE_COOLDOWN_MS });
|
|
1376
|
+
const resyncReqGate = createResyncNudgeGate({ cooldownMs: RESYNC_REQ_COOLDOWN_MS });
|
|
1377
|
+
// 权威快照对齐:data-resync 快照无条件发,全屏重绘 nudge 走 nudgeGate 冷却。
|
|
1378
|
+
// bpGate.onResume / floodGate.onTruncate / 客户端 resync-request 三路共用;
|
|
1379
|
+
// 调用前须 floodGate.reset()(快照已含 pending/微合并缓冲字节,残留会在快照后回灌重复)。
|
|
1380
|
+
const sendResync = (buffered = 0) => {
|
|
1381
|
+
if (ws.readyState !== 1) return;
|
|
1382
|
+
try { ws.send(JSON.stringify({ type: 'data-resync', data: getOutputBuffer() })); } catch {}
|
|
1383
|
+
if (!nudgeGate.shouldNudge()) { _bpLog('nudge-skip', buffered); return; }
|
|
1384
|
+
try {
|
|
1385
|
+
if (process.platform !== 'win32') {
|
|
1386
|
+
// POSIX:与下方 _needRedrawBootstrap 同款 SIGWINCH 兜底
|
|
1387
|
+
const pid = getClaudePid();
|
|
1388
|
+
if (pid && pid !== process.pid) process.kill(pid, 'SIGWINCH');
|
|
1389
|
+
} else {
|
|
1390
|
+
// Windows 无 SIGWINCH:resize 抖动经 ConPTY 通知重绘(恢复路径偶发,闪烁可接受)。
|
|
1391
|
+
// 尺寸仲裁与 resize 消息处理一致:移动端优先,否则活跃客户端(activeWs 为 null 时
|
|
1392
|
+
// 本 ws 视为所有者)——恢复的 ws 可能是非权威的慢后台 tab,用它自己的尺寸抖动会把
|
|
1393
|
+
// 共享 PTY 永久改成它的尺寸、挤掉活跃/移动端画面;无权威尺寸则跳过抖动。
|
|
1394
|
+
const mSize = getMobileSize();
|
|
1395
|
+
const size = mSize
|
|
1396
|
+
|| ((activeWs === ws || activeWs === null) ? clientSizes.get(ws) : clientSizes.get(activeWs));
|
|
1397
|
+
if (size) {
|
|
1398
|
+
resizePty(size.cols, size.rows + 1);
|
|
1399
|
+
resizePty(size.cols, size.rows);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
} catch {}
|
|
1403
|
+
};
|
|
1355
1404
|
const bpGate = createBackpressureGate({
|
|
1356
1405
|
getBufferedAmount: () => ws.bufferedAmount,
|
|
1357
1406
|
onBehind: (buffered) => {
|
|
@@ -1361,28 +1410,7 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1361
1410
|
onResume: (buffered) => {
|
|
1362
1411
|
_bpLog('resume', buffered);
|
|
1363
1412
|
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 {}
|
|
1413
|
+
sendResync(buffered);
|
|
1386
1414
|
},
|
|
1387
1415
|
onTimeout: (buffered) => {
|
|
1388
1416
|
_bpLog('timeout', buffered);
|
|
@@ -1404,6 +1432,13 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1404
1432
|
findSafeSliceStart,
|
|
1405
1433
|
onFloodStart: (bytes) => _floodLog('start', bytes),
|
|
1406
1434
|
onFloodEnd: () => _floodLog('end', 0),
|
|
1435
|
+
// 洪泛期丢过中段:安全切片只保证残片不上屏,被截掉的内容对增量 TUI 流
|
|
1436
|
+
// (macOS/Linux forkpty)不会自愈——回落直通后用权威快照对齐一次
|
|
1437
|
+
onTruncate: (dropped) => {
|
|
1438
|
+
_floodLog('truncate', dropped);
|
|
1439
|
+
floodGate.reset();
|
|
1440
|
+
sendResync();
|
|
1441
|
+
},
|
|
1407
1442
|
});
|
|
1408
1443
|
|
|
1409
1444
|
// PTY 输出 → WebSocket(合并 ws 后客户端自行按 msg.type 分发,server 端不再 role 过滤)
|
|
@@ -1488,6 +1523,12 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1488
1523
|
} else {
|
|
1489
1524
|
replyDone(false);
|
|
1490
1525
|
}
|
|
1526
|
+
} else if (msg.type === 'resync-request') {
|
|
1527
|
+
// 客户端 write-queue 积压丢弃后请求快照对齐(utils/terminalWriteQueue.js onTrim)
|
|
1528
|
+
if (resyncReqGate.shouldNudge()) {
|
|
1529
|
+
floodGate.reset();
|
|
1530
|
+
sendResync();
|
|
1531
|
+
}
|
|
1491
1532
|
} else if (msg.type === 'ask-hook-answer') {
|
|
1492
1533
|
// Client answered AskUserQuestion via hook bridge.
|
|
1493
1534
|
// New protocol: msg.id required to address one of multiple pending asks.
|