@co0ontty/wand 1.32.3 → 1.34.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 +290 -43
- package/dist/web-ui/content/styles.css +36 -63
- 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,
|
|
@@ -1387,7 +1390,7 @@
|
|
|
1387
1390
|
checkDmgAutoUpdate();
|
|
1388
1391
|
}
|
|
1389
1392
|
if (state.claudeHistoryExpanded && !state.claudeHistoryLoaded) {
|
|
1390
|
-
|
|
1393
|
+
ensureClaudeHistoryLoaded();
|
|
1391
1394
|
}
|
|
1392
1395
|
});
|
|
1393
1396
|
})
|
|
@@ -1459,8 +1462,11 @@
|
|
|
1459
1462
|
// Suppress CSS transitions during initial DOM build
|
|
1460
1463
|
document.documentElement.classList.add("no-transition");
|
|
1461
1464
|
|
|
1462
|
-
// Apply persisted pin state before rendering
|
|
1463
|
-
|
|
1465
|
+
// Apply persisted pin state before rendering.
|
|
1466
|
+
// 窄条(collapsed)形态不靠 .open 显示,靠 .pinned.collapsed 的 width:56px
|
|
1467
|
+
// 常驻;此时强制 sessionsDrawerOpen=true 会与 toggleSidebarCollapsed 里设的
|
|
1468
|
+
// false 打架,并在手机端误触发背景遮罩。窄条态下不强制 open。
|
|
1469
|
+
if (state.sidebarPinned && !state.sidebarCollapsed && !isMobileLayout()) {
|
|
1464
1470
|
state.sessionsDrawerOpen = true;
|
|
1465
1471
|
}
|
|
1466
1472
|
app.innerHTML = isLoggedIn ? renderAppShell() : renderLogin();
|
|
@@ -3555,6 +3561,11 @@
|
|
|
3555
3561
|
|
|
3556
3562
|
function toggleManageMode(force) {
|
|
3557
3563
|
state.sessionsManageMode = typeof force === "boolean" ? force : !state.sessionsManageMode;
|
|
3564
|
+
if (state.sessionsManageMode && !state.claudeHistoryLoaded) {
|
|
3565
|
+
// 进入管理模式即后台补齐 Claude 历史,让「已选 N」「全选」计数从一开始
|
|
3566
|
+
// 就覆盖全部历史,而不是只统计已加载的那部分。
|
|
3567
|
+
ensureClaudeHistoryLoaded().then(updateSessionsList);
|
|
3568
|
+
}
|
|
3558
3569
|
if (!state.sessionsManageMode) {
|
|
3559
3570
|
clearManageSelections();
|
|
3560
3571
|
closeSwipedItem();
|
|
@@ -3571,6 +3582,16 @@
|
|
|
3571
3582
|
}
|
|
3572
3583
|
|
|
3573
3584
|
function selectAllVisibleItems() {
|
|
3585
|
+
// 全选语义 = 选中所有可管理项(会话 + 全部 Claude 历史)。历史在登录后
|
|
3586
|
+
// 异步扫描,若用户在扫描完成前点「全选」,state.claudeHistory 仍为空会漏选,
|
|
3587
|
+
// 删除时表现为"只删了已加载的,跨目录/未扫完的历史还在"。这里先确保历史
|
|
3588
|
+
// 加载完成再全选。
|
|
3589
|
+
if (!state.claudeHistoryLoaded) {
|
|
3590
|
+
ensureClaudeHistoryLoaded().then(selectAllVisibleItems);
|
|
3591
|
+
return;
|
|
3592
|
+
}
|
|
3593
|
+
// 展开 Claude 历史分组,让用户能直观看到这些历史项也被选中了。
|
|
3594
|
+
state.claudeHistoryExpanded = true;
|
|
3574
3595
|
var nextSessionIds = {};
|
|
3575
3596
|
getSelectableSessions().forEach(function(session) {
|
|
3576
3597
|
nextSessionIds[session.id] = true;
|
|
@@ -3779,6 +3800,21 @@
|
|
|
3779
3800
|
});
|
|
3780
3801
|
}
|
|
3781
3802
|
|
|
3803
|
+
// 去重包装:登录后历史会异步扫描,多个入口(管理模式、全选、展开分组)
|
|
3804
|
+
// 可能同时想确保历史就绪。共享同一个 in-flight Promise,避免重复 fetch,
|
|
3805
|
+
// 且在已加载时立即 resolve。
|
|
3806
|
+
var _claudeHistoryLoadingPromise = null;
|
|
3807
|
+
function ensureClaudeHistoryLoaded() {
|
|
3808
|
+
if (state.claudeHistoryLoaded) return Promise.resolve();
|
|
3809
|
+
if (_claudeHistoryLoadingPromise) return _claudeHistoryLoadingPromise;
|
|
3810
|
+
_claudeHistoryLoadingPromise = loadClaudeHistory().then(function() {
|
|
3811
|
+
_claudeHistoryLoadingPromise = null;
|
|
3812
|
+
}, function() {
|
|
3813
|
+
_claudeHistoryLoadingPromise = null;
|
|
3814
|
+
});
|
|
3815
|
+
return _claudeHistoryLoadingPromise;
|
|
3816
|
+
}
|
|
3817
|
+
|
|
3782
3818
|
function isMobileLayout() {
|
|
3783
3819
|
return window.innerWidth <= 768;
|
|
3784
3820
|
}
|
|
@@ -5889,11 +5925,17 @@
|
|
|
5889
5925
|
if (sidebarMoreBtn && sidebarOverflow) {
|
|
5890
5926
|
sidebarMoreBtn.addEventListener("click", function(e) {
|
|
5891
5927
|
e.stopPropagation();
|
|
5892
|
-
sidebarOverflow.classList.
|
|
5928
|
+
var willOpen = !sidebarOverflow.classList.contains("open");
|
|
5929
|
+
sidebarOverflow.classList.toggle("open", willOpen);
|
|
5930
|
+
if (willOpen) positionSidebarOverflowMenu(sidebarOverflow);
|
|
5893
5931
|
});
|
|
5894
5932
|
document.addEventListener("click", function() {
|
|
5895
5933
|
sidebarOverflow.classList.remove("open");
|
|
5896
5934
|
});
|
|
5935
|
+
// 视口尺寸变化时关闭,避免 clamp 后的定位与新尺寸不符。
|
|
5936
|
+
window.addEventListener("resize", function() {
|
|
5937
|
+
sidebarOverflow.classList.remove("open");
|
|
5938
|
+
});
|
|
5897
5939
|
}
|
|
5898
5940
|
var homeBtn = document.getElementById("sidebar-home-btn");
|
|
5899
5941
|
if (homeBtn) homeBtn.addEventListener("click", function() {
|
|
@@ -6179,12 +6221,25 @@
|
|
|
6179
6221
|
inputBox.addEventListener("keydown", handleInputBoxKeydown);
|
|
6180
6222
|
inputBox.addEventListener("paste", handleInputPaste);
|
|
6181
6223
|
inputBox.addEventListener("input", function() {
|
|
6224
|
+
// INPUT-3: IME 组字期间不把半成品发给 PTY,等 compositionend 再统一发。
|
|
6225
|
+
if (state.terminalComposing) return;
|
|
6182
6226
|
if (handleInteractiveTextInput(inputBox)) {
|
|
6183
6227
|
return;
|
|
6184
6228
|
}
|
|
6185
6229
|
refreshInputBoxState(inputBox);
|
|
6186
6230
|
setDraftValue(inputBox.value, true);
|
|
6187
6231
|
});
|
|
6232
|
+
// INPUT-3: 交互模式 IME 组字承接。compositionstart 起置位标志让 input
|
|
6233
|
+
// handler 静默;compositionend 取最终组字结果发 PTY 并清空。非交互模式不
|
|
6234
|
+
// 介入,正常的中文聊天输入不受影响。
|
|
6235
|
+
inputBox.addEventListener("compositionstart", function() {
|
|
6236
|
+
if (state.terminalInteractive) state.terminalComposing = true;
|
|
6237
|
+
});
|
|
6238
|
+
inputBox.addEventListener("compositionend", function() {
|
|
6239
|
+
if (!state.terminalComposing) return;
|
|
6240
|
+
state.terminalComposing = false;
|
|
6241
|
+
if (state.terminalInteractive) handleInteractiveTextInput(inputBox);
|
|
6242
|
+
});
|
|
6188
6243
|
inputBox.addEventListener("focus", function() {
|
|
6189
6244
|
// Close drawer when user focuses input to avoid backdrop blocking clicks
|
|
6190
6245
|
closeSessionsDrawer();
|
|
@@ -6948,7 +7003,7 @@
|
|
|
6948
7003
|
event.stopPropagation();
|
|
6949
7004
|
state.claudeHistoryExpanded = !state.claudeHistoryExpanded;
|
|
6950
7005
|
if (state.claudeHistoryExpanded && !state.claudeHistoryLoaded) {
|
|
6951
|
-
|
|
7006
|
+
ensureClaudeHistoryLoaded();
|
|
6952
7007
|
}
|
|
6953
7008
|
updateSessionsList();
|
|
6954
7009
|
return;
|
|
@@ -7132,7 +7187,10 @@
|
|
|
7132
7187
|
var shouldShow = !!state.selectedId
|
|
7133
7188
|
&& state.currentView === "terminal"
|
|
7134
7189
|
&& !state.terminalAutoFollow
|
|
7135
|
-
|
|
7190
|
+
// SCROLL-2: 隐藏判据用严格 2px(isTerminalAtBottom) 而非 12px。否则距底
|
|
7191
|
+
// 3–12px 区间 autoFollow 恒 false(scroll handler 只在 ≤2px 才恢复)却又
|
|
7192
|
+
// 因 isTerminalNearBottom()=true 隐藏按钮 → 既不跟随又无回底入口的死区。
|
|
7193
|
+
&& !isTerminalAtBottom();
|
|
7136
7194
|
state.showTerminalJumpToBottom = shouldShow;
|
|
7137
7195
|
if (button) {
|
|
7138
7196
|
button.classList.toggle("visible", shouldShow);
|
|
@@ -7180,22 +7238,19 @@
|
|
|
7180
7238
|
}
|
|
7181
7239
|
}
|
|
7182
7240
|
|
|
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
7241
|
function setTerminalManualScrollActive() {
|
|
7197
7242
|
state.terminalAutoFollow = false;
|
|
7198
7243
|
clearTerminalScrollIdleTimer();
|
|
7244
|
+
// SCROLL-1: 用户一旦表达"看历史"意图,立刻作废任何在途的程序性拽底。
|
|
7245
|
+
// 否则一个已排队、尚未 fire 的 wterm rAF _doRender 仍读着旧的
|
|
7246
|
+
// _shouldScrollToBottom=true 把视口拽回底,而那次拽底触发的 scroll 事件正好
|
|
7247
|
+
// 落在 120ms 程序窗口内被 scroll handler early-return 吞掉——上滚意图被悄悄
|
|
7248
|
+
// 撤回。这里同步按掉 wterm 的跟随意图、并清零程序窗口,让紧随的真实 scroll
|
|
7249
|
+
// 事件能被 handler 正常复判。
|
|
7250
|
+
state.terminalProgrammaticScrollUntil = 0;
|
|
7251
|
+
if (state.terminal && "_shouldScrollToBottom" in state.terminal) {
|
|
7252
|
+
state.terminal._shouldScrollToBottom = false;
|
|
7253
|
+
}
|
|
7199
7254
|
updateTerminalJumpToBottomButton();
|
|
7200
7255
|
}
|
|
7201
7256
|
|
|
@@ -7446,9 +7501,17 @@
|
|
|
7446
7501
|
|
|
7447
7502
|
function createWideParserState() { return { mode: "normal" }; }
|
|
7448
7503
|
|
|
7504
|
+
// PERF-1: 整块纯 ASCII 且无 ESC ⇒ 无宽字符、无 ANSI 序列,可跳过逐字符扫描。
|
|
7505
|
+
function isAsciiNonEscape(s) {
|
|
7506
|
+
return !/[^\x00-\x7f]/.test(s) && s.indexOf("\x1b") === -1;
|
|
7507
|
+
}
|
|
7508
|
+
|
|
7449
7509
|
function widePadAnsi(data, st) {
|
|
7450
7510
|
if (!data) return "";
|
|
7451
7511
|
var s = String(data);
|
|
7512
|
+
// PERF-1: 不在 ANSI 解析中间态、且整块纯 ASCII 无转义时原样返回,省下逐字符
|
|
7513
|
+
// 拼接与 U+2060 注入。Claude 流式输出与全量重放大量命中此快路径。
|
|
7514
|
+
if (st.mode === "normal" && isAsciiNonEscape(s)) return s;
|
|
7452
7515
|
var out = "";
|
|
7453
7516
|
for (var i = 0; i < s.length; i++) {
|
|
7454
7517
|
var code = s.charCodeAt(i);
|
|
@@ -7535,6 +7598,9 @@
|
|
|
7535
7598
|
// 护栏:超长/超时强制 flush,避免永久卡死
|
|
7536
7599
|
out += state.syncOutputBuffer;
|
|
7537
7600
|
state.syncOutputBuffer = null;
|
|
7601
|
+
// FLICKER: 这是 NEW-A 唯一"失手"路径——半个 ?2026 帧被透传给 wterm,
|
|
7602
|
+
// 可能渲染错位。打标记让 R6 chunk 兜底 resync 一次(且仅此时触发)。
|
|
7603
|
+
state.syncFramingResidue = true;
|
|
7538
7604
|
}
|
|
7539
7605
|
return out;
|
|
7540
7606
|
}
|
|
@@ -7634,10 +7700,17 @@
|
|
|
7634
7700
|
var node = anchor && anchor.nodeType === 3 ? anchor.parentNode : anchor;
|
|
7635
7701
|
var output = document.getElementById("output");
|
|
7636
7702
|
if (!output || !node || !output.contains(node)) return;
|
|
7703
|
+
// COPY-1: 终端每行被 wterm 补齐到整列宽(空 cell 输出真实空格 + white-space:pre),
|
|
7704
|
+
// 选中复制会带一长串行尾空格;同时宽字符后插了零宽填充符 U+2060。两者一起清:
|
|
7705
|
+
// 逐行剥 filler + trimEnd。只要选区落在 #output 内就处理(不再要求"含 filler
|
|
7706
|
+
// 才改写",否则纯 ASCII 行的尾随空格漏网)。
|
|
7637
7707
|
var text = sel.toString();
|
|
7638
|
-
|
|
7708
|
+
var cleaned = text.split("\n").map(function(line) {
|
|
7709
|
+
return line.split(WAND_WIDE_FILLER).join("").replace(/[ \t]+$/, "");
|
|
7710
|
+
}).join("\n");
|
|
7711
|
+
if (cleaned === text) return; // 无可清理内容,交回浏览器默认复制
|
|
7639
7712
|
if (e.clipboardData) {
|
|
7640
|
-
e.clipboardData.setData("text/plain",
|
|
7713
|
+
e.clipboardData.setData("text/plain", cleaned);
|
|
7641
7714
|
e.preventDefault();
|
|
7642
7715
|
}
|
|
7643
7716
|
});
|
|
@@ -7760,6 +7833,11 @@
|
|
|
7760
7833
|
var RESYNC_BUDGET_WINDOW_MS = 5000;
|
|
7761
7834
|
var RESYNC_BUDGET_MAX = 12;
|
|
7762
7835
|
var RESYNC_WARN_COOLDOWN_MS = 30000;
|
|
7836
|
+
// RENDER-1: softResync 自身的重放(wandTerminalWrite 末尾会调 maybeScheduleResyncForChunk)
|
|
7837
|
+
// 不应再触发新一轮 resync,否则从 health-check/onResize/刷新/重连等路径进来时,
|
|
7838
|
+
// 整段含 CSI 的 replaySource 会让单次 resync 被放大成 2~3 次全量重放。重放期间置位
|
|
7839
|
+
// 此标志,maybeScheduleResyncForChunk 开头据此短路。
|
|
7840
|
+
var _resyncInProgress = false;
|
|
7763
7841
|
function softResyncTerminal(options) {
|
|
7764
7842
|
if (!state.terminal || !state.terminalOutput) return false;
|
|
7765
7843
|
var opts = options || {};
|
|
@@ -7772,8 +7850,13 @@
|
|
|
7772
7850
|
var replaySource = marker > 0 ? state.terminalOutput.slice(marker) : state.terminalOutput;
|
|
7773
7851
|
var bufLen = replaySource.length;
|
|
7774
7852
|
var startedAt = (typeof performance !== "undefined" && performance.now) ? performance.now() : Date.now();
|
|
7775
|
-
|
|
7776
|
-
|
|
7853
|
+
_resyncInProgress = true;
|
|
7854
|
+
try {
|
|
7855
|
+
resetTerminal();
|
|
7856
|
+
wandTerminalWrite(state.terminal, replaySource);
|
|
7857
|
+
} finally {
|
|
7858
|
+
_resyncInProgress = false;
|
|
7859
|
+
}
|
|
7777
7860
|
state.lastTerminalResyncAt = Date.now();
|
|
7778
7861
|
maybeScrollTerminalToBottom("output");
|
|
7779
7862
|
if (!opts.skipFit) ensureTerminalFit("refresh");
|
|
@@ -7824,6 +7907,14 @@
|
|
|
7824
7907
|
var _resyncChunkLastAt = 0;
|
|
7825
7908
|
var _resyncChunkTailTimer = null;
|
|
7826
7909
|
function maybeScheduleResyncForChunk(chunk) {
|
|
7910
|
+
if (_resyncInProgress) return; // RENDER-1: 屏蔽 softResync 自身重放触发的递归
|
|
7911
|
+
// FLICKER: R6 chunk 热路径 resync 仅在 NEW-A 失手时兜底——即 ?2026 残帧被
|
|
7912
|
+
// processSyncOutputFraming 超时/超长强制 flush 透传给 wterm 之后。正常完整包帧
|
|
7913
|
+
// 的重绘(菜单/todo/thinking spinner)已被 NEW-A 原子化、wterm 渲染正确,无需
|
|
7914
|
+
// resync。旧实现对每个含 CSI 的 chunk 都触发,使 thinking 期间每 ~1.5s 一次
|
|
7915
|
+
// resetTerminal→空白帧→重画,终端区持续闪烁。残帧标记在此消费一次(仅此触发)。
|
|
7916
|
+
if (!state.syncFramingResidue) return;
|
|
7917
|
+
state.syncFramingResidue = false;
|
|
7827
7918
|
if (!chunk || typeof chunk !== "string") return;
|
|
7828
7919
|
if (chunk.indexOf("\x1b[") === -1) return;
|
|
7829
7920
|
if (!IN_PLACE_REDRAW_RE.test(chunk)) return;
|
|
@@ -8053,15 +8144,32 @@
|
|
|
8053
8144
|
}
|
|
8054
8145
|
setTerminalManualScrollActive();
|
|
8055
8146
|
};
|
|
8056
|
-
|
|
8057
|
-
|
|
8147
|
+
// SCROLL-3: 触摸只在"看历史"方向(手指下拉、clientY 增大)才下台跟随;
|
|
8148
|
+
// 手指上滑(朝新内容/底部)不关跟随,交给 scroll handler 在到底时恢复。
|
|
8149
|
+
// 终端非 column-reverse:手指下拉=内容下移=看上方历史=上滚意图,与 wheel
|
|
8150
|
+
// 的 deltaY<0 对称。原实现任何 touchmove 都关跟随,移动端在底部轻微回弹
|
|
8151
|
+
// 就丢跟随。
|
|
8152
|
+
state.terminalViewportTouchStartHandler = function(e) {
|
|
8153
|
+
if (e.touches && e.touches.length === 1) {
|
|
8154
|
+
state.terminalTouchStartY = e.touches[0].clientY;
|
|
8155
|
+
}
|
|
8156
|
+
};
|
|
8157
|
+
state.terminalViewportTouchHandler = function(e) {
|
|
8158
|
+
if (!e.touches || e.touches.length !== 1) return;
|
|
8159
|
+
if (typeof state.terminalTouchStartY !== "number") return;
|
|
8160
|
+
if (e.touches[0].clientY - state.terminalTouchStartY > 4) {
|
|
8161
|
+
setTerminalManualScrollActive();
|
|
8162
|
+
}
|
|
8058
8163
|
};
|
|
8059
8164
|
viewport.addEventListener("scroll", state.terminalViewportScrollHandler, { passive: true });
|
|
8165
|
+
viewport.addEventListener("touchstart", state.terminalViewportTouchStartHandler, { passive: true });
|
|
8060
8166
|
viewport.addEventListener("touchmove", state.terminalViewportTouchHandler, { passive: true });
|
|
8061
8167
|
}
|
|
8062
8168
|
|
|
8169
|
+
// SCROLL-5: 只在上滚(朝历史,deltaY<0)时下台跟随;向下滚(想回去)交给
|
|
8170
|
+
// scroll handler 在真正到底时恢复,避免"远离底部时向下滚也被一直按住不跟随"。
|
|
8063
8171
|
state.terminalWheelHandler = function(e) {
|
|
8064
|
-
if (
|
|
8172
|
+
if (e.deltaY < 0) {
|
|
8065
8173
|
setTerminalManualScrollActive();
|
|
8066
8174
|
}
|
|
8067
8175
|
e.stopPropagation();
|
|
@@ -8079,7 +8187,13 @@
|
|
|
8079
8187
|
wandTerminalWrite(term, "点击上方「新对话」开始你的第一次对话。\r\n");
|
|
8080
8188
|
}
|
|
8081
8189
|
|
|
8082
|
-
|
|
8190
|
+
// COPY-4: 有终端选区时不抢焦点到输入框,否则打断双击选词/三击选行后的复制
|
|
8191
|
+
// (焦点与后续 Ctrl+C 目标被夺走)。wterm 自带 _onClickFocus 有同款护栏。
|
|
8192
|
+
// 透传 event 给 focusInputBox(skipMobile),保留"移动端点终端不自动弹键盘"。
|
|
8193
|
+
state.terminalClickHandler = function(e) {
|
|
8194
|
+
if (hasActiveTerminalSelection()) return;
|
|
8195
|
+
focusInputBox(e);
|
|
8196
|
+
};
|
|
8083
8197
|
container.addEventListener("click", state.terminalClickHandler);
|
|
8084
8198
|
updateTerminalJumpToBottomButton();
|
|
8085
8199
|
initTerminalResizeHandle();
|
|
@@ -9513,10 +9627,55 @@
|
|
|
9513
9627
|
// 桌面端展开窄条 → 300px 全栏常驻。
|
|
9514
9628
|
state.sessionsDrawerOpen = true;
|
|
9515
9629
|
}
|
|
9516
|
-
render()
|
|
9630
|
+
// 轻量更新而非全量 render():render() 会 teardown 并重建整个终端 DOM,
|
|
9631
|
+
// 导致收窄/展开时终端闪烁、丢失滚动与渲染状态。这里只切布局 class
|
|
9632
|
+
// (宽度 56↔300 走 CSS width transition)、重渲侧栏列表内容、刷新
|
|
9633
|
+
// 收窄按钮自身的图标/文案,终端区保持不动。
|
|
9634
|
+
updateLayoutState();
|
|
9635
|
+
updateSessionsList();
|
|
9636
|
+
updateSidebarCollapseButton();
|
|
9637
|
+
hideCollapsedTileBubble();
|
|
9517
9638
|
scheduleTerminalRefitAfterPaddingTransition();
|
|
9518
9639
|
}
|
|
9519
9640
|
|
|
9641
|
+
// 收窄按钮的图标/title/状态随 collapsed 切换。抽出来给轻量更新路径用,
|
|
9642
|
+
// 避免为了换一个箭头方向就走全量 render()。
|
|
9643
|
+
function updateSidebarCollapseButton() {
|
|
9644
|
+
var btn = document.getElementById("sidebar-collapse-btn");
|
|
9645
|
+
if (!btn) return;
|
|
9646
|
+
var isCollapsed = !!state.sidebarPinned && !!state.sidebarCollapsed;
|
|
9647
|
+
btn.classList.toggle("collapsed", isCollapsed);
|
|
9648
|
+
btn.title = isCollapsed ? "展开侧栏" : "收起为窄条";
|
|
9649
|
+
btn.setAttribute("aria-label", isCollapsed ? "展开侧栏" : "收起为窄条");
|
|
9650
|
+
btn.innerHTML = isCollapsed
|
|
9651
|
+
? '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="10 6 16 12 10 18"/><line x1="20" y1="5" x2="20" y2="19"/></svg>'
|
|
9652
|
+
: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="14 6 8 12 14 18"/><line x1="4" y1="5" x2="4" y2="19"/></svg>';
|
|
9653
|
+
}
|
|
9654
|
+
|
|
9655
|
+
// 「更多操作」下拉默认 right:0 贴 more 按钮右沿向左展开。手机窄屏下这条会把
|
|
9656
|
+
// 菜单左缘顶出屏幕外。打开时按视口边界 clamp:先保持 CSS 默认右对齐,仅当
|
|
9657
|
+
// 真的越界才改写 left/right 把菜单拉回视口内(留 8px 边距)。
|
|
9658
|
+
function positionSidebarOverflowMenu(menu) {
|
|
9659
|
+
if (!menu) return;
|
|
9660
|
+
menu.style.left = "";
|
|
9661
|
+
menu.style.right = "";
|
|
9662
|
+
var parent = menu.offsetParent || menu.parentElement;
|
|
9663
|
+
if (!parent) return;
|
|
9664
|
+
var margin = 8;
|
|
9665
|
+
var parentRect = parent.getBoundingClientRect();
|
|
9666
|
+
var rect = menu.getBoundingClientRect();
|
|
9667
|
+
var vw = window.innerWidth;
|
|
9668
|
+
if (rect.left < margin) {
|
|
9669
|
+
// 左缘越界:改用 left 定位,把左缘顶到视口左 margin。
|
|
9670
|
+
menu.style.right = "auto";
|
|
9671
|
+
menu.style.left = (margin - parentRect.left) + "px";
|
|
9672
|
+
} else if (rect.right > vw - margin) {
|
|
9673
|
+
// 右缘越界:拉回视口右 margin(桌面右对齐时几乎不会触发)。
|
|
9674
|
+
menu.style.left = "auto";
|
|
9675
|
+
menu.style.right = (parentRect.right - (vw - margin)) + "px";
|
|
9676
|
+
}
|
|
9677
|
+
}
|
|
9678
|
+
|
|
9520
9679
|
|
|
9521
9680
|
// Store last focused element for focus trap
|
|
9522
9681
|
var lastFocusedElement = null;
|
|
@@ -11431,29 +11590,57 @@
|
|
|
11431
11590
|
|
|
11432
11591
|
if (event.key === "Escape") {
|
|
11433
11592
|
event.preventDefault();
|
|
11434
|
-
|
|
11593
|
+
var escSess = getSelectedSession();
|
|
11594
|
+
if (isStructuredSession(escSess)) {
|
|
11595
|
+
// INPUT-2: 结构化会话没有 PTY,Esc 不能把 \x1b 当消息发给 Claude。
|
|
11596
|
+
// 用户语义 Esc=中断:正在生成时等同点"停止"按钮,否则无操作。
|
|
11597
|
+
if (escSess && escSess.structuredState && escSess.structuredState.inFlight) {
|
|
11598
|
+
stopSession();
|
|
11599
|
+
}
|
|
11600
|
+
} else {
|
|
11601
|
+
queueDirectInput(getControlInput("escape"), "escape");
|
|
11602
|
+
}
|
|
11435
11603
|
return;
|
|
11436
11604
|
}
|
|
11437
11605
|
|
|
11438
11606
|
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "c") {
|
|
11439
|
-
//
|
|
11440
|
-
|
|
11441
|
-
var
|
|
11442
|
-
|
|
11607
|
+
// COPY-2: 有选区(输入框内或终端输出区)时放行浏览器原生复制,而不是发
|
|
11608
|
+
// SIGINT。原实现只看输入框选区,漏了文档级终端选区。
|
|
11609
|
+
var inputBoxC = document.getElementById("input-box");
|
|
11610
|
+
var hasSelectionC = (inputBoxC && inputBoxC.selectionStart !== inputBoxC.selectionEnd)
|
|
11611
|
+
|| hasActiveTerminalSelection();
|
|
11612
|
+
if (hasSelectionC) {
|
|
11443
11613
|
return; // Let browser handle copy
|
|
11444
11614
|
}
|
|
11615
|
+
var ccSess = getSelectedSession();
|
|
11616
|
+
if (isStructuredSession(ccSess)) {
|
|
11617
|
+
// INPUT-2: 结构化会话不把 SIGINT(\x03) 当消息发给 Claude。Ctrl+C 视为
|
|
11618
|
+
// 中断当前生成(等同停止按钮),非生成态则无操作。
|
|
11619
|
+
event.preventDefault();
|
|
11620
|
+
if (ccSess && ccSess.structuredState && ccSess.structuredState.inFlight) {
|
|
11621
|
+
stopSession();
|
|
11622
|
+
}
|
|
11623
|
+
return;
|
|
11624
|
+
}
|
|
11445
11625
|
event.preventDefault();
|
|
11446
11626
|
queueDirectInput(getControlInput("ctrl_c"), "ctrl_c");
|
|
11447
11627
|
return;
|
|
11448
11628
|
}
|
|
11449
11629
|
|
|
11450
11630
|
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "d") {
|
|
11451
|
-
//
|
|
11631
|
+
// COPY-2: 有选区(输入框内或终端输出区)时放行浏览器复制。
|
|
11452
11632
|
var inputBox2 = document.getElementById("input-box");
|
|
11453
|
-
var hasSelection2 = inputBox2 && (inputBox2.selectionStart !== inputBox2.selectionEnd)
|
|
11633
|
+
var hasSelection2 = (inputBox2 && (inputBox2.selectionStart !== inputBox2.selectionEnd))
|
|
11634
|
+
|| hasActiveTerminalSelection();
|
|
11454
11635
|
if (hasSelection2) {
|
|
11455
11636
|
return; // Let browser handle copy
|
|
11456
11637
|
}
|
|
11638
|
+
var cdSess = getSelectedSession();
|
|
11639
|
+
if (isStructuredSession(cdSess)) {
|
|
11640
|
+
// INPUT-2: 结构化会话吞掉 Ctrl+D(EOF 对 Claude 对话无意义,别当消息发)
|
|
11641
|
+
event.preventDefault();
|
|
11642
|
+
return;
|
|
11643
|
+
}
|
|
11457
11644
|
event.preventDefault();
|
|
11458
11645
|
queueDirectInput(getControlInput("ctrl_d"), "ctrl_d");
|
|
11459
11646
|
return;
|
|
@@ -11461,6 +11648,11 @@
|
|
|
11461
11648
|
|
|
11462
11649
|
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "l") {
|
|
11463
11650
|
event.preventDefault();
|
|
11651
|
+
var clSess = getSelectedSession();
|
|
11652
|
+
if (isStructuredSession(clSess)) {
|
|
11653
|
+
// INPUT-2: 结构化会话吞掉 Ctrl+L(清屏对 Claude 对话无意义)
|
|
11654
|
+
return;
|
|
11655
|
+
}
|
|
11464
11656
|
queueDirectInput(getControlInput("ctrl_l"), "ctrl_l");
|
|
11465
11657
|
return;
|
|
11466
11658
|
}
|
|
@@ -13547,10 +13739,12 @@
|
|
|
13547
13739
|
composer.placeholder = getComposerPlaceholder(selectedSession, state.terminalInteractive);
|
|
13548
13740
|
composer.disabled = !structured && !!selectedSession && !isRunning && !canResumeOnSend;
|
|
13549
13741
|
composer.setAttribute("aria-disabled", composer.disabled ? "true" : "false");
|
|
13550
|
-
//
|
|
13551
|
-
//
|
|
13552
|
-
//
|
|
13553
|
-
|
|
13742
|
+
// INPUT-3: 交互模式不再设 readOnly。readOnly 的 textarea 上 IME 根本不会
|
|
13743
|
+
// 激活(compositionstart 不触发),导致中文/日文等组字输入彻底打不出。普通
|
|
13744
|
+
// 字符 keydown 已由 capture 阶段的 captureTerminalInput preventDefault 拦截、
|
|
13745
|
+
// 不会落进 textarea;唯独 IME 组字期间 capture 放行(isComposing),字符临时
|
|
13746
|
+
// 落入 textarea,由 compositionend 取最终文本发 PTY 后清空。
|
|
13747
|
+
composer.readOnly = false;
|
|
13554
13748
|
composer.classList.toggle("is-terminal-passthrough", !!state.terminalInteractive);
|
|
13555
13749
|
}
|
|
13556
13750
|
var sendBtn = document.getElementById("send-input-button");
|
|
@@ -13566,15 +13760,40 @@
|
|
|
13566
13760
|
if (container) container.classList.toggle("interactive", !structured && state.terminalInteractive);
|
|
13567
13761
|
}
|
|
13568
13762
|
|
|
13763
|
+
// COPY-2/COPY-4: 是否存在落在终端输出区(#output)内的活动文本选区。用于:
|
|
13764
|
+
// 有选区时 Ctrl+C 放行浏览器原生复制而非发 SIGINT;click 不抢焦点以免打断
|
|
13765
|
+
// 双击选词/三击选行后的复制。
|
|
13766
|
+
function hasActiveTerminalSelection() {
|
|
13767
|
+
var sel = window.getSelection && window.getSelection();
|
|
13768
|
+
if (!sel || sel.isCollapsed) return false;
|
|
13769
|
+
var output = document.getElementById("output");
|
|
13770
|
+
if (!output) return false;
|
|
13771
|
+
var node = sel.anchorNode;
|
|
13772
|
+
if (node && node.nodeType === 3) node = node.parentNode;
|
|
13773
|
+
return !!(node && output.contains(node));
|
|
13774
|
+
}
|
|
13775
|
+
|
|
13569
13776
|
function captureTerminalInput(event) {
|
|
13570
13777
|
if (!shouldCaptureTerminalEvent(event)) return;
|
|
13778
|
+
// INPUT-1: 放行 Cmd/Meta 组合键给浏览器(复制/粘贴/刷新/切标签)。PTY 用
|
|
13779
|
+
// Ctrl 不用 Cmd,拦下来既破坏 macOS 原生快捷键,又会把裸字母(Cmd+X→'x')
|
|
13780
|
+
// 误塞进 PTY。
|
|
13781
|
+
if (event.metaKey) return;
|
|
13571
13782
|
var key = keyFromKeyboardEvent(event);
|
|
13572
13783
|
if (!key) return;
|
|
13573
|
-
event.preventDefault();
|
|
13574
13784
|
var mods = getModifierStateFromEvent(event, key);
|
|
13575
13785
|
if (isModifierKey(key)) return;
|
|
13786
|
+
// COPY-2: 有选区时 Ctrl+C 放行浏览器原生复制,而不是发 SIGINT(0x03) 把进程
|
|
13787
|
+
// 杀了还复制不到。无选区的 Ctrl+C 仍透传给 PTY。
|
|
13788
|
+
if (mods.ctrl && key.length === 1 && key.toLowerCase() === "c" && hasActiveTerminalSelection()) {
|
|
13789
|
+
return;
|
|
13790
|
+
}
|
|
13576
13791
|
var sequence = buildPtySequence(key, mods);
|
|
13577
|
-
|
|
13792
|
+
// INPUT-4: 只有真正要发给 PTY 的键才 preventDefault;空序列(F5/F12/死键等)
|
|
13793
|
+
// 放行给浏览器,避免"既没发 PTY 又吞掉浏览器默认行为"。
|
|
13794
|
+
if (!sequence) return;
|
|
13795
|
+
event.preventDefault();
|
|
13796
|
+
sendTerminalSequence(sequence, key);
|
|
13578
13797
|
}
|
|
13579
13798
|
|
|
13580
13799
|
function handleMiniKeyboardClick(event) {
|
|
@@ -15255,7 +15474,11 @@
|
|
|
15255
15474
|
var now = Date.now();
|
|
15256
15475
|
var chunkPause = state.lastChunkAt > 0 && (now - state.lastChunkAt > 300);
|
|
15257
15476
|
var resyncDue = (now - state.lastTerminalResyncAt) > 30000;
|
|
15258
|
-
|
|
15477
|
+
// RENDER-2: 仅在"自上次 resync 以来确有新输出"时才重放。静止/已结束会话
|
|
15478
|
+
// buffer 不再变化,30s 周期 resync 是纯无用功,还会把 cursor-home 中间帧
|
|
15479
|
+
// 重堆进 scrollback、扰动滚动位置。lastChunkAt>lastTerminalResyncAt 即脏。
|
|
15480
|
+
var dirtySinceResync = state.lastChunkAt > state.lastTerminalResyncAt;
|
|
15481
|
+
if (resyncDue && dirtySinceResync && (chunkPause || selectedSession.status !== "running") && state.terminalOutput) {
|
|
15259
15482
|
softResyncTerminal();
|
|
15260
15483
|
}
|
|
15261
15484
|
}, 5000);
|
|
@@ -15311,6 +15534,9 @@
|
|
|
15311
15534
|
if (state.terminalViewportTouchHandler) {
|
|
15312
15535
|
state.terminalViewportEl.removeEventListener("touchmove", state.terminalViewportTouchHandler);
|
|
15313
15536
|
}
|
|
15537
|
+
if (state.terminalViewportTouchStartHandler) {
|
|
15538
|
+
state.terminalViewportEl.removeEventListener("touchstart", state.terminalViewportTouchStartHandler);
|
|
15539
|
+
}
|
|
15314
15540
|
}
|
|
15315
15541
|
if (output) {
|
|
15316
15542
|
if (state.terminalWheelHandler) {
|
|
@@ -15323,13 +15549,24 @@
|
|
|
15323
15549
|
state.terminalViewportEl = null;
|
|
15324
15550
|
state.terminalViewportScrollHandler = null;
|
|
15325
15551
|
state.terminalViewportTouchHandler = null;
|
|
15552
|
+
state.terminalViewportTouchStartHandler = null;
|
|
15326
15553
|
state.terminalWheelHandler = null;
|
|
15327
15554
|
state.terminalClickHandler = null;
|
|
15555
|
+
// LIFE-1: 清理 initTerminalScrollbar 注册的 hide-timer 与拖拽状态。否则
|
|
15556
|
+
// hide-timer 闭包引用游离节点(置 El=null 拦不住它),且若拖拽中途 teardown,
|
|
15557
|
+
// 残留 dragging=true 会让新会话 scrollbar 的 scheduleHideScrollbar 被永久抑制
|
|
15558
|
+
// (开头 if(dragging)return)→ 滚动条出现后再也不自动消失。
|
|
15559
|
+
if (state.terminalScrollbarHideTimer) {
|
|
15560
|
+
clearTimeout(state.terminalScrollbarHideTimer);
|
|
15561
|
+
state.terminalScrollbarHideTimer = null;
|
|
15562
|
+
}
|
|
15328
15563
|
if (state.terminalScrollbarEl && state.terminalScrollbarEl.parentNode) {
|
|
15329
15564
|
state.terminalScrollbarEl.parentNode.removeChild(state.terminalScrollbarEl);
|
|
15330
15565
|
}
|
|
15331
15566
|
state.terminalScrollbarEl = null;
|
|
15332
15567
|
state.terminalScrollbarThumbEl = null;
|
|
15568
|
+
state.terminalScrollbarDragging = false;
|
|
15569
|
+
state.terminalScrollbarRafPending = false;
|
|
15333
15570
|
if (state.terminal) {
|
|
15334
15571
|
state.terminal.destroy();
|
|
15335
15572
|
state.terminal = null;
|
|
@@ -15354,6 +15591,7 @@
|
|
|
15354
15591
|
// 会让新会话的首批 PTY 字节全部被吞进 buffer 等永远不会来的 end。
|
|
15355
15592
|
state.syncOutputBuffer = null;
|
|
15356
15593
|
state.syncOutputDeadline = 0;
|
|
15594
|
+
state.syncFramingResidue = false;
|
|
15357
15595
|
state.terminalSessionId = null;
|
|
15358
15596
|
state.terminalOutput = "";
|
|
15359
15597
|
state.terminalOutputMarker = 0; // R8: teardown 时重置 /clear marker
|
|
@@ -15376,6 +15614,11 @@
|
|
|
15376
15614
|
_resyncStatsWindowStart = 0;
|
|
15377
15615
|
_resyncStatsCount = 0;
|
|
15378
15616
|
_resyncLastWarnAt = 0;
|
|
15617
|
+
_resyncInProgress = false;
|
|
15618
|
+
// SIZE-2: lastResize 是全局去重缓存,记"上次 POST 的尺寸"。切到另一个在不同
|
|
15619
|
+
// 尺寸下创建的会话时,若新算出的 cols/rows 恰好等于上次值会被去重跳过,导致
|
|
15620
|
+
// 后端该会话列宽停在旧值、整段折行。teardown 重置后新会话首次 resize 必发出。
|
|
15621
|
+
state.lastResize = { cols: 0, rows: 0 };
|
|
15379
15622
|
}
|
|
15380
15623
|
|
|
15381
15624
|
function sendTerminalResize(cols, rows) {
|
|
@@ -15388,6 +15631,10 @@
|
|
|
15388
15631
|
// wterm WASM grid 的 maxCols 硬编码 256。POST 给服务端的 cols 也同步
|
|
15389
15632
|
// clamp,避免服务端 pty.resize 给 Claude 一个 wterm 实际渲不下的列宽。
|
|
15390
15633
|
if (cols > 256) cols = 256;
|
|
15634
|
+
// SIZE-1: rows 也 clamp 到后端上限 160(process-manager clampDimension 10..160)。
|
|
15635
|
+
// 否则高分屏+小字号客户端算出 rows>160 时,后端压到 160,客户端网格底部多出的
|
|
15636
|
+
// 行对应的 PTY 内容永不写入 → 底部大片空白 / 光标错位 / 全屏菜单画到不存在的行。
|
|
15637
|
+
if (rows > 160) rows = 160;
|
|
15391
15638
|
var nextSize = { cols: cols, rows: rows };
|
|
15392
15639
|
if (state.lastResize.cols !== nextSize.cols || state.lastResize.rows !== nextSize.rows) {
|
|
15393
15640
|
state.lastResize = nextSize;
|
|
@@ -574,9 +574,8 @@
|
|
|
574
574
|
justify-content: center;
|
|
575
575
|
padding: 0;
|
|
576
576
|
box-shadow:
|
|
577
|
-
0 1px
|
|
578
|
-
inset 0 1px 0 rgba(255, 255, 255, 0.28)
|
|
579
|
-
inset 0 -1px 0 rgba(0, 0, 0, 0.08);
|
|
577
|
+
0 1px 2px rgba(184, 92, 55, 0.22),
|
|
578
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.28);
|
|
580
579
|
transition:
|
|
581
580
|
filter var(--transition-fast),
|
|
582
581
|
box-shadow 0.22s ease,
|
|
@@ -586,40 +585,31 @@
|
|
|
586
585
|
filter: brightness(1.06);
|
|
587
586
|
transform: translateY(-1px);
|
|
588
587
|
box-shadow:
|
|
589
|
-
0 4px
|
|
590
|
-
inset 0 1px 0 rgba(255, 255, 255, 0.
|
|
591
|
-
inset 0 -1px 0 rgba(0, 0, 0, 0.08);
|
|
588
|
+
0 4px 10px -3px rgba(184, 92, 55, 0.36),
|
|
589
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.32);
|
|
592
590
|
}
|
|
593
591
|
/* History tile — softer cream tone to distinguish from active sessions */
|
|
594
592
|
.sidebar-collapsed-tile.history {
|
|
595
593
|
background: linear-gradient(145deg, #e8d3c0 0%, #d3b69d 50%, #b89478 100%);
|
|
596
594
|
color: rgba(89, 58, 32, 0.88);
|
|
597
595
|
box-shadow:
|
|
598
|
-
0 1px
|
|
599
|
-
inset 0 1px 0 rgba(255, 255, 255, 0.
|
|
600
|
-
inset 0 -1px 0 rgba(0, 0, 0, 0.06);
|
|
596
|
+
0 1px 2px rgba(120, 88, 56, 0.18),
|
|
597
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.42);
|
|
601
598
|
}
|
|
602
599
|
.sidebar-collapsed-tile.history:hover {
|
|
603
600
|
box-shadow:
|
|
604
|
-
0 4px
|
|
605
|
-
inset 0 1px 0 rgba(255, 255, 255, 0.
|
|
606
|
-
inset 0 -1px 0 rgba(0, 0, 0, 0.06);
|
|
601
|
+
0 4px 10px -3px rgba(120, 88, 56, 0.3),
|
|
602
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.48);
|
|
607
603
|
}
|
|
608
|
-
/* Active —
|
|
604
|
+
/* Active — accent ring + 一层柔和暖投影 + 顶高光。原先 7 层叠加在 36px 小块上
|
|
605
|
+
糊成一团,精简到 3 层即可表达「升起 + 选中」。 */
|
|
609
606
|
.sidebar-collapsed-tile.active {
|
|
610
607
|
background: linear-gradient(145deg, #d27358 0%, #b35434 50%, #934128 100%);
|
|
611
608
|
transform: translateY(-1px);
|
|
612
609
|
box-shadow:
|
|
613
|
-
|
|
614
|
-
0
|
|
615
|
-
|
|
616
|
-
0 8px 22px -4px rgba(160, 74, 46, 0.55),
|
|
617
|
-
0 3px 8px -2px rgba(160, 74, 46, 0.32),
|
|
618
|
-
/* top sheen */
|
|
619
|
-
inset 0 1px 0 rgba(255, 255, 255, 0.45),
|
|
620
|
-
inset 1px 0 0 rgba(255, 255, 255, 0.18),
|
|
621
|
-
/* bottom depth */
|
|
622
|
-
inset 0 -1px 0 rgba(0, 0, 0, 0.16);
|
|
610
|
+
0 0 0 2px rgba(197, 101, 61, 0.3),
|
|
611
|
+
0 5px 14px -4px rgba(160, 74, 46, 0.45),
|
|
612
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.4);
|
|
623
613
|
}
|
|
624
614
|
.sidebar-collapsed-tile:active {
|
|
625
615
|
transform: translateY(0) scale(0.94);
|
|
@@ -9034,7 +9024,6 @@
|
|
|
9034
9024
|
background: rgba(178, 79, 69, 0.12);
|
|
9035
9025
|
color: var(--danger);
|
|
9036
9026
|
transform: rotate(90deg);
|
|
9037
|
-
box-shadow: 0 2px 8px rgba(178, 79, 69, 0.16);
|
|
9038
9027
|
}
|
|
9039
9028
|
.drawer-close-btn:active {
|
|
9040
9029
|
background: rgba(178, 79, 69, 0.2);
|
|
@@ -9184,17 +9173,17 @@
|
|
|
9184
9173
|
opacity: 1;
|
|
9185
9174
|
}
|
|
9186
9175
|
|
|
9187
|
-
/* ── Session item — clean card on glass ──
|
|
9176
|
+
/* ── Session item — clean card on glass ──
|
|
9177
|
+
克制原则:基础态用 hairline 描边 + 极轻顶部高光让卡片「贴」在侧栏上,
|
|
9178
|
+
不给每张卡都加漂浮投影;hover/active 才升起一层柔和投影。避免多层
|
|
9179
|
+
box-shadow 在半透明侧栏里互相叠加显脏。 */
|
|
9188
9180
|
.session-item {
|
|
9189
|
-
background: linear-gradient(180deg, rgba(255, 255, 255, 0.
|
|
9190
|
-
border:
|
|
9181
|
+
background: linear-gradient(180deg, rgba(255, 255, 255, 0.55) 0%, rgba(255, 252, 247, 0.38) 100%);
|
|
9182
|
+
border: 1px solid rgba(125, 91, 57, 0.08);
|
|
9191
9183
|
border-radius: 13px;
|
|
9192
9184
|
padding: 11px 14px;
|
|
9193
9185
|
margin-bottom: 6px;
|
|
9194
|
-
box-shadow:
|
|
9195
|
-
0 0 0 0.5px rgba(125, 91, 57, 0.05),
|
|
9196
|
-
0 1px 2px rgba(125, 91, 57, 0.03),
|
|
9197
|
-
inset 0 1px 0 rgba(255, 255, 255, 0.6);
|
|
9186
|
+
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5);
|
|
9198
9187
|
transition:
|
|
9199
9188
|
background 0.2s ease,
|
|
9200
9189
|
border-color 0.2s ease,
|
|
@@ -9211,53 +9200,39 @@
|
|
|
9211
9200
|
}
|
|
9212
9201
|
.session-item:hover {
|
|
9213
9202
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.85) 0%, rgba(255, 253, 248, 0.62) 100%);
|
|
9214
|
-
border-color: rgba(
|
|
9203
|
+
border-color: rgba(125, 91, 57, 0.14);
|
|
9215
9204
|
transform: translateY(-1px);
|
|
9216
|
-
box-shadow:
|
|
9217
|
-
0 0 0 0.5px rgba(125, 91, 57, 0.08),
|
|
9218
|
-
0 6px 16px -6px rgba(125, 91, 57, 0.18),
|
|
9219
|
-
inset 0 1px 0 rgba(255, 255, 255, 0.8);
|
|
9205
|
+
box-shadow: 0 4px 12px -4px rgba(125, 91, 57, 0.16);
|
|
9220
9206
|
}
|
|
9221
9207
|
.session-item:hover::before {
|
|
9222
9208
|
opacity: 0.5;
|
|
9223
9209
|
transform: scaleY(1);
|
|
9224
9210
|
}
|
|
9225
|
-
/* Selected (active) —
|
|
9211
|
+
/* Selected (active) — accent ring 用边框表达,再叠一层柔和暖投影 + 顶高光。
|
|
9212
|
+
去掉原先的 6 层叠加(外环/光晕/底影/三向 inset),层数过多在玻璃上显脏。 */
|
|
9226
9213
|
.session-item.active {
|
|
9227
9214
|
background:
|
|
9228
9215
|
linear-gradient(180deg, rgba(255, 252, 247, 0.92) 0%, rgba(255, 244, 232, 0.78) 100%);
|
|
9229
|
-
border:
|
|
9216
|
+
border: 1px solid rgba(197, 101, 61, 0.32);
|
|
9230
9217
|
transform: translateY(-1px);
|
|
9231
9218
|
backdrop-filter: blur(20px) saturate(180%);
|
|
9232
9219
|
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
|
9233
9220
|
box-shadow:
|
|
9234
|
-
|
|
9235
|
-
|
|
9236
|
-
/* warm glow halo */
|
|
9237
|
-
0 8px 28px -6px rgba(197, 101, 61, 0.32),
|
|
9238
|
-
/* base elevation */
|
|
9239
|
-
0 2px 6px -1px rgba(125, 91, 57, 0.1),
|
|
9240
|
-
/* top inner highlight — light from above */
|
|
9241
|
-
inset 0 1px 0 rgba(255, 255, 255, 0.95),
|
|
9242
|
-
/* bottom inner shadow — depth at base */
|
|
9243
|
-
inset 0 -1px 0 rgba(197, 101, 61, 0.08),
|
|
9244
|
-
/* left inner glint */
|
|
9245
|
-
inset 1px 0 0 rgba(255, 255, 255, 0.4);
|
|
9221
|
+
0 4px 14px -4px rgba(197, 101, 61, 0.24),
|
|
9222
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.7);
|
|
9246
9223
|
}
|
|
9224
|
+
/* ::before 的左侧高亮条受父级 overflow:hidden 裁切,原来的 box-shadow 光晕
|
|
9225
|
+
根本显示不出来,移除以省一次绘制。 */
|
|
9247
9226
|
.session-item.active::before {
|
|
9248
9227
|
opacity: 1;
|
|
9249
9228
|
transform: scaleY(1);
|
|
9250
|
-
box-shadow: 0 0 8px rgba(197, 101, 61, 0.5);
|
|
9251
9229
|
}
|
|
9252
9230
|
/* Multi-select state — quieter glass tint, no accent halo */
|
|
9253
9231
|
.session-item.selected {
|
|
9254
9232
|
background:
|
|
9255
9233
|
linear-gradient(180deg, rgba(255, 250, 244, 0.85) 0%, rgba(255, 246, 236, 0.65) 100%);
|
|
9256
|
-
border:
|
|
9257
|
-
box-shadow:
|
|
9258
|
-
0 0 0 1px rgba(197, 101, 61, 0.18),
|
|
9259
|
-
0 4px 14px -4px rgba(125, 91, 57, 0.14),
|
|
9260
|
-
inset 0 1px 0 rgba(255, 255, 255, 0.85);
|
|
9234
|
+
border: 1px solid rgba(197, 101, 61, 0.2);
|
|
9235
|
+
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.6);
|
|
9261
9236
|
}
|
|
9262
9237
|
.session-item.selected::before {
|
|
9263
9238
|
opacity: 0.7;
|
|
@@ -9402,21 +9377,19 @@
|
|
|
9402
9377
|
transform: scale(0.96);
|
|
9403
9378
|
transition-duration: 0.06s;
|
|
9404
9379
|
}
|
|
9405
|
-
/* Footer tab active — same liquid glass language as .session-item.active
|
|
9380
|
+
/* Footer tab active — same liquid glass language as .session-item.active
|
|
9381
|
+
(同步精简为 accent 边框 + 1 层柔和投影 + 顶高光) */
|
|
9406
9382
|
.sidebar-footer-actions .btn.active {
|
|
9407
9383
|
background:
|
|
9408
9384
|
linear-gradient(180deg, rgba(255, 252, 247, 0.95) 0%, rgba(255, 242, 228, 0.78) 100%);
|
|
9409
|
-
border:
|
|
9385
|
+
border: 1px solid rgba(197, 101, 61, 0.28);
|
|
9410
9386
|
color: var(--accent);
|
|
9411
9387
|
transform: translateY(-1px);
|
|
9412
9388
|
backdrop-filter: blur(16px) saturate(180%);
|
|
9413
9389
|
-webkit-backdrop-filter: blur(16px) saturate(180%);
|
|
9414
9390
|
box-shadow:
|
|
9415
|
-
0
|
|
9416
|
-
0
|
|
9417
|
-
0 2px 4px -1px rgba(125, 91, 57, 0.08),
|
|
9418
|
-
inset 0 1px 0 rgba(255, 255, 255, 0.95),
|
|
9419
|
-
inset 0 -1px 0 rgba(197, 101, 61, 0.08);
|
|
9391
|
+
0 3px 10px -3px rgba(197, 101, 61, 0.22),
|
|
9392
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.7);
|
|
9420
9393
|
}
|
|
9421
9394
|
.sidebar-footer-actions .btn.sidebar-logout:hover {
|
|
9422
9395
|
background: rgba(178, 79, 69, 0.08);
|