@co0ontty/wand 1.32.3 → 1.33.0
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/web-ui/content/scripts.js +200 -37
- package/package.json +1 -1
|
@@ -135,6 +135,7 @@
|
|
|
135
135
|
// askuserquestion 菜单多份叠加的最强候选根因。
|
|
136
136
|
syncOutputBuffer: null,
|
|
137
137
|
syncOutputDeadline: 0,
|
|
138
|
+
syncFramingResidue: false,
|
|
138
139
|
lastChunkAt: 0,
|
|
139
140
|
terminalHealthTimer: null,
|
|
140
141
|
lastTerminalResyncAt: 0,
|
|
@@ -147,12 +148,14 @@
|
|
|
147
148
|
// scroll 动画),多次调用用 Math.max 合并、不会被短窗口缩短。
|
|
148
149
|
terminalProgrammaticScrollUntil: 0,
|
|
149
150
|
terminalScrollIdleTimer: null,
|
|
150
|
-
terminalScrollIdleMs: 1800,
|
|
151
151
|
terminalScrollThreshold: 12,
|
|
152
152
|
showTerminalJumpToBottom: false,
|
|
153
153
|
terminalViewportEl: null,
|
|
154
154
|
terminalViewportScrollHandler: null,
|
|
155
155
|
terminalViewportTouchHandler: null,
|
|
156
|
+
terminalViewportTouchStartHandler: null,
|
|
157
|
+
terminalTouchStartY: 0,
|
|
158
|
+
terminalComposing: false,
|
|
156
159
|
resizeObserver: null,
|
|
157
160
|
resizeHandler: null,
|
|
158
161
|
resizeTimer: null,
|
|
@@ -6179,12 +6182,25 @@
|
|
|
6179
6182
|
inputBox.addEventListener("keydown", handleInputBoxKeydown);
|
|
6180
6183
|
inputBox.addEventListener("paste", handleInputPaste);
|
|
6181
6184
|
inputBox.addEventListener("input", function() {
|
|
6185
|
+
// INPUT-3: IME 组字期间不把半成品发给 PTY,等 compositionend 再统一发。
|
|
6186
|
+
if (state.terminalComposing) return;
|
|
6182
6187
|
if (handleInteractiveTextInput(inputBox)) {
|
|
6183
6188
|
return;
|
|
6184
6189
|
}
|
|
6185
6190
|
refreshInputBoxState(inputBox);
|
|
6186
6191
|
setDraftValue(inputBox.value, true);
|
|
6187
6192
|
});
|
|
6193
|
+
// INPUT-3: 交互模式 IME 组字承接。compositionstart 起置位标志让 input
|
|
6194
|
+
// handler 静默;compositionend 取最终组字结果发 PTY 并清空。非交互模式不
|
|
6195
|
+
// 介入,正常的中文聊天输入不受影响。
|
|
6196
|
+
inputBox.addEventListener("compositionstart", function() {
|
|
6197
|
+
if (state.terminalInteractive) state.terminalComposing = true;
|
|
6198
|
+
});
|
|
6199
|
+
inputBox.addEventListener("compositionend", function() {
|
|
6200
|
+
if (!state.terminalComposing) return;
|
|
6201
|
+
state.terminalComposing = false;
|
|
6202
|
+
if (state.terminalInteractive) handleInteractiveTextInput(inputBox);
|
|
6203
|
+
});
|
|
6188
6204
|
inputBox.addEventListener("focus", function() {
|
|
6189
6205
|
// Close drawer when user focuses input to avoid backdrop blocking clicks
|
|
6190
6206
|
closeSessionsDrawer();
|
|
@@ -7132,7 +7148,10 @@
|
|
|
7132
7148
|
var shouldShow = !!state.selectedId
|
|
7133
7149
|
&& state.currentView === "terminal"
|
|
7134
7150
|
&& !state.terminalAutoFollow
|
|
7135
|
-
|
|
7151
|
+
// SCROLL-2: 隐藏判据用严格 2px(isTerminalAtBottom) 而非 12px。否则距底
|
|
7152
|
+
// 3–12px 区间 autoFollow 恒 false(scroll handler 只在 ≤2px 才恢复)却又
|
|
7153
|
+
// 因 isTerminalNearBottom()=true 隐藏按钮 → 既不跟随又无回底入口的死区。
|
|
7154
|
+
&& !isTerminalAtBottom();
|
|
7136
7155
|
state.showTerminalJumpToBottom = shouldShow;
|
|
7137
7156
|
if (button) {
|
|
7138
7157
|
button.classList.toggle("visible", shouldShow);
|
|
@@ -7180,22 +7199,19 @@
|
|
|
7180
7199
|
}
|
|
7181
7200
|
}
|
|
7182
7201
|
|
|
7183
|
-
function scheduleTerminalResumeFollow() {
|
|
7184
|
-
clearTerminalScrollIdleTimer();
|
|
7185
|
-
updateTerminalJumpToBottomButton();
|
|
7186
|
-
state.terminalScrollIdleTimer = setTimeout(function() {
|
|
7187
|
-
state.terminalScrollIdleTimer = null;
|
|
7188
|
-
state.terminalAutoFollow = true;
|
|
7189
|
-
if (!isTerminalNearBottom()) {
|
|
7190
|
-
scrollTerminalToBottom(true);
|
|
7191
|
-
}
|
|
7192
|
-
updateTerminalJumpToBottomButton();
|
|
7193
|
-
}, state.terminalScrollIdleMs);
|
|
7194
|
-
}
|
|
7195
|
-
|
|
7196
7202
|
function setTerminalManualScrollActive() {
|
|
7197
7203
|
state.terminalAutoFollow = false;
|
|
7198
7204
|
clearTerminalScrollIdleTimer();
|
|
7205
|
+
// SCROLL-1: 用户一旦表达"看历史"意图,立刻作废任何在途的程序性拽底。
|
|
7206
|
+
// 否则一个已排队、尚未 fire 的 wterm rAF _doRender 仍读着旧的
|
|
7207
|
+
// _shouldScrollToBottom=true 把视口拽回底,而那次拽底触发的 scroll 事件正好
|
|
7208
|
+
// 落在 120ms 程序窗口内被 scroll handler early-return 吞掉——上滚意图被悄悄
|
|
7209
|
+
// 撤回。这里同步按掉 wterm 的跟随意图、并清零程序窗口,让紧随的真实 scroll
|
|
7210
|
+
// 事件能被 handler 正常复判。
|
|
7211
|
+
state.terminalProgrammaticScrollUntil = 0;
|
|
7212
|
+
if (state.terminal && "_shouldScrollToBottom" in state.terminal) {
|
|
7213
|
+
state.terminal._shouldScrollToBottom = false;
|
|
7214
|
+
}
|
|
7199
7215
|
updateTerminalJumpToBottomButton();
|
|
7200
7216
|
}
|
|
7201
7217
|
|
|
@@ -7446,9 +7462,17 @@
|
|
|
7446
7462
|
|
|
7447
7463
|
function createWideParserState() { return { mode: "normal" }; }
|
|
7448
7464
|
|
|
7465
|
+
// PERF-1: 整块纯 ASCII 且无 ESC ⇒ 无宽字符、无 ANSI 序列,可跳过逐字符扫描。
|
|
7466
|
+
function isAsciiNonEscape(s) {
|
|
7467
|
+
return !/[^\x00-\x7f]/.test(s) && s.indexOf("\x1b") === -1;
|
|
7468
|
+
}
|
|
7469
|
+
|
|
7449
7470
|
function widePadAnsi(data, st) {
|
|
7450
7471
|
if (!data) return "";
|
|
7451
7472
|
var s = String(data);
|
|
7473
|
+
// PERF-1: 不在 ANSI 解析中间态、且整块纯 ASCII 无转义时原样返回,省下逐字符
|
|
7474
|
+
// 拼接与 U+2060 注入。Claude 流式输出与全量重放大量命中此快路径。
|
|
7475
|
+
if (st.mode === "normal" && isAsciiNonEscape(s)) return s;
|
|
7452
7476
|
var out = "";
|
|
7453
7477
|
for (var i = 0; i < s.length; i++) {
|
|
7454
7478
|
var code = s.charCodeAt(i);
|
|
@@ -7535,6 +7559,9 @@
|
|
|
7535
7559
|
// 护栏:超长/超时强制 flush,避免永久卡死
|
|
7536
7560
|
out += state.syncOutputBuffer;
|
|
7537
7561
|
state.syncOutputBuffer = null;
|
|
7562
|
+
// FLICKER: 这是 NEW-A 唯一"失手"路径——半个 ?2026 帧被透传给 wterm,
|
|
7563
|
+
// 可能渲染错位。打标记让 R6 chunk 兜底 resync 一次(且仅此时触发)。
|
|
7564
|
+
state.syncFramingResidue = true;
|
|
7538
7565
|
}
|
|
7539
7566
|
return out;
|
|
7540
7567
|
}
|
|
@@ -7634,10 +7661,17 @@
|
|
|
7634
7661
|
var node = anchor && anchor.nodeType === 3 ? anchor.parentNode : anchor;
|
|
7635
7662
|
var output = document.getElementById("output");
|
|
7636
7663
|
if (!output || !node || !output.contains(node)) return;
|
|
7664
|
+
// COPY-1: 终端每行被 wterm 补齐到整列宽(空 cell 输出真实空格 + white-space:pre),
|
|
7665
|
+
// 选中复制会带一长串行尾空格;同时宽字符后插了零宽填充符 U+2060。两者一起清:
|
|
7666
|
+
// 逐行剥 filler + trimEnd。只要选区落在 #output 内就处理(不再要求"含 filler
|
|
7667
|
+
// 才改写",否则纯 ASCII 行的尾随空格漏网)。
|
|
7637
7668
|
var text = sel.toString();
|
|
7638
|
-
|
|
7669
|
+
var cleaned = text.split("\n").map(function(line) {
|
|
7670
|
+
return line.split(WAND_WIDE_FILLER).join("").replace(/[ \t]+$/, "");
|
|
7671
|
+
}).join("\n");
|
|
7672
|
+
if (cleaned === text) return; // 无可清理内容,交回浏览器默认复制
|
|
7639
7673
|
if (e.clipboardData) {
|
|
7640
|
-
e.clipboardData.setData("text/plain",
|
|
7674
|
+
e.clipboardData.setData("text/plain", cleaned);
|
|
7641
7675
|
e.preventDefault();
|
|
7642
7676
|
}
|
|
7643
7677
|
});
|
|
@@ -7760,6 +7794,11 @@
|
|
|
7760
7794
|
var RESYNC_BUDGET_WINDOW_MS = 5000;
|
|
7761
7795
|
var RESYNC_BUDGET_MAX = 12;
|
|
7762
7796
|
var RESYNC_WARN_COOLDOWN_MS = 30000;
|
|
7797
|
+
// RENDER-1: softResync 自身的重放(wandTerminalWrite 末尾会调 maybeScheduleResyncForChunk)
|
|
7798
|
+
// 不应再触发新一轮 resync,否则从 health-check/onResize/刷新/重连等路径进来时,
|
|
7799
|
+
// 整段含 CSI 的 replaySource 会让单次 resync 被放大成 2~3 次全量重放。重放期间置位
|
|
7800
|
+
// 此标志,maybeScheduleResyncForChunk 开头据此短路。
|
|
7801
|
+
var _resyncInProgress = false;
|
|
7763
7802
|
function softResyncTerminal(options) {
|
|
7764
7803
|
if (!state.terminal || !state.terminalOutput) return false;
|
|
7765
7804
|
var opts = options || {};
|
|
@@ -7772,8 +7811,13 @@
|
|
|
7772
7811
|
var replaySource = marker > 0 ? state.terminalOutput.slice(marker) : state.terminalOutput;
|
|
7773
7812
|
var bufLen = replaySource.length;
|
|
7774
7813
|
var startedAt = (typeof performance !== "undefined" && performance.now) ? performance.now() : Date.now();
|
|
7775
|
-
|
|
7776
|
-
|
|
7814
|
+
_resyncInProgress = true;
|
|
7815
|
+
try {
|
|
7816
|
+
resetTerminal();
|
|
7817
|
+
wandTerminalWrite(state.terminal, replaySource);
|
|
7818
|
+
} finally {
|
|
7819
|
+
_resyncInProgress = false;
|
|
7820
|
+
}
|
|
7777
7821
|
state.lastTerminalResyncAt = Date.now();
|
|
7778
7822
|
maybeScrollTerminalToBottom("output");
|
|
7779
7823
|
if (!opts.skipFit) ensureTerminalFit("refresh");
|
|
@@ -7824,6 +7868,14 @@
|
|
|
7824
7868
|
var _resyncChunkLastAt = 0;
|
|
7825
7869
|
var _resyncChunkTailTimer = null;
|
|
7826
7870
|
function maybeScheduleResyncForChunk(chunk) {
|
|
7871
|
+
if (_resyncInProgress) return; // RENDER-1: 屏蔽 softResync 自身重放触发的递归
|
|
7872
|
+
// FLICKER: R6 chunk 热路径 resync 仅在 NEW-A 失手时兜底——即 ?2026 残帧被
|
|
7873
|
+
// processSyncOutputFraming 超时/超长强制 flush 透传给 wterm 之后。正常完整包帧
|
|
7874
|
+
// 的重绘(菜单/todo/thinking spinner)已被 NEW-A 原子化、wterm 渲染正确,无需
|
|
7875
|
+
// resync。旧实现对每个含 CSI 的 chunk 都触发,使 thinking 期间每 ~1.5s 一次
|
|
7876
|
+
// resetTerminal→空白帧→重画,终端区持续闪烁。残帧标记在此消费一次(仅此触发)。
|
|
7877
|
+
if (!state.syncFramingResidue) return;
|
|
7878
|
+
state.syncFramingResidue = false;
|
|
7827
7879
|
if (!chunk || typeof chunk !== "string") return;
|
|
7828
7880
|
if (chunk.indexOf("\x1b[") === -1) return;
|
|
7829
7881
|
if (!IN_PLACE_REDRAW_RE.test(chunk)) return;
|
|
@@ -8053,15 +8105,32 @@
|
|
|
8053
8105
|
}
|
|
8054
8106
|
setTerminalManualScrollActive();
|
|
8055
8107
|
};
|
|
8056
|
-
|
|
8057
|
-
|
|
8108
|
+
// SCROLL-3: 触摸只在"看历史"方向(手指下拉、clientY 增大)才下台跟随;
|
|
8109
|
+
// 手指上滑(朝新内容/底部)不关跟随,交给 scroll handler 在到底时恢复。
|
|
8110
|
+
// 终端非 column-reverse:手指下拉=内容下移=看上方历史=上滚意图,与 wheel
|
|
8111
|
+
// 的 deltaY<0 对称。原实现任何 touchmove 都关跟随,移动端在底部轻微回弹
|
|
8112
|
+
// 就丢跟随。
|
|
8113
|
+
state.terminalViewportTouchStartHandler = function(e) {
|
|
8114
|
+
if (e.touches && e.touches.length === 1) {
|
|
8115
|
+
state.terminalTouchStartY = e.touches[0].clientY;
|
|
8116
|
+
}
|
|
8117
|
+
};
|
|
8118
|
+
state.terminalViewportTouchHandler = function(e) {
|
|
8119
|
+
if (!e.touches || e.touches.length !== 1) return;
|
|
8120
|
+
if (typeof state.terminalTouchStartY !== "number") return;
|
|
8121
|
+
if (e.touches[0].clientY - state.terminalTouchStartY > 4) {
|
|
8122
|
+
setTerminalManualScrollActive();
|
|
8123
|
+
}
|
|
8058
8124
|
};
|
|
8059
8125
|
viewport.addEventListener("scroll", state.terminalViewportScrollHandler, { passive: true });
|
|
8126
|
+
viewport.addEventListener("touchstart", state.terminalViewportTouchStartHandler, { passive: true });
|
|
8060
8127
|
viewport.addEventListener("touchmove", state.terminalViewportTouchHandler, { passive: true });
|
|
8061
8128
|
}
|
|
8062
8129
|
|
|
8130
|
+
// SCROLL-5: 只在上滚(朝历史,deltaY<0)时下台跟随;向下滚(想回去)交给
|
|
8131
|
+
// scroll handler 在真正到底时恢复,避免"远离底部时向下滚也被一直按住不跟随"。
|
|
8063
8132
|
state.terminalWheelHandler = function(e) {
|
|
8064
|
-
if (
|
|
8133
|
+
if (e.deltaY < 0) {
|
|
8065
8134
|
setTerminalManualScrollActive();
|
|
8066
8135
|
}
|
|
8067
8136
|
e.stopPropagation();
|
|
@@ -8079,7 +8148,13 @@
|
|
|
8079
8148
|
wandTerminalWrite(term, "点击上方「新对话」开始你的第一次对话。\r\n");
|
|
8080
8149
|
}
|
|
8081
8150
|
|
|
8082
|
-
|
|
8151
|
+
// COPY-4: 有终端选区时不抢焦点到输入框,否则打断双击选词/三击选行后的复制
|
|
8152
|
+
// (焦点与后续 Ctrl+C 目标被夺走)。wterm 自带 _onClickFocus 有同款护栏。
|
|
8153
|
+
// 透传 event 给 focusInputBox(skipMobile),保留"移动端点终端不自动弹键盘"。
|
|
8154
|
+
state.terminalClickHandler = function(e) {
|
|
8155
|
+
if (hasActiveTerminalSelection()) return;
|
|
8156
|
+
focusInputBox(e);
|
|
8157
|
+
};
|
|
8083
8158
|
container.addEventListener("click", state.terminalClickHandler);
|
|
8084
8159
|
updateTerminalJumpToBottomButton();
|
|
8085
8160
|
initTerminalResizeHandle();
|
|
@@ -11431,29 +11506,57 @@
|
|
|
11431
11506
|
|
|
11432
11507
|
if (event.key === "Escape") {
|
|
11433
11508
|
event.preventDefault();
|
|
11434
|
-
|
|
11509
|
+
var escSess = getSelectedSession();
|
|
11510
|
+
if (isStructuredSession(escSess)) {
|
|
11511
|
+
// INPUT-2: 结构化会话没有 PTY,Esc 不能把 \x1b 当消息发给 Claude。
|
|
11512
|
+
// 用户语义 Esc=中断:正在生成时等同点"停止"按钮,否则无操作。
|
|
11513
|
+
if (escSess && escSess.structuredState && escSess.structuredState.inFlight) {
|
|
11514
|
+
stopSession();
|
|
11515
|
+
}
|
|
11516
|
+
} else {
|
|
11517
|
+
queueDirectInput(getControlInput("escape"), "escape");
|
|
11518
|
+
}
|
|
11435
11519
|
return;
|
|
11436
11520
|
}
|
|
11437
11521
|
|
|
11438
11522
|
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "c") {
|
|
11439
|
-
//
|
|
11440
|
-
|
|
11441
|
-
var
|
|
11442
|
-
|
|
11523
|
+
// COPY-2: 有选区(输入框内或终端输出区)时放行浏览器原生复制,而不是发
|
|
11524
|
+
// SIGINT。原实现只看输入框选区,漏了文档级终端选区。
|
|
11525
|
+
var inputBoxC = document.getElementById("input-box");
|
|
11526
|
+
var hasSelectionC = (inputBoxC && inputBoxC.selectionStart !== inputBoxC.selectionEnd)
|
|
11527
|
+
|| hasActiveTerminalSelection();
|
|
11528
|
+
if (hasSelectionC) {
|
|
11443
11529
|
return; // Let browser handle copy
|
|
11444
11530
|
}
|
|
11531
|
+
var ccSess = getSelectedSession();
|
|
11532
|
+
if (isStructuredSession(ccSess)) {
|
|
11533
|
+
// INPUT-2: 结构化会话不把 SIGINT(\x03) 当消息发给 Claude。Ctrl+C 视为
|
|
11534
|
+
// 中断当前生成(等同停止按钮),非生成态则无操作。
|
|
11535
|
+
event.preventDefault();
|
|
11536
|
+
if (ccSess && ccSess.structuredState && ccSess.structuredState.inFlight) {
|
|
11537
|
+
stopSession();
|
|
11538
|
+
}
|
|
11539
|
+
return;
|
|
11540
|
+
}
|
|
11445
11541
|
event.preventDefault();
|
|
11446
11542
|
queueDirectInput(getControlInput("ctrl_c"), "ctrl_c");
|
|
11447
11543
|
return;
|
|
11448
11544
|
}
|
|
11449
11545
|
|
|
11450
11546
|
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "d") {
|
|
11451
|
-
//
|
|
11547
|
+
// COPY-2: 有选区(输入框内或终端输出区)时放行浏览器复制。
|
|
11452
11548
|
var inputBox2 = document.getElementById("input-box");
|
|
11453
|
-
var hasSelection2 = inputBox2 && (inputBox2.selectionStart !== inputBox2.selectionEnd)
|
|
11549
|
+
var hasSelection2 = (inputBox2 && (inputBox2.selectionStart !== inputBox2.selectionEnd))
|
|
11550
|
+
|| hasActiveTerminalSelection();
|
|
11454
11551
|
if (hasSelection2) {
|
|
11455
11552
|
return; // Let browser handle copy
|
|
11456
11553
|
}
|
|
11554
|
+
var cdSess = getSelectedSession();
|
|
11555
|
+
if (isStructuredSession(cdSess)) {
|
|
11556
|
+
// INPUT-2: 结构化会话吞掉 Ctrl+D(EOF 对 Claude 对话无意义,别当消息发)
|
|
11557
|
+
event.preventDefault();
|
|
11558
|
+
return;
|
|
11559
|
+
}
|
|
11457
11560
|
event.preventDefault();
|
|
11458
11561
|
queueDirectInput(getControlInput("ctrl_d"), "ctrl_d");
|
|
11459
11562
|
return;
|
|
@@ -11461,6 +11564,11 @@
|
|
|
11461
11564
|
|
|
11462
11565
|
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "l") {
|
|
11463
11566
|
event.preventDefault();
|
|
11567
|
+
var clSess = getSelectedSession();
|
|
11568
|
+
if (isStructuredSession(clSess)) {
|
|
11569
|
+
// INPUT-2: 结构化会话吞掉 Ctrl+L(清屏对 Claude 对话无意义)
|
|
11570
|
+
return;
|
|
11571
|
+
}
|
|
11464
11572
|
queueDirectInput(getControlInput("ctrl_l"), "ctrl_l");
|
|
11465
11573
|
return;
|
|
11466
11574
|
}
|
|
@@ -13547,10 +13655,12 @@
|
|
|
13547
13655
|
composer.placeholder = getComposerPlaceholder(selectedSession, state.terminalInteractive);
|
|
13548
13656
|
composer.disabled = !structured && !!selectedSession && !isRunning && !canResumeOnSend;
|
|
13549
13657
|
composer.setAttribute("aria-disabled", composer.disabled ? "true" : "false");
|
|
13550
|
-
//
|
|
13551
|
-
//
|
|
13552
|
-
//
|
|
13553
|
-
|
|
13658
|
+
// INPUT-3: 交互模式不再设 readOnly。readOnly 的 textarea 上 IME 根本不会
|
|
13659
|
+
// 激活(compositionstart 不触发),导致中文/日文等组字输入彻底打不出。普通
|
|
13660
|
+
// 字符 keydown 已由 capture 阶段的 captureTerminalInput preventDefault 拦截、
|
|
13661
|
+
// 不会落进 textarea;唯独 IME 组字期间 capture 放行(isComposing),字符临时
|
|
13662
|
+
// 落入 textarea,由 compositionend 取最终文本发 PTY 后清空。
|
|
13663
|
+
composer.readOnly = false;
|
|
13554
13664
|
composer.classList.toggle("is-terminal-passthrough", !!state.terminalInteractive);
|
|
13555
13665
|
}
|
|
13556
13666
|
var sendBtn = document.getElementById("send-input-button");
|
|
@@ -13566,15 +13676,40 @@
|
|
|
13566
13676
|
if (container) container.classList.toggle("interactive", !structured && state.terminalInteractive);
|
|
13567
13677
|
}
|
|
13568
13678
|
|
|
13679
|
+
// COPY-2/COPY-4: 是否存在落在终端输出区(#output)内的活动文本选区。用于:
|
|
13680
|
+
// 有选区时 Ctrl+C 放行浏览器原生复制而非发 SIGINT;click 不抢焦点以免打断
|
|
13681
|
+
// 双击选词/三击选行后的复制。
|
|
13682
|
+
function hasActiveTerminalSelection() {
|
|
13683
|
+
var sel = window.getSelection && window.getSelection();
|
|
13684
|
+
if (!sel || sel.isCollapsed) return false;
|
|
13685
|
+
var output = document.getElementById("output");
|
|
13686
|
+
if (!output) return false;
|
|
13687
|
+
var node = sel.anchorNode;
|
|
13688
|
+
if (node && node.nodeType === 3) node = node.parentNode;
|
|
13689
|
+
return !!(node && output.contains(node));
|
|
13690
|
+
}
|
|
13691
|
+
|
|
13569
13692
|
function captureTerminalInput(event) {
|
|
13570
13693
|
if (!shouldCaptureTerminalEvent(event)) return;
|
|
13694
|
+
// INPUT-1: 放行 Cmd/Meta 组合键给浏览器(复制/粘贴/刷新/切标签)。PTY 用
|
|
13695
|
+
// Ctrl 不用 Cmd,拦下来既破坏 macOS 原生快捷键,又会把裸字母(Cmd+X→'x')
|
|
13696
|
+
// 误塞进 PTY。
|
|
13697
|
+
if (event.metaKey) return;
|
|
13571
13698
|
var key = keyFromKeyboardEvent(event);
|
|
13572
13699
|
if (!key) return;
|
|
13573
|
-
event.preventDefault();
|
|
13574
13700
|
var mods = getModifierStateFromEvent(event, key);
|
|
13575
13701
|
if (isModifierKey(key)) return;
|
|
13702
|
+
// COPY-2: 有选区时 Ctrl+C 放行浏览器原生复制,而不是发 SIGINT(0x03) 把进程
|
|
13703
|
+
// 杀了还复制不到。无选区的 Ctrl+C 仍透传给 PTY。
|
|
13704
|
+
if (mods.ctrl && key.length === 1 && key.toLowerCase() === "c" && hasActiveTerminalSelection()) {
|
|
13705
|
+
return;
|
|
13706
|
+
}
|
|
13576
13707
|
var sequence = buildPtySequence(key, mods);
|
|
13577
|
-
|
|
13708
|
+
// INPUT-4: 只有真正要发给 PTY 的键才 preventDefault;空序列(F5/F12/死键等)
|
|
13709
|
+
// 放行给浏览器,避免"既没发 PTY 又吞掉浏览器默认行为"。
|
|
13710
|
+
if (!sequence) return;
|
|
13711
|
+
event.preventDefault();
|
|
13712
|
+
sendTerminalSequence(sequence, key);
|
|
13578
13713
|
}
|
|
13579
13714
|
|
|
13580
13715
|
function handleMiniKeyboardClick(event) {
|
|
@@ -15255,7 +15390,11 @@
|
|
|
15255
15390
|
var now = Date.now();
|
|
15256
15391
|
var chunkPause = state.lastChunkAt > 0 && (now - state.lastChunkAt > 300);
|
|
15257
15392
|
var resyncDue = (now - state.lastTerminalResyncAt) > 30000;
|
|
15258
|
-
|
|
15393
|
+
// RENDER-2: 仅在"自上次 resync 以来确有新输出"时才重放。静止/已结束会话
|
|
15394
|
+
// buffer 不再变化,30s 周期 resync 是纯无用功,还会把 cursor-home 中间帧
|
|
15395
|
+
// 重堆进 scrollback、扰动滚动位置。lastChunkAt>lastTerminalResyncAt 即脏。
|
|
15396
|
+
var dirtySinceResync = state.lastChunkAt > state.lastTerminalResyncAt;
|
|
15397
|
+
if (resyncDue && dirtySinceResync && (chunkPause || selectedSession.status !== "running") && state.terminalOutput) {
|
|
15259
15398
|
softResyncTerminal();
|
|
15260
15399
|
}
|
|
15261
15400
|
}, 5000);
|
|
@@ -15311,6 +15450,9 @@
|
|
|
15311
15450
|
if (state.terminalViewportTouchHandler) {
|
|
15312
15451
|
state.terminalViewportEl.removeEventListener("touchmove", state.terminalViewportTouchHandler);
|
|
15313
15452
|
}
|
|
15453
|
+
if (state.terminalViewportTouchStartHandler) {
|
|
15454
|
+
state.terminalViewportEl.removeEventListener("touchstart", state.terminalViewportTouchStartHandler);
|
|
15455
|
+
}
|
|
15314
15456
|
}
|
|
15315
15457
|
if (output) {
|
|
15316
15458
|
if (state.terminalWheelHandler) {
|
|
@@ -15323,13 +15465,24 @@
|
|
|
15323
15465
|
state.terminalViewportEl = null;
|
|
15324
15466
|
state.terminalViewportScrollHandler = null;
|
|
15325
15467
|
state.terminalViewportTouchHandler = null;
|
|
15468
|
+
state.terminalViewportTouchStartHandler = null;
|
|
15326
15469
|
state.terminalWheelHandler = null;
|
|
15327
15470
|
state.terminalClickHandler = null;
|
|
15471
|
+
// LIFE-1: 清理 initTerminalScrollbar 注册的 hide-timer 与拖拽状态。否则
|
|
15472
|
+
// hide-timer 闭包引用游离节点(置 El=null 拦不住它),且若拖拽中途 teardown,
|
|
15473
|
+
// 残留 dragging=true 会让新会话 scrollbar 的 scheduleHideScrollbar 被永久抑制
|
|
15474
|
+
// (开头 if(dragging)return)→ 滚动条出现后再也不自动消失。
|
|
15475
|
+
if (state.terminalScrollbarHideTimer) {
|
|
15476
|
+
clearTimeout(state.terminalScrollbarHideTimer);
|
|
15477
|
+
state.terminalScrollbarHideTimer = null;
|
|
15478
|
+
}
|
|
15328
15479
|
if (state.terminalScrollbarEl && state.terminalScrollbarEl.parentNode) {
|
|
15329
15480
|
state.terminalScrollbarEl.parentNode.removeChild(state.terminalScrollbarEl);
|
|
15330
15481
|
}
|
|
15331
15482
|
state.terminalScrollbarEl = null;
|
|
15332
15483
|
state.terminalScrollbarThumbEl = null;
|
|
15484
|
+
state.terminalScrollbarDragging = false;
|
|
15485
|
+
state.terminalScrollbarRafPending = false;
|
|
15333
15486
|
if (state.terminal) {
|
|
15334
15487
|
state.terminal.destroy();
|
|
15335
15488
|
state.terminal = null;
|
|
@@ -15354,6 +15507,7 @@
|
|
|
15354
15507
|
// 会让新会话的首批 PTY 字节全部被吞进 buffer 等永远不会来的 end。
|
|
15355
15508
|
state.syncOutputBuffer = null;
|
|
15356
15509
|
state.syncOutputDeadline = 0;
|
|
15510
|
+
state.syncFramingResidue = false;
|
|
15357
15511
|
state.terminalSessionId = null;
|
|
15358
15512
|
state.terminalOutput = "";
|
|
15359
15513
|
state.terminalOutputMarker = 0; // R8: teardown 时重置 /clear marker
|
|
@@ -15376,6 +15530,11 @@
|
|
|
15376
15530
|
_resyncStatsWindowStart = 0;
|
|
15377
15531
|
_resyncStatsCount = 0;
|
|
15378
15532
|
_resyncLastWarnAt = 0;
|
|
15533
|
+
_resyncInProgress = false;
|
|
15534
|
+
// SIZE-2: lastResize 是全局去重缓存,记"上次 POST 的尺寸"。切到另一个在不同
|
|
15535
|
+
// 尺寸下创建的会话时,若新算出的 cols/rows 恰好等于上次值会被去重跳过,导致
|
|
15536
|
+
// 后端该会话列宽停在旧值、整段折行。teardown 重置后新会话首次 resize 必发出。
|
|
15537
|
+
state.lastResize = { cols: 0, rows: 0 };
|
|
15379
15538
|
}
|
|
15380
15539
|
|
|
15381
15540
|
function sendTerminalResize(cols, rows) {
|
|
@@ -15388,6 +15547,10 @@
|
|
|
15388
15547
|
// wterm WASM grid 的 maxCols 硬编码 256。POST 给服务端的 cols 也同步
|
|
15389
15548
|
// clamp,避免服务端 pty.resize 给 Claude 一个 wterm 实际渲不下的列宽。
|
|
15390
15549
|
if (cols > 256) cols = 256;
|
|
15550
|
+
// SIZE-1: rows 也 clamp 到后端上限 160(process-manager clampDimension 10..160)。
|
|
15551
|
+
// 否则高分屏+小字号客户端算出 rows>160 时,后端压到 160,客户端网格底部多出的
|
|
15552
|
+
// 行对应的 PTY 内容永不写入 → 底部大片空白 / 光标错位 / 全屏菜单画到不存在的行。
|
|
15553
|
+
if (rows > 160) rows = 160;
|
|
15391
15554
|
var nextSize = { cols: cols, rows: rows };
|
|
15392
15555
|
if (state.lastResize.cols !== nextSize.cols || state.lastResize.rows !== nextSize.rows) {
|
|
15393
15556
|
state.lastResize = nextSize;
|