@co0ontty/wand 1.14.2 → 1.14.6

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.
@@ -109,6 +109,9 @@
109
109
  loginChecked: false,
110
110
  bootstrapping: true,
111
111
  sessionsDrawerOpen: false,
112
+ sidebarPinned: (function() {
113
+ try { return localStorage.getItem("wand-sidebar-pinned") === "true"; } catch (e) { return false; }
114
+ })(),
112
115
  modalOpen: false,
113
116
  presetValue: "",
114
117
  cwdValue: "",
@@ -652,6 +655,7 @@
652
655
  if (!el) return false;
653
656
  switch (kind) {
654
657
  case "tool-card":
658
+ case "diff":
655
659
  return !el.classList.contains("collapsed");
656
660
  case "thinking":
657
661
  return el.classList.contains("expanded") && !el.classList.contains("collapsed");
@@ -672,7 +676,8 @@
672
676
  function applyExpandedState(el, kind, expanded) {
673
677
  if (!el) return;
674
678
  switch (kind) {
675
- case "tool-card": {
679
+ case "tool-card":
680
+ case "diff": {
676
681
  el.classList.toggle("collapsed", !expanded);
677
682
  break;
678
683
  }
@@ -932,6 +937,10 @@
932
937
  // Suppress CSS transitions during initial DOM build
933
938
  document.documentElement.classList.add("no-transition");
934
939
 
940
+ // Apply persisted pin state before rendering
941
+ if (state.sidebarPinned && !isMobileLayout()) {
942
+ state.sessionsDrawerOpen = true;
943
+ }
935
944
  app.innerHTML = isLoggedIn ? renderAppShell() : renderLogin();
936
945
  // Reset chat render tracking since DOM was fully replaced
937
946
  resetChatRenderCache();
@@ -1083,8 +1092,8 @@
1083
1092
 
1084
1093
  return '<div class="app-container">' +
1085
1094
  '<div id="sessions-drawer-backdrop" class="drawer-backdrop' + drawerClass + '"></div>' +
1086
- '<div class="main-layout' + (state.sessionsDrawerOpen ? ' sidebar-open' : '') + '">' +
1087
- '<aside id="sessions-drawer" class="sidebar' + drawerClass + '">' +
1095
+ '<div class="main-layout' + (state.sessionsDrawerOpen ? ' sidebar-open' : '') + (state.sidebarPinned && !isMobileLayout() ? ' sidebar-pinned' : '') + '">' +
1096
+ '<aside id="sessions-drawer" class="sidebar' + drawerClass + (state.sidebarPinned && !isMobileLayout() ? ' pinned' : '') + '">' +
1088
1097
  '<div class="sidebar-header">' +
1089
1098
  '<div class="sidebar-header-main">' +
1090
1099
  '<div class="topbar-logo-icon">W</div>' +
@@ -1098,6 +1107,9 @@
1098
1107
  '<button id="sidebar-refresh-btn" class="btn btn-ghost btn-sm" type="button" title="刷新页面">' +
1099
1108
  '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>' +
1100
1109
  '</button>' +
1110
+ '<button id="sidebar-pin-btn" class="btn btn-ghost btn-sm sidebar-pin-toggle' + (state.sidebarPinned ? ' pinned' : '') + '" type="button" title="' + (state.sidebarPinned ? '取消固定侧栏' : '固定侧栏') + '">' +
1111
+ '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="17" x2="12" y2="22"/><path d="M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24z"/></svg>' +
1112
+ '</button>' +
1101
1113
  '<button id="close-drawer-button" class="btn btn-ghost btn-sm sidebar-close" type="button" aria-label="关闭菜单">×</button>' +
1102
1114
  '</div>' +
1103
1115
  '</div>' +
@@ -1379,6 +1391,17 @@
1379
1391
  '<label class="field-label" for="cfg-notif-bubble">\u5e94\u7528\u5185\u901a\u77e5\u6c14\u6ce1</label>' +
1380
1392
  '</div>' +
1381
1393
  '<p class="hint" style="margin-top:0;margin-bottom:10px">\u5728\u9875\u9762\u9876\u90e8\u5f39\u51fa\u6d6e\u52a8\u901a\u77e5\u6c14\u6ce1</p>' +
1394
+ '<div id="native-sound-section" class="settings-notification-section hidden" style="margin-top:6px">' +
1395
+ '<div class="settings-section-title">\u7cfb\u7edf\u901a\u77e5\u94c3\u58f0</div>' +
1396
+ '<div class="settings-about-row">' +
1397
+ '<span class="settings-label">\u94c3\u58f0</span>' +
1398
+ '<div style="display:flex;align-items:center;gap:6px">' +
1399
+ '<select id="native-sound-select" class="field-select" style="min-width:100px"></select>' +
1400
+ '<button id="native-sound-preview" class="btn btn-ghost btn-sm">\u25b6 \u8bd5\u542c</button>' +
1401
+ '</div>' +
1402
+ '</div>' +
1403
+ '<p class="hint" style="margin-top:0">\u9009\u62e9 Android \u7cfb\u7edf\u901a\u77e5\u4f7f\u7528\u7684\u94c3\u58f0</p>' +
1404
+ '</div>' +
1382
1405
  '<div class="settings-notification-section" style="margin-top:6px">' +
1383
1406
  '<div class="settings-section-title">\u6d4f\u89c8\u5668\u901a\u77e5</div>' +
1384
1407
  '<div class="settings-about-row">' +
@@ -1514,32 +1537,49 @@
1514
1537
  '<div class="settings-panel" id="settings-tab-display">' +
1515
1538
  '<div class="settings-section-title">卡片默认展开状态</div>' +
1516
1539
  '<p class="hint" style="margin-top:-4px;margin-bottom:12px">设置结构化聊天视图中各类卡片的默认展开/折叠状态。手动操作的展开状态优先于此默认设置。</p>' +
1517
- '<div class="field field-inline">' +
1518
- '<input id="cfg-card-edit" type="checkbox" class="field-checkbox" />' +
1519
- '<label class="field-label" for="cfg-card-edit">编辑卡片 (Edit/Write)</label>' +
1520
- '</div>' +
1521
- '<p class="hint" style="margin-top:0;margin-bottom:10px">文件编辑和写入操作的 diff 视图</p>' +
1522
- '<div class="field field-inline">' +
1523
- '<input id="cfg-card-inline" type="checkbox" class="field-checkbox" />' +
1524
- '<label class="field-label" for="cfg-card-inline">内联工具 (Read/Glob/Grep)</label>' +
1525
- '</div>' +
1526
- '<p class="hint" style="margin-top:0;margin-bottom:10px">文件读取、搜索等工具的结果</p>' +
1527
- '<div class="field field-inline">' +
1528
- '<input id="cfg-card-terminal" type="checkbox" class="field-checkbox" />' +
1529
- '<label class="field-label" for="cfg-card-terminal">终端输出 (Bash)</label>' +
1530
- '</div>' +
1531
- '<p class="hint" style="margin-top:0;margin-bottom:10px">命令行执行结果</p>' +
1532
- '<div class="field field-inline">' +
1533
- '<input id="cfg-card-thinking" type="checkbox" class="field-checkbox" />' +
1534
- '<label class="field-label" for="cfg-card-thinking">思考过程 (Thinking)</label>' +
1535
- '</div>' +
1536
- '<p class="hint" style="margin-top:0;margin-bottom:10px">Claude 的思考过程块</p>' +
1537
- '<div class="field field-inline">' +
1538
- '<input id="cfg-card-toolgroup" type="checkbox" class="field-checkbox" />' +
1539
- '<label class="field-label" for="cfg-card-toolgroup">工具组</label>' +
1540
+ '<div class="switch-card-list">' +
1541
+ '<label class="switch-card" for="cfg-card-edit">' +
1542
+ '<div class="switch-card-header">' +
1543
+ '<span class="switch-card-title">编辑卡片 (Edit/Write)</span>' +
1544
+ '<input id="cfg-card-edit" type="checkbox" class="switch-toggle" />' +
1545
+ '<span class="switch-slider"></span>' +
1546
+ '</div>' +
1547
+ '<div class="switch-card-desc">文件编辑和写入操作的 diff 视图</div>' +
1548
+ '</label>' +
1549
+ '<label class="switch-card" for="cfg-card-inline">' +
1550
+ '<div class="switch-card-header">' +
1551
+ '<span class="switch-card-title">内联工具 (Read/Glob/Grep)</span>' +
1552
+ '<input id="cfg-card-inline" type="checkbox" class="switch-toggle" />' +
1553
+ '<span class="switch-slider"></span>' +
1554
+ '</div>' +
1555
+ '<div class="switch-card-desc">文件读取、搜索等工具的结果</div>' +
1556
+ '</label>' +
1557
+ '<label class="switch-card" for="cfg-card-terminal">' +
1558
+ '<div class="switch-card-header">' +
1559
+ '<span class="switch-card-title">终端输出 (Bash)</span>' +
1560
+ '<input id="cfg-card-terminal" type="checkbox" class="switch-toggle" />' +
1561
+ '<span class="switch-slider"></span>' +
1562
+ '</div>' +
1563
+ '<div class="switch-card-desc">命令行执行结果</div>' +
1564
+ '</label>' +
1565
+ '<label class="switch-card" for="cfg-card-thinking">' +
1566
+ '<div class="switch-card-header">' +
1567
+ '<span class="switch-card-title">思考过程 (Thinking)</span>' +
1568
+ '<input id="cfg-card-thinking" type="checkbox" class="switch-toggle" />' +
1569
+ '<span class="switch-slider"></span>' +
1570
+ '</div>' +
1571
+ '<div class="switch-card-desc">Claude 的思考过程块</div>' +
1572
+ '</label>' +
1573
+ '<label class="switch-card" for="cfg-card-toolgroup">' +
1574
+ '<div class="switch-card-header">' +
1575
+ '<span class="switch-card-title">工具组</span>' +
1576
+ '<input id="cfg-card-toolgroup" type="checkbox" class="switch-toggle" />' +
1577
+ '<span class="switch-slider"></span>' +
1578
+ '</div>' +
1579
+ '<div class="switch-card-desc">连续同类工具调用的折叠组</div>' +
1580
+ '</label>' +
1540
1581
  '</div>' +
1541
- '<p class="hint" style="margin-top:0;margin-bottom:10px">连续同类工具调用的折叠组</p>' +
1542
- '<button id="save-display-button" class="btn btn-primary btn-block">保存显示设置</button>' +
1582
+ '<button id="save-display-button" class="btn btn-primary btn-block" style="margin-top:16px">保存显示设置</button>' +
1543
1583
  '<p id="display-message" class="hint hidden"></p>' +
1544
1584
  '</div>' +
1545
1585
  '</div>' +
@@ -2757,11 +2797,12 @@
2757
2797
  }
2758
2798
 
2759
2799
  window.__tcToggle = function(e, headerEl) {
2760
- var card = headerEl.closest(".tool-use-card");
2800
+ var card = headerEl.closest(".tool-use-card") || headerEl.closest(".inline-diff");
2761
2801
  if (card) {
2762
2802
  var wasCollapsed = card.classList.contains("collapsed");
2763
2803
  card.classList.toggle("collapsed");
2764
- persistElementExpandState(card, "tool-card");
2804
+ var expandKind = card.dataset.expandKind || "tool-card";
2805
+ persistElementExpandState(card, expandKind);
2765
2806
  // Lazy-load truncated content on expand
2766
2807
  if (wasCollapsed && card.dataset.truncated === "true" && card.dataset.loaded !== "true") {
2767
2808
  var toolUseId = card.dataset.toolUseId;
@@ -2876,6 +2917,7 @@
2876
2917
  }
2877
2918
  });
2878
2919
  }
2920
+ }
2879
2921
  };
