@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.
@@ -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
- loadClaudeHistory();
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
- if (state.sidebarPinned && !isMobileLayout()) {
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.toggle("open");
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
- loadClaudeHistory();
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
- && !isTerminalNearBottom();
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
- if (text.indexOf(WAND_WIDE_FILLER) === -1) return;
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", text.split(WAND_WIDE_FILLER).join(""));
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
- resetTerminal();
7776
- wandTerminalWrite(state.terminal, replaySource);
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
- state.terminalViewportTouchHandler = function() {
8057
- setTerminalManualScrollActive();
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 (!isTerminalNearBottom() || e.deltaY < 0) {
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
- state.terminalClickHandler = focusInputBox;
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
- queueDirectInput(getControlInput("escape"), "escape");
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
- // Allow copy when text is selected; otherwise send SIGINT to terminal
11440
- var inputBox = document.getElementById("input-box");
11441
- var hasSelection = inputBox && (inputBox.selectionStart !== inputBox.selectionEnd);
11442
- if (hasSelection) {
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
- // Allow copy when text is selected; otherwise send EOF to terminal
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
- // 终端交互模式下按键由 document capture phase 透传到 PTY;用
13551
- // readOnly 而非 disabled 防止 IME 组合输入等边界场景下字符同时
13552
- // 落到 textarea,又保留 focus 能力。
13553
- composer.readOnly = !!state.terminalInteractive;
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
- if (sequence) sendTerminalSequence(sequence, key);
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
- if (resyncDue && (chunkPause || selectedSession.status !== "running") && state.terminalOutput) {
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 3px rgba(184, 92, 55, 0.28),
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 12px -2px rgba(184, 92, 55, 0.42),
590
- inset 0 1px 0 rgba(255, 255, 255, 0.35),
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 3px rgba(120, 88, 56, 0.22),
599
- inset 0 1px 0 rgba(255, 255, 255, 0.45),
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 12px -2px rgba(120, 88, 56, 0.34),
605
- inset 0 1px 0 rgba(255, 255, 255, 0.5),
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 — liquid glass crown: outer ring + warm halo + crisp top sheen */
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
- /* outer glow halo */
614
- 0 0 0 1px rgba(255, 255, 255, 0.7),
615
- 0 0 0 3px rgba(197, 101, 61, 0.32),
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.62) 0%, rgba(255, 252, 247, 0.42) 100%);
9190
- border: 0.5px solid rgba(255, 255, 255, 0.55);
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(255, 255, 255, 0.7);
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) — true liquid glass pill with depth + accent glow */
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: 0.5px solid rgba(255, 255, 255, 0.85);
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
- /* outer accent ring (soft) */
9235
- 0 0 0 1px rgba(197, 101, 61, 0.28),
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: 0.5px solid rgba(255, 255, 255, 0.7);
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: 0.5px solid rgba(255, 255, 255, 0.85);
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 0 0 1px rgba(197, 101, 61, 0.28),
9416
- 0 6px 18px -4px rgba(197, 101, 61, 0.32),
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@co0ontty/wand",
3
- "version": "1.32.3",
3
+ "version": "1.34.0",
4
4
  "description": "A web terminal for local CLI tools like Claude.",
5
5
  "type": "module",
6
6
  "bin": {