@co0ontty/wand 1.14.3 → 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">' +
@@ -2774,11 +2797,12 @@
2774
2797
  }
2775
2798
 
2776
2799
  window.__tcToggle = function(e, headerEl) {
2777
- var card = headerEl.closest(".tool-use-card");
2800
+ var card = headerEl.closest(".tool-use-card") || headerEl.closest(".inline-diff");
2778
2801
  if (card) {
2779
2802
  var wasCollapsed = card.classList.contains("collapsed");
2780
2803
  card.classList.toggle("collapsed");
2781
- persistElementExpandState(card, "tool-card");
2804
+ var expandKind = card.dataset.expandKind || "tool-card";
2805
+ persistElementExpandState(card, expandKind);
2782
2806
  // Lazy-load truncated content on expand
2783
2807
  if (wasCollapsed && card.dataset.truncated === "true" && card.dataset.loaded !== "true") {
2784
2808
  var toolUseId = card.dataset.toolUseId;
@@ -3130,6 +3154,8 @@
3130
3154
  if (drawerBackdrop) drawerBackdrop.addEventListener("click", closeSessionsDrawer);
3131
3155
  var closeDrawerBtn = document.getElementById("close-drawer-button");
3132
3156
  if (closeDrawerBtn) closeDrawerBtn.addEventListener("click", closeSessionsDrawer);
3157
+ var pinBtn = document.getElementById("sidebar-pin-btn");
3158
+ if (pinBtn) pinBtn.addEventListener("click", toggleSidebarPin);
3133
3159
  var homeBtn = document.getElementById("sidebar-home-btn");
3134
3160
  if (homeBtn) homeBtn.addEventListener("click", function() {
3135
3161
  state.selectedId = null;
@@ -3237,6 +3263,35 @@
3237
3263
  var notifTestBtn = document.getElementById("notification-test-btn");
3238
3264
  if (notifTestBtn) notifTestBtn.addEventListener("click", testNotification);
3239
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
+ }
3240
3295
  var newSessBtn = document.getElementById("topbar-new-session-button");
3241
3296
  if (newSessBtn) newSessBtn.addEventListener("click", openSessionModal);
3242
3297
  var drawerNewSessBtn = document.getElementById("drawer-new-session-button");
@@ -3803,7 +3858,9 @@
3803
3858
  } else {
3804
3859
  selectSession(sessionId);
3805
3860
  }
3806
- closeSessionsDrawer();
3861
+ if (!state.sidebarPinned || isMobileLayout()) {
3862
+ closeSessionsDrawer();
3863
+ }
3807
3864
  }
3808
3865
 
3809
3866
  function handleSessionItemClick(event) {
@@ -4308,6 +4365,12 @@
4308
4365
  } else {
4309
4366
  updateTerminalJumpToBottomButton();
4310
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
+ }
4311
4374
  return wrote || sessionChanged;
4312
4375
  }
4313
4376
 
@@ -4382,6 +4445,26 @@
4382
4445
  state.fitAddon = fitAddonConstructor ? new fitAddonConstructor() : null;
4383
4446
  if (state.fitAddon) {
4384
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
+ };
4385
4468
  } else {
4386
4469
  console.error("[wand] xterm fit addon failed to load; continuing without fit support.");
4387
4470
  }
@@ -4400,6 +4483,24 @@
4400
4483
  // Retry-based fit: wait for browser to complete layout before measuring and fitting
4401
4484
  if (state.fitAddon) {
4402
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);
4403
4504
  }
4404
4505
 
4405
4506
  var viewport = getTerminalViewport();
@@ -5170,6 +5271,22 @@
5170
5271
  subscribeToSession(id);
5171
5272
  }
5172
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
+
5173
5290
  function updateDrawerState() {
5174
5291
  var drawer = document.getElementById("sessions-drawer");
5175
5292
  var backdrop = document.getElementById("sessions-drawer-backdrop");
@@ -5187,9 +5304,11 @@
5187
5304
  if (toggleBtn) {
5188
5305
  toggleBtn.classList.toggle("active", state.sessionsDrawerOpen);
5189
5306
  }
5307
+ updatePinState();
5190
5308
  }
5191
5309
 
5192
5310
  function toggleSessionsDrawer() {
5311
+ if (state.sidebarPinned && !isMobileLayout()) return;
5193
5312
  state.sessionsDrawerOpen = !state.sessionsDrawerOpen;
5194
5313
  if (state.sessionsDrawerOpen && isMobileLayout()) {
5195
5314
  state.filePanelOpen = false;
@@ -5201,12 +5320,38 @@
5201
5320
  }
5202
5321
 
5203
5322
  function closeSessionsDrawer() {
5323
+ if (state.sidebarPinned && !isMobileLayout()) return;
5204
5324
  if (!state.sessionsDrawerOpen) return;
5205
5325
  closeSwipedItem();
5206
5326
  state.sessionsDrawerOpen = false;
5207
5327
  updateLayoutState();
5208
5328
  }
5209
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
+
5210
5355
  // Store last focused element for focus trap
5211
5356
  var lastFocusedElement = null;
5212
5357
  var focusTrapHandler = null;
@@ -5472,6 +5617,13 @@
5472
5617
  if (typeof WandNative !== "undefined" && typeof WandNative.getAppIcon === "function") {
5473
5618
  try { _updateAppIconSelection(WandNative.getAppIcon() || "shorthair"); } catch (_e) {}
5474
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
+ }
5475
5627
  }
5476
5628
  }
5477
5629
 
@@ -9295,7 +9447,22 @@
9295
9447
  state.resizeObserver = new ResizeObserver(function() { scheduleTerminalResize(true); });
9296
9448
  state.resizeObserver.observe(output);
9297
9449
  }
9298
- 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
+ };
9299
9466
  window.addEventListener("resize", state.resizeHandler);
9300
9467
  // Also listen to visualViewport resize for pinch-zoom / browser zoom
9301
9468
  if (window.visualViewport) {
@@ -9365,8 +9532,24 @@
9365
9532
  updateTerminalJumpToBottomButton();
9366
9533
  }
9367
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
+
9368
9551
  function ensureTerminalFit() {
9369
- var maxAttempts = 10;
9552
+ var maxAttempts = 20;
9370
9553
  var attempt = 0;
9371
9554
  function tryFit() {
9372
9555
  attempt++;
@@ -9374,19 +9557,19 @@
9374
9557
  if (shouldResizeTerminalViewport() && state.fitAddon) {
9375
9558
  state.fitAddon.fit();
9376
9559
  maybeScrollTerminalToBottom("resize");
9377
- if (state.selectedId && state.terminal) {
9378
- var selectedSess = state.sessions.find(function(s) { return s.id === state.selectedId; });
9379
- if (!isStructuredSession(selectedSess)) {
9380
- var nextSize = { cols: state.terminal.cols, rows: state.terminal.rows };
9381
- if (state.lastResize.cols !== nextSize.cols || state.lastResize.rows !== nextSize.rows) {
9382
- state.lastResize = nextSize;
9383
- fetch("/api/sessions/" + state.selectedId + "/resize", {
9384
- method: "POST",
9385
- headers: { "Content-Type": "application/json" },
9386
- credentials: "same-origin",
9387
- body: JSON.stringify(nextSize)
9388
- }).catch(function() {});
9389
- }
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;
9390
9573
  }
9391
9574
  }
9392
9575
  } else if (attempt < maxAttempts) {
@@ -9419,27 +9602,7 @@
9419
9602
  maybeScrollTerminalToBottom("resize");
9420
9603
  }
9421
9604
 
9422
- var nextSize = {
9423
- cols: state.terminal.cols,
9424
- rows: state.terminal.rows
9425
- };
9426
-
9427
- if (!state.selectedId) return;
9428
-
9429
- // Skip resize for structured sessions (no PTY)
9430
- var resizeSess = state.sessions.find(function(s) { return s.id === state.selectedId; });
9431
- if (isStructuredSession(resizeSess)) return;
9432
-
9433
- // Only send resize API call if dimensions actually changed
9434
- if (state.lastResize.cols !== nextSize.cols || state.lastResize.rows !== nextSize.rows) {
9435
- state.lastResize = nextSize;
9436
- fetch("/api/sessions/" + state.selectedId + "/resize", {
9437
- method: "POST",
9438
- headers: { "Content-Type": "application/json" },
9439
- credentials: "same-origin",
9440
- body: JSON.stringify(nextSize)
9441
- }).catch(function() {});
9442
- }
9605
+ sendTerminalResize(state.terminal.cols, state.terminal.rows);
9443
9606
  }
9444
9607
 
9445
9608
  function startPolling() {
@@ -9482,6 +9645,12 @@
9482
9645
  subscribeToSession(state.selectedId);
9483
9646
  // Flush pending messages after reconnection
9484
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
+ }
9485
9654
  };
9486
9655
 
9487
9656
  ws.onmessage = function(event) {
@@ -10335,7 +10504,7 @@
10335
10504
  // Only expand the single newest tool card (first chat-message = newest due to column-reverse)
10336
10505
  var firstMsg = chatMessages.querySelector(".chat-message:not(.system-info)");
10337
10506
  if (firstMsg) {
10338
- var cards = firstMsg.querySelectorAll(".tool-use-card");
10507
+ var cards = firstMsg.querySelectorAll(".tool-use-card, .inline-diff[data-expand-key]");
10339
10508
  if (cards.length > 0) {
10340
10509
  var firstCard = cards[0];
10341
10510
  var firstCardKey = getElementExpandKey(firstCard);
@@ -10345,6 +10514,8 @@
10345
10514
  for (var ci = 1; ci < cards.length; ci++) {
10346
10515
  var cardKey = getElementExpandKey(cards[ci]);
10347
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;
10348
10519
  cards[ci].classList.add("collapsed");
10349
10520
  }
10350
10521
  }
@@ -10360,10 +10531,13 @@
10360
10531
  // Collapse all tool-use cards except those in the new message elements (marked with animate-in)
10361
10532
  // newEls: NodeList/Array of newly added message elements, or null to keep only the first card expanded
10362
10533
  function collapseOldToolCards(container, newEls) {
10363
- var allCards = container.querySelectorAll(".tool-use-card");
10534
+ var allCards = container.querySelectorAll(".tool-use-card, .inline-diff[data-expand-key]");
10364
10535
  allCards.forEach(function(c) {
10365
10536
  var cardKey = getElementExpandKey(c);
10366
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;
10367
10541
  // Keep expanded if this card is inside a newly added message
10368
10542
  if (newEls) {
10369
10543
  for (var i = 0; i < newEls.length; i++) {
@@ -10483,11 +10657,13 @@
10483
10657
  smartScrollToBottom(chatMessages);
10484
10658
  });
10485
10659
  var newestMsgEl = chatMessages.querySelector(".chat-message");
10486
- var allCards = chatMessages.querySelectorAll(".tool-use-card");
10660
+ var allCards = chatMessages.querySelectorAll(".tool-use-card, .inline-diff[data-expand-key]");
10487
10661
  var newestCard = null;
10488
10662
  allCards.forEach(function(c) {
10489
10663
  var cardKey = getElementExpandKey(c);
10490
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;
10491
10667
  if (newestMsgEl && newestMsgEl.contains(c)) {
10492
10668
  if (!newestCard) newestCard = c;
10493
10669
  else c.classList.add("collapsed");
@@ -12047,10 +12223,11 @@
12047
12223
  return "";
12048
12224
  }
12049
12225
 
12050
- function renderDiffTool(block, toolResult, toolName) {
12226
+ function renderDiffTool(block, toolResult, toolName, messageKey, index) {
12051
12227
  var inputData = block.input || {};
12052
12228
  var path = inputData.file_path || inputData.path || "";
12053
12229
  var fileName = path.split("/").pop() || path;
12230
+ var toolId = block.id || "tool-" + toolName + "-" + (typeof index === "number" ? index : 0);
12054
12231
 
12055
12232
  var oldStr = inputData.old_string || "";
12056
12233
  var newStr = inputData.new_string || inputData.content || "";
@@ -12095,16 +12272,26 @@
12095
12272
  statusText = "执行中";
12096
12273
  }
12097
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
+
12098
12282
  // If only one column has content, show full width
12099
12283
  var bothCols = leftCol && rightCol;
12100
12284
  var colClass = bothCols ? "diff-col-half" : "diff-col-full";
12101
12285
 
12102
- return '<div class="inline-diff" data-tool-name="' + escapeHtml(toolName) + '">' +
12103
- '<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)">' +
12104
12290
  '<span class="diff-file-icon"></span>' +
12105
12291
  '<span class="diff-file-name">' + escapeHtml(fileName) + '</span>' +
12106
12292
  '<span class="diff-path">' + escapeHtml(path) + '</span>' +
12107
12293
  '<span class="diff-status ' + statusClass + '">' + statusText + '</span>' +
12294
+ '<span class="diff-toggle">▼</span>' +
12108
12295
  '</div>' +
12109
12296
  '<div class="diff-body">' +
12110
12297
  '<div class="diff-columns">' +
@@ -12138,7 +12325,7 @@
12138
12325
 
12139
12326
  // ── Diff-style: Edit, Write, MultiEdit
12140
12327
  if (toolName === "Edit" || toolName === "Write" || toolName === "MultiEdit") {
12141
- return renderDiffTool(block, toolResult, toolName);
12328
+ return renderDiffTool(block, toolResult, toolName, messageKey, index);
12142
12329
  }
12143
12330
 
12144
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@co0ontty/wand",
3
- "version": "1.14.3",
3
+ "version": "1.14.6",
4
4
  "description": "A web terminal for local CLI tools like Claude.",
5
5
  "type": "module",
6
6
  "bin": {