2880
2922
  // Update streaming thinking content (called from WebSocket handler)
2881
2923
  function updateStreamingThinking(text) {
@@ -3112,6 +3154,8 @@
3112
3154
  if (drawerBackdrop) drawerBackdrop.addEventListener("click", closeSessionsDrawer);
3113
3155
  var closeDrawerBtn = document.getElementById("close-drawer-button");
3114
3156
  if (closeDrawerBtn) closeDrawerBtn.addEventListener("click", closeSessionsDrawer);
3157
+ var pinBtn = document.getElementById("sidebar-pin-btn");
3158
+ if (pinBtn) pinBtn.addEventListener("click", toggleSidebarPin);
3115
3159
  var homeBtn = document.getElementById("sidebar-home-btn");
3116
3160
  if (homeBtn) homeBtn.addEventListener("click", function() {
3117
3161
  state.selectedId = null;
@@ -3219,6 +3263,35 @@
3219
3263
  var notifTestBtn = document.getElementById("notification-test-btn");
3220
3264
  if (notifTestBtn) notifTestBtn.addEventListener("click", testNotification);
3221
3265
  updateNotificationStatus();
3266
+ // Native notification sound selector (APK only)
3267
+ if (_hasNativeBridge && typeof WandNative.getAvailableSounds === "function") {
3268
+ var nativeSoundSection = document.getElementById("native-sound-section");
3269
+ var nativeSoundSelect = document.getElementById("native-sound-select");
3270
+ var nativeSoundPreview = document.getElementById("native-sound-preview");
3271
+ if (nativeSoundSection && nativeSoundSelect) {
3272
+ nativeSoundSection.classList.remove("hidden");
3273
+ try {
3274
+ var sounds = JSON.parse(WandNative.getAvailableSounds());
3275
+ var current = WandNative.getNotificationSound();
3276
+ nativeSoundSelect.innerHTML = "";
3277
+ for (var si = 0; si < sounds.length; si++) {
3278
+ var opt = document.createElement("option");
3279
+ opt.value = sounds[si].id;
3280
+ opt.textContent = sounds[si].name;
3281
+ if (sounds[si].id === current) opt.selected = true;
3282
+ nativeSoundSelect.appendChild(opt);
3283
+ }
3284
+ nativeSoundSelect.addEventListener("change", function() {
3285
+ try { WandNative.setNotificationSound(nativeSoundSelect.value); } catch (_e) {}
3286
+ });
3287
+ if (nativeSoundPreview) {
3288
+ nativeSoundPreview.addEventListener("click", function() {
3289
+ try { WandNative.previewSound(nativeSoundSelect.value); } catch (_e) {}
3290
+ });
3291
+ }
3292
+ } catch (_e) {}
3293
+ }
3294
+ }
3222
3295
  var newSessBtn = document.getElementById("topbar-new-session-button");
3223
3296
  if (newSessBtn) newSessBtn.addEventListener("click", openSessionModal);
3224
3297
  var drawerNewSessBtn = document.getElementById("drawer-new-session-button");
@@ -3785,7 +3858,9 @@
3785
3858
  } else {
3786
3859
  selectSession(sessionId);
3787
3860
  }
3788
- closeSessionsDrawer();
3861
+ if (!state.sidebarPinned || isMobileLayout()) {
3862
+ closeSessionsDrawer();
3863
+ }
3789
3864
  }
3790
3865
 
3791
3866
  function handleSessionItemClick(event) {
@@ -4290,6 +4365,12 @@
4290
4365
  } else {
4291
4366
  updateTerminalJumpToBottomButton();
4292
4367
  }
4368
+ // When switching sessions, re-fit the terminal so the PTY receives
4369
+ // the correct dimensions for this client's viewport.
4370
+ if (sessionChanged && state.fitAddon) {
4371
+ state.terminalViewportSize = { width: 0, height: 0 };
4372
+ scheduleTerminalResize(true);
4373
+ }
4293
4374
  return wrote || sessionChanged;
4294
4375
  }
4295
4376
 
@@ -4364,6 +4445,26 @@
4364
4445
  state.fitAddon = fitAddonConstructor ? new fitAddonConstructor() : null;
4365
4446
  if (state.fitAddon) {
4366
4447
  state.terminal.loadAddon(state.fitAddon);
4448
+ // Patch: FitAddon subtracts 14px for a scrollbar that CSS hides;
4449
+ // recalculate cols without the scrollbar deduction.
4450
+ var _origPropose = state.fitAddon.proposeDimensions;
4451
+ state.fitAddon.proposeDimensions = function() {
4452
+ var result = _origPropose.call(state.fitAddon);
4453
+ if (result && state.terminal) {
4454
+ try {
4455
+ var core = state.terminal._core;
4456
+ var cellW = core._renderService.dimensions.css.cell.width;
4457
+ var el = state.terminal.element;
4458
+ if (cellW > 0 && el && el.parentElement) {
4459
+ var pw = Math.max(0, parseInt(window.getComputedStyle(el.parentElement).getPropertyValue("width")));
4460
+ var es = window.getComputedStyle(el);
4461
+ var ePad = parseInt(es.getPropertyValue("padding-left")) + parseInt(es.getPropertyValue("padding-right"));
4462
+ result.cols = Math.max(2, Math.floor((pw - ePad) / cellW));
4463
+ }
4464
+ } catch(e) {}
4465
+ }
4466
+ return result;
4467
+ };
4367
4468
  } else {
4368
4469
  console.error("[wand] xterm fit addon failed to load; continuing without fit support.");
4369
4470
  }
@@ -4382,6 +4483,24 @@
4382
4483
  // Retry-based fit: wait for browser to complete layout before measuring and fitting
4383
4484
  if (state.fitAddon) {
4384
4485
  ensureTerminalFit();
4486
+ // Secondary fit after fonts are loaded — FitAddon measures character
4487
+ // dimensions from the rendered font; if a custom web font (e.g. Geist
4488
+ // Mono) hasn't loaded yet the initial fit() uses fallback metrics and
4489
+ // computes too few columns.
4490
+ if (document.fonts && document.fonts.ready) {
4491
+ document.fonts.ready.then(function() {
4492
+ state.terminalViewportSize = { width: 0, height: 0 };
4493
+ ensureTerminalFit();
4494
+ });
4495
+ }
4496
+ // Safety-net fit after layout has fully stabilised (CSS transitions,
4497
+ // deferred reflows, late font loads, etc.)
4498
+ setTimeout(function() {
4499
+ if (state.terminal && state.fitAddon) {
4500
+ state.terminalViewportSize = { width: 0, height: 0 };
4501
+ ensureTerminalFit();
4502
+ }
4503
+ }, 500);
4385
4504
  }
4386
4505
 
4387
4506
  var viewport = getTerminalViewport();
@@ -5152,6 +5271,22 @@
5152
5271
  subscribeToSession(id);
5153
5272
  }
5154
5273
 
5274
+ function updatePinState() {
5275
+ var drawer = document.getElementById("sessions-drawer");
5276
+ var mainLayout = document.querySelector(".main-layout");
5277
+ var pinBtn = document.getElementById("sidebar-pin-btn");
5278
+ if (drawer) {
5279
+ drawer.classList.toggle("pinned", state.sidebarPinned && !isMobileLayout());
5280
+ }
5281
+ if (mainLayout) {
5282
+ mainLayout.classList.toggle("sidebar-pinned", state.sidebarPinned && !isMobileLayout());
5283
+ }
5284
+ if (pinBtn) {
5285
+ pinBtn.classList.toggle("pinned", state.sidebarPinned);
5286
+ pinBtn.title = state.sidebarPinned ? "取消固定侧栏" : "固定侧栏";
5287
+ }
5288
+ }
5289
+
5155
5290
  function updateDrawerState() {
5156
5291
  var drawer = document.getElementById("sessions-drawer");
5157
5292
  var backdrop = document.getElementById("sessions-drawer-backdrop");
@@ -5169,9 +5304,11 @@
5169
5304
  if (toggleBtn) {
5170
5305
  toggleBtn.classList.toggle("active", state.sessionsDrawerOpen);
5171
5306
  }
5307
+ updatePinState();
5172
5308
  }
5173
5309
 
5174
5310
  function toggleSessionsDrawer() {
5311
+ if (state.sidebarPinned && !isMobileLayout()) return;
5175
5312
  state.sessionsDrawerOpen = !state.sessionsDrawerOpen;
5176
5313
  if (state.sessionsDrawerOpen && isMobileLayout()) {
5177
5314
  state.filePanelOpen = false;
@@ -5183,12 +5320,38 @@
5183
5320
  }
5184
5321
 
5185
5322
  function closeSessionsDrawer() {
5323
+ if (state.sidebarPinned && !isMobileLayout()) return;
5186
5324
  if (!state.sessionsDrawerOpen) return;
5187
5325
  closeSwipedItem();
5188
5326
  state.sessionsDrawerOpen = false;
5189
5327
  updateLayoutState();
5190
5328
  }
5191
5329
 
5330
+ function toggleSidebarPin() {
5331
+ if (isMobileLayout()) return;
5332
+ state.sidebarPinned = !state.sidebarPinned;
5333
+ try {
5334
+ localStorage.setItem("wand-sidebar-pinned", String(state.sidebarPinned));
5335
+ } catch (e) {}
5336
+ if (state.sidebarPinned) {
5337
+ state.sessionsDrawerOpen = true;
5338
+ }
5339
+ updateLayoutState();
5340
+ // Refit terminal after padding-left transition completes
5341
+ var mainLayout = document.querySelector(".main-layout");
5342
+ if (mainLayout) {
5343
+ var onEnd = function(e) {
5344
+ if (e.propertyName === "padding-left") {
5345
+ mainLayout.removeEventListener("transitionend", onEnd);
5346
+ scheduleTerminalResize(true);
5347
+ }
5348
+ };
5349
+ mainLayout.addEventListener("transitionend", onEnd);
5350
+ }
5351
+ // Fallback refit in case transition doesn't fire
5352
+ setTimeout(function() { scheduleTerminalResize(true); }, 350);
5353
+ }
5354
+
5192
5355
  // Store last focused element for focus trap
5193
5356
  var lastFocusedElement = null;
5194
5357
  var focusTrapHandler = null;
@@ -5454,6 +5617,13 @@
5454
5617
  if (typeof WandNative !== "undefined" && typeof WandNative.getAppIcon === "function") {
5455
5618
  try { _updateAppIconSelection(WandNative.getAppIcon() || "shorthair"); } catch (_e) {}
5456
5619
  }
5620
+ // Sync native notification sound selector (APK only)
5621
+ if (_hasNativeBridge && typeof WandNative.getNotificationSound === "function") {
5622
+ try {
5623
+ var nsSel = document.getElementById("native-sound-select");
5624
+ if (nsSel) nsSel.value = WandNative.getNotificationSound();
5625
+ } catch (_e) {}
5626
+ }
5457
5627
  }
5458
5628
  }
5459
5629
 
@@ -9277,7 +9447,22 @@
9277
9447
  state.resizeObserver = new ResizeObserver(function() { scheduleTerminalResize(true); });
9278
9448
  state.resizeObserver.observe(output);
9279
9449
  }
9280
- state.resizeHandler = function() { scheduleTerminalResize(true); };
9450
+ var lastKnownDesktop = !isMobileLayout();
9451
+ state.resizeHandler = function() {
9452
+ scheduleTerminalResize(true);
9453
+ // Handle sidebar pin state across mobile/desktop breakpoint
9454
+ var isDesktop = !isMobileLayout();
9455
+ if (lastKnownDesktop !== isDesktop) {
9456
+ lastKnownDesktop = isDesktop;
9457
+ if (!isDesktop && state.sidebarPinned && state.sessionsDrawerOpen) {
9458
+ state.sessionsDrawerOpen = false;
9459
+ updateDrawerState();
9460
+ } else if (isDesktop && state.sidebarPinned && !state.sessionsDrawerOpen) {
9461
+ state.sessionsDrawerOpen = true;
9462
+ updateDrawerState();
9463
+ }
9464
+ }
9465
+ };
9281
9466
  window.addEventListener("resize", state.resizeHandler);
9282
9467
  // Also listen to visualViewport resize for pinch-zoom / browser zoom
9283
9468
  if (window.visualViewport) {
@@ -9347,8 +9532,24 @@
9347
9532
  updateTerminalJumpToBottomButton();
9348
9533
  }
9349
9534
 
9535
+ function sendTerminalResize(cols, rows) {
9536
+ if (!state.selectedId) return;
9537
+ var selectedSess = state.sessions.find(function(s) { return s.id === state.selectedId; });
9538
+ if (isStructuredSession(selectedSess)) return;
9539
+ var nextSize = { cols: cols, rows: rows };
9540
+ if (state.lastResize.cols !== nextSize.cols || state.lastResize.rows !== nextSize.rows) {
9541
+ state.lastResize = nextSize;
9542
+ fetch("/api/sessions/" + state.selectedId + "/resize", {
9543
+ method: "POST",
9544
+ headers: { "Content-Type": "application/json" },
9545
+ credentials: "same-origin",
9546
+ body: JSON.stringify(nextSize)
9547
+ }).catch(function() {});
9548
+ }
9549
+ }
9550
+
9350
9551
  function ensureTerminalFit() {
9351
- var maxAttempts = 10;
9552
+ var maxAttempts = 20;
9352
9553
  var attempt = 0;
9353
9554
  function tryFit() {
9354
9555
  attempt++;
@@ -9356,19 +9557,19 @@
9356
9557
  if (shouldResizeTerminalViewport() && state.fitAddon) {
9357
9558
  state.fitAddon.fit();
9358
9559
  maybeScrollTerminalToBottom("resize");
9359
- if (state.selectedId && state.terminal) {
9360
- var selectedSess = state.sessions.find(function(s) { return s.id === state.selectedId; });
9361
- if (!isStructuredSession(selectedSess)) {
9362
- var nextSize = { cols: state.terminal.cols, rows: state.terminal.rows };
9363
- if (state.lastResize.cols !== nextSize.cols || state.lastResize.rows !== nextSize.rows) {
9364
- state.lastResize = nextSize;
9365
- fetch("/api/sessions/" + state.selectedId + "/resize", {
9366
- method: "POST",
9367
- headers: { "Content-Type": "application/json" },
9368
- credentials: "same-origin",
9369
- body: JSON.stringify(nextSize)
9370
- }).catch(function() {});
9371
- }
9560
+ if (state.terminal) {
9561
+ sendTerminalResize(state.terminal.cols, state.terminal.rows);
9562
+ }
9563
+ // Validate: if the fitted cols look suspiciously small relative to
9564
+ // the container width, schedule another attempt the font metrics
9565
+ // or layout may not have settled yet.
9566
+ var output = document.getElementById("output");
9567
+ if (output && state.terminal) {
9568
+ var containerW = output.getBoundingClientRect().width;
9569
+ var expectedMinCols = Math.floor(containerW / 20); // very conservative estimate
9570
+ if (state.terminal.cols < expectedMinCols && attempt < maxAttempts) {
9571
+ requestAnimationFrame(tryFit);
9572
+ return;
9372
9573
  }
9373
9574
  }
9374
9575
  } else if (attempt < maxAttempts) {
@@ -9401,27 +9602,7 @@
9401
9602
  maybeScrollTerminalToBottom("resize");
9402
9603
  }
9403
9604
 
9404
- var nextSize = {
9405
- cols: state.terminal.cols,
9406
- rows: state.terminal.rows
9407
- };
9408
-
9409
- if (!state.selectedId) return;
9410
-
9411
- // Skip resize for structured sessions (no PTY)
9412
- var resizeSess = state.sessions.find(function(s) { return s.id === state.selectedId; });
9413
- if (isStructuredSession(resizeSess)) return;
9414
-
9415
- // Only send resize API call if dimensions actually changed
9416
- if (state.lastResize.cols !== nextSize.cols || state.lastResize.rows !== nextSize.rows) {
9417
- state.lastResize = nextSize;
9418
- fetch("/api/sessions/" + state.selectedId + "/resize", {
9419
- method: "POST",
9420
- headers: { "Content-Type": "application/json" },
9421
- credentials: "same-origin",
9422
- body: JSON.stringify(nextSize)
9423
- }).catch(function() {});
9424
- }
9605
+ sendTerminalResize(state.terminal.cols, state.terminal.rows);
9425
9606
  }
9426
9607
 
9427
9608
  function startPolling() {
@@ -9464,6 +9645,12 @@
9464
9645
  subscribeToSession(state.selectedId);
9465
9646
  // Flush pending messages after reconnection
9466
9647
  flushPendingMessages();
9648
+ // Re-fit terminal on reconnect — the viewport may have changed
9649
+ // while disconnected, and the PTY needs up-to-date dimensions.
9650
+ if (state.terminal && state.fitAddon) {
9651
+ state.terminalViewportSize = { width: 0, height: 0 };
9652
+ ensureTerminalFit();
9653
+ }
9467
9654
  };
9468
9655
 
9469
9656
  ws.onmessage = function(event) {
@@ -10317,7 +10504,7 @@
10317
10504
  // Only expand the single newest tool card (first chat-message = newest due to column-reverse)
10318
10505
  var firstMsg = chatMessages.querySelector(".chat-message:not(.system-info)");
10319
10506
  if (firstMsg) {
10320
- var cards = firstMsg.querySelectorAll(".tool-use-card");
10507
+ var cards = firstMsg.querySelectorAll(".tool-use-card, .inline-diff[data-expand-key]");
10321
10508
  if (cards.length > 0) {
10322
10509
  var firstCard = cards[0];
10323
10510
  var firstCardKey = getElementExpandKey(firstCard);
@@ -10327,6 +10514,8 @@
10327
10514
  for (var ci = 1; ci < cards.length; ci++) {
10328
10515
  var cardKey = getElementExpandKey(cards[ci]);
10329
10516
  if (getPersistedExpandState(cardKey) === null) {
10517
+ // Never collapse unanswered AskUserQuestion cards
10518
+ if (cards[ci].classList.contains("ask-user") && !cards[ci].classList.contains("ask-user-answered")) continue;
10330
10519
  cards[ci].classList.add("collapsed");
10331
10520
  }
10332
10521
  }
@@ -10342,10 +10531,13 @@
10342
10531
  // Collapse all tool-use cards except those in the new message elements (marked with animate-in)
10343
10532
  // newEls: NodeList/Array of newly added message elements, or null to keep only the first card expanded
10344
10533
  function collapseOldToolCards(container, newEls) {
10345
- var allCards = container.querySelectorAll(".tool-use-card");
10534
+ var allCards = container.querySelectorAll(".tool-use-card, .inline-diff[data-expand-key]");
10346
10535
  allCards.forEach(function(c) {
10347
10536
  var cardKey = getElementExpandKey(c);
10348
10537
  if (getPersistedExpandState(cardKey) !== null) return;
10538
+ // Never collapse unanswered AskUserQuestion cards — the user
10539
+ // needs to interact with the options.
10540
+ if (c.classList.contains("ask-user") && !c.classList.contains("ask-user-answered")) return;
10349
10541
  // Keep expanded if this card is inside a newly added message
10350
10542
  if (newEls) {
10351
10543
  for (var i = 0; i < newEls.length; i++) {
@@ -10465,11 +10657,13 @@
10465
10657
  smartScrollToBottom(chatMessages);
10466
10658
  });
10467
10659
  var newestMsgEl = chatMessages.querySelector(".chat-message");
10468
- var allCards = chatMessages.querySelectorAll(".tool-use-card");
10660
+ var allCards = chatMessages.querySelectorAll(".tool-use-card, .inline-diff[data-expand-key]");
10469
10661
  var newestCard = null;
10470
10662
  allCards.forEach(function(c) {
10471
10663
  var cardKey = getElementExpandKey(c);
10472
10664
  if (getPersistedExpandState(cardKey) !== null) return;
10665
+ // Never collapse unanswered AskUserQuestion cards
10666
+ if (c.classList.contains("ask-user") && !c.classList.contains("ask-user-answered")) return;
10473
10667
  if (newestMsgEl && newestMsgEl.contains(c)) {
10474
10668
  if (!newestCard) newestCard = c;
10475
10669
  else c.classList.add("collapsed");
@@ -12029,10 +12223,11 @@
12029
12223
  return "";
12030
12224
  }
12031
12225
 
12032
- function renderDiffTool(block, toolResult, toolName) {
12226
+ function renderDiffTool(block, toolResult, toolName, messageKey, index) {
12033
12227
  var inputData = block.input || {};
12034
12228
  var path = inputData.file_path || inputData.path || "";
12035
12229
  var fileName = path.split("/").pop() || path;
12230
+ var toolId = block.id || "tool-" + toolName + "-" + (typeof index === "number" ? index : 0);
12036
12231
 
12037
12232
  var oldStr = inputData.old_string || "";
12038
12233
  var newStr = inputData.new_string || inputData.content || "";
@@ -12077,16 +12272,26 @@
12077
12272
  statusText = "执行中";
12078
12273
  }
12079
12274
 
12275
+ // Expand state: respect cardDefaults.editCards and persisted state
12276
+ var expandKey = buildExpandKey("diff", [messageKey, toolId || index, index]);
12277
+ var persistedExpanded = getPersistedExpandState(expandKey);
12278
+ var cardDefaultExpand = !!(state.config && state.config.cardDefaults && state.config.cardDefaults.editCards);
12279
+ var shouldExpand = persistedExpanded === null ? (statusClass === "diff-pending" || cardDefaultExpand) : persistedExpanded;
12280
+ var collapsedClass = shouldExpand ? "" : " collapsed";
12281
+
12080
12282
  // If only one column has content, show full width
12081
12283
  var bothCols = leftCol && rightCol;
12082
12284
  var colClass = bothCols ? "diff-col-half" : "diff-col-full";
12083
12285
 
12084
- return '<div class="inline-diff" data-tool-name="' + escapeHtml(toolName) + '">' +
12085
- '<div class="diff-header">' +
12286
+ return '<div class="inline-diff' + collapsedClass + '" data-tool-name="' + escapeHtml(toolName) + '"' +
12287
+ ' data-expand-kind="diff" data-expand-key="' + escapeHtml(expandKey) + '"' +
12288
+ ' data-tool-use-id="' + escapeHtml(toolId) + '">' +
12289
+ '<div class="diff-header" onclick="__tcToggle(event,this)">' +
12086
12290
  '<span class="diff-file-icon"></span>' +
12087
12291
  '<span class="diff-file-name">' + escapeHtml(fileName) + '</span>' +
12088
12292
  '<span class="diff-path">' + escapeHtml(path) + '</span>' +
12089
12293
  '<span class="diff-status ' + statusClass + '">' + statusText + '</span>' +
12294
+ '<span class="diff-toggle">▼</span>' +
12090
12295
  '</div>' +
12091
12296
  '<div class="diff-body">' +
12092
12297
  '<div class="diff-columns">' +
@@ -12120,7 +12325,7 @@
12120
12325
 
12121
12326
  // ── Diff-style: Edit, Write, MultiEdit
12122
12327
  if (toolName === "Edit" || toolName === "Write" || toolName === "MultiEdit") {
12123
- return renderDiffTool(block, toolResult, toolName);
12328
+ return renderDiffTool(block, toolResult, toolName, messageKey, index);
12124
12329
  }
12125
12330
 
12126
12331
  // ── AskUserQuestion tool — special card with batch submit
@@ -370,12 +370,20 @@
370
370
  transition: padding-left var(--transition-normal);
371
371
  }
372
372
  /* .sidebar-open class toggled for semantic purposes only; sidebar overlays without resizing main layout */
373
- .sidebar-open .input-panel {
373
+ .sidebar-open:not(.sidebar-pinned) .input-panel {
374
374
  opacity: 0;
375
375
  pointer-events: none;
376
376
  transition: opacity 0.2s ease;
377
377
  }
378
378
 
379
+ /* ===== 侧边栏常驻 ===== */
380
+ .main-layout.sidebar-pinned {
381
+ padding-left: var(--sidebar-width);
382
+ }
383
+ .main-layout.sidebar-pinned .floating-sidebar-toggle {
384
+ display: none;
385
+ }
386
+
379
387
  /* ===== 抽屉背景遮罩 ===== */
380
388
  .drawer-backdrop {
381
389
  position: fixed;
@@ -426,6 +434,27 @@
426
434
  opacity: 1;
427
435
  }
428
436
 
437
+ .sidebar.pinned {
438
+ transform: translateX(0);
439
+ pointer-events: auto;
440
+ opacity: 1;
441
+ box-shadow: none;
442
+ }
443
+
444
+ .sidebar.pinned .sidebar-close {
445
+ display: none;
446
+ }
447
+
448
+ /* ===== 图钉按钮 ===== */
449
+ .sidebar-pin-toggle {
450
+ flex-shrink: 0;
451
+ transition: transform var(--transition-fast), color var(--transition-fast);
452
+ }
453
+ .sidebar-pin-toggle.pinned {
454
+ color: var(--primary);
455
+ transform: rotate(45deg);
456
+ }
457
+
429
458
  /* ===== 侧边栏头部 ===== */
430
459
  .sidebar-header {
431
460
  display: flex;
@@ -2139,7 +2168,6 @@
2139
2168
  radial-gradient(circle at top right, rgba(91, 58, 34, 0.2), transparent 35%),
2140
2169
  radial-gradient(circle at bottom left, rgba(197, 101, 61, 0.08), transparent 40%),
2141
2170
  linear-gradient(180deg, #2a221c 0%, #1f1b17 50%, #1a1613 100%);
2142
- padding: 10px;
2143
2171
  overflow: hidden;
2144
2172
  min-height: 0;
2145
2173
  min-width: 0;
@@ -2165,8 +2193,6 @@
2165
2193
  left: 0;
2166
2194
  right: 0;
2167
2195
  bottom: 0;
2168
- width: 100%;
2169
- height: 100%;
2170
2196
  padding: 0;
2171
2197
  overflow: hidden;
2172
2198
  }
@@ -4084,6 +4110,26 @@
4084
4110
  padding: 6px 10px;
4085
4111
  background: rgba(0, 0, 0, 0.03);
4086
4112
  font-size: 0.75rem;
4113
+ cursor: pointer;
4114
+ }
4115
+ .diff-header:hover {
4116
+ background: rgba(0, 0, 0, 0.05);
4117
+ }
4118
+ .diff-toggle {
4119
+ font-size: 0.625rem;
4120
+ color: var(--text-muted);
4121
+ transition: transform 0.3s var(--ease-spring);
4122
+ flex-shrink: 0;
4123
+ margin-left: auto;
4124
+ }
4125
+ .inline-diff.collapsed .diff-toggle {
4126
+ transform: rotate(-90deg);
4127
+ }
4128
+ .inline-diff.collapsed .diff-body {
4129
+ max-height: 0;
4130
+ overflow: hidden;
4131
+ opacity: 0;
4132
+ transition: max-height 0.35s var(--ease-out-expo), opacity 0.25s ease;
4087
4133
  }
4088
4134
  .diff-file-icon {
4089
4135
  display: none;
@@ -6068,6 +6114,18 @@
6068
6114
 
6069
6115
  /* 平板适配 */
6070
6116
  @media (max-width: 768px) {
6117
+ .sidebar-pin-toggle { display: none; }
6118
+ .sidebar.pinned:not(.open) {
6119
+ transform: translateX(-100%);
6120
+ pointer-events: none;
6121
+ opacity: 0;
6122
+ }
6123
+ .main-layout.sidebar-pinned {
6124
+ padding-left: 0;
6125
+ }
6126
+ .main-layout.sidebar-pinned .floating-sidebar-toggle {
6127
+ display: inline-flex;
6128
+ }
6071
6129
  .app-container {
6072
6130
  --layout-main-file-panel-width: 0px;
6073
6131
  }
@@ -7473,6 +7531,93 @@
7473
7531
  cursor: pointer;
7474
7532
  }
7475
7533
 
7534
+ /* Switch card list */
7535
+ .switch-card-list {
7536
+ display: flex;
7537
+ flex-direction: column;
7538
+ gap: 8px;
7539
+ }
7540
+ .switch-card {
7541
+ display: block;
7542
+ background: var(--bg-secondary);
7543
+ border: 1px solid var(--border);
7544
+ border-radius: 10px;
7545
+ padding: 12px 14px;
7546
+ cursor: pointer;
7547
+ transition: border-color 0.2s, background 0.2s;
7548
+ user-select: none;
7549
+ }
7550
+ .switch-card:hover {
7551
+ border-color: var(--text-muted);
7552
+ }
7553
+ .switch-card-header {
7554
+ display: flex;
7555
+ align-items: center;
7556
+ gap: 10px;
7557
+ }
7558
+ .switch-card-title {
7559
+ flex: 1;
7560
+ font-size: 0.8125rem;
7561
+ font-weight: 600;
7562
+ color: var(--text-primary);
7563
+ }
7564
+ .switch-card-desc {
7565
+ font-size: 0.75rem;
7566
+ color: var(--text-muted);
7567
+ margin-top: 0;
7568
+ max-height: 0;
7569
+ overflow: hidden;
7570
+ opacity: 0;
7571
+ transition: max-height 0.25s ease, opacity 0.2s ease, margin-top 0.25s ease;
7572
+ }
7573
+ /* When switch is ON, expand the description */
7574
+ .switch-card:has(.switch-toggle:checked) .switch-card-desc {
7575
+ max-height: 40px;
7576
+ opacity: 1;
7577
+ margin-top: 8px;
7578
+ }
7579
+ .switch-card:has(.switch-toggle:checked) {
7580
+ border-color: var(--accent);
7581
+ background: color-mix(in srgb, var(--accent) 6%, var(--bg-secondary));
7582
+ }
7583
+
7584
+ /* Switch toggle (iOS style) */
7585
+ .switch-toggle {
7586
+ position: absolute;
7587
+ opacity: 0;
7588
+ width: 0;
7589
+ height: 0;
7590
+ pointer-events: none;
7591
+ }
7592
+ .switch-slider {
7593
+ position: relative;
7594
+ display: inline-block;
7595
+ width: 40px;
7596
+ height: 22px;
7597
+ min-width: 40px;
7598
+ background: #c4b8a8;
7599
+ border-radius: 11px;
7600
+ transition: background 0.25s;
7601
+ }
7602
+ .switch-slider::after {
7603
+ content: "";
7604
+ position: absolute;
7605
+ top: 3px;
7606
+ left: 3px;
7607
+ width: 16px;
7608
+ height: 16px;
7609
+ background: #fff;
7610
+ border-radius: 50%;
7611
+ transition: transform 0.25s;
7612
+ box-shadow: 0 1px 3px rgba(0,0,0,0.2);
7613
+ }
7614
+ .switch-toggle:checked + .switch-slider {
7615
+ background: var(--accent);
7616
+ }
7617
+ .switch-toggle:checked + .switch-slider::after {
7618
+ transform: translateX(18px);
7619
+ }
7620
+
7476
7621
  .field-file {
7477
7622
  font-size: 0.8rem;
7478
7623
  padding: 6px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@co0ontty/wand",
3
- "version": "1.14.2",
3
+ "version": "1.14.6",
4
4
  "description": "A web terminal for local CLI tools like Claude.",
5
5
  "type": "module",
6
6
  "bin": {