@co0ontty/wand 1.3.3 → 1.3.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.
@@ -173,6 +173,12 @@
173
173
  return (state.config && state.config.defaultCwd) || "/tmp";
174
174
  }
175
175
 
176
+ function resetChatRenderCache() {
177
+ state.lastRenderedHash = 0;
178
+ state.lastRenderedMsgCount = 0;
179
+ state.lastRenderedEmpty = null;
180
+ }
181
+
176
182
  function getEffectiveCwd() {
177
183
  return state.workingDir || getConfigCwd();
178
184
  }
@@ -324,9 +330,7 @@
324
330
 
325
331
  app.innerHTML = isLoggedIn ? renderAppShell() : renderLogin();
326
332
  // Reset chat render tracking since DOM was fully replaced
327
- state.lastRenderedHash = 0;
328
- state.lastRenderedMsgCount = 0;
329
- state.lastRenderedEmpty = null;
333
+ resetChatRenderCache();
330
334
  attachEventListeners();
331
335
  updateDrawerState();
332
336
  syncComposerModeSelect();
@@ -367,6 +371,26 @@
367
371
  '<button class="shortcut-key" data-key="escape" type="button">Esc</button>';
368
372
  }
369
373
 
374
+ function renderApprovalStatsBadge() {
375
+ var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
376
+ var stats = selectedSession && selectedSession.approvalStats;
377
+ if (!stats || stats.total === 0) return '<span class="approval-stats hidden" id="approval-stats"></span>';
378
+ return '<span class="approval-stats" id="approval-stats">' +
379
+ '<span class="approval-stats-divider"></span>' +
380
+ '<span class="approval-stats-badge" id="approval-stats-badge" title="本次会话自动批准统计">' +
381
+ '<svg class="approval-stats-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>' +
382
+ '<span class="approval-stats-total">' + stats.total + '</span>' +
383
+ '</span>' +
384
+ '<span class="approval-stats-popup" id="approval-stats-popup">' +
385
+ '<span class="approval-stats-popup-title">自动批准统计</span>' +
386
+ (stats.command > 0 ? '<span class="approval-stats-row"><span class="approval-stats-row-icon">⚡</span><span class="approval-stats-row-label">命令执行</span><span class="approval-stats-row-count">' + stats.command + '</span></span>' : '') +
387
+ (stats.file > 0 ? '<span class="approval-stats-row"><span class="approval-stats-row-icon">📝</span><span class="approval-stats-row-label">文件写入</span><span class="approval-stats-row-count">' + stats.file + '</span></span>' : '') +
388
+ (stats.tool > 0 ? '<span class="approval-stats-row"><span class="approval-stats-row-icon">🔧</span><span class="approval-stats-row-label">其他工具</span><span class="approval-stats-row-count">' + stats.tool + '</span></span>' : '') +
389
+ '<span class="approval-stats-row approval-stats-row-total"><span class="approval-stats-row-icon">∑</span><span class="approval-stats-row-label">合计</span><span class="approval-stats-row-count">' + stats.total + '</span></span>' +
390
+ '</span>' +
391
+ '</span>';
392
+ }
393
+
370
394
  function renderInlineKeyboard() {
371
395
  if (!state.selectedId) return "";
372
396
  var isTerminal = state.currentView === "terminal";
@@ -462,6 +486,12 @@
462
486
  '<span class="session-count" id="session-count">' + String(state.sessions.length) + '</span>' +
463
487
  '</div>' +
464
488
  '<div class="sidebar-header-actions">' +
489
+ '<button id="sidebar-home-btn" class="btn btn-ghost btn-sm" type="button" title="回到首页">' +
490
+ '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>' +
491
+ '</button>' +
492
+ '<button id="sidebar-refresh-btn" class="btn btn-ghost btn-sm" type="button" title="刷新页面">' +
493
+ '<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>' +
494
+ '</button>' +
465
495
  '<button id="close-drawer-button" class="btn btn-ghost btn-sm sidebar-close" type="button" aria-label="关闭菜单">×</button>' +
466
496
  '</div>' +
467
497
  '</div>' +
@@ -569,6 +599,7 @@
569
599
  '<button id="approve-permission-btn" class="btn btn-permission btn-permission-approve" type="button">批准</button>' +
570
600
  '<button id="deny-permission-btn" class="btn btn-permission btn-permission-deny" type="button">拒绝</button>' +
571
601
  '</span>' +
602
+ renderApprovalStatsBadge() +
572
603
  '</div>' +
573
604
  '<div class="input-composer-right">' +
574
605
  '<span id="queue-counter" class="queue-counter hidden">队列: 0</span>' +
@@ -1904,6 +1935,18 @@
1904
1935
  if (drawerBackdrop) drawerBackdrop.addEventListener("click", closeSessionsDrawer);
1905
1936
  var closeDrawerBtn = document.getElementById("close-drawer-button");
1906
1937
  if (closeDrawerBtn) closeDrawerBtn.addEventListener("click", closeSessionsDrawer);
1938
+ var homeBtn = document.getElementById("sidebar-home-btn");
1939
+ if (homeBtn) homeBtn.addEventListener("click", function() {
1940
+ state.selectedId = null;
1941
+ persistSelectedId();
1942
+ resetChatRenderCache();
1943
+ closeSessionsDrawer();
1944
+ renderApp();
1945
+ });
1946
+ var refreshBtn = document.getElementById("sidebar-refresh-btn");
1947
+ if (refreshBtn) refreshBtn.addEventListener("click", function() {
1948
+ window.location.reload();
1949
+ });
1907
1950
  var logoutBtn = document.getElementById("logout-button");
1908
1951
  if (logoutBtn) logoutBtn.addEventListener("click", logout);
1909
1952
  var settingsBtn = document.getElementById("settings-button");
@@ -3561,6 +3604,15 @@
3561
3604
  return fetch("/api/sessions/" + id, { credentials: "same-origin" })
3562
3605
  .then(function(res) { return res.json(); })
3563
3606
  .then(function(data) {
3607
+ if (data.error) {
3608
+ // Session no longer exists — deselect and refresh list
3609
+ if (state.selectedId === id) {
3610
+ state.selectedId = null;
3611
+ persistSelectedId();
3612
+ }
3613
+ loadSessions();
3614
+ return;
3615
+ }
3564
3616
  updateSessionSnapshot(data);
3565
3617
  updateShellChrome();
3566
3618
 
@@ -3578,9 +3630,7 @@
3578
3630
  function selectSession(id) {
3579
3631
  state.selectedId = id;
3580
3632
  persistSelectedId();
3581
- state.lastRenderedHash = 0;
3582
- state.lastRenderedMsgCount = 0;
3583
- state.lastRenderedEmpty = null;
3633
+ resetChatRenderCache();
3584
3634
  state.currentMessages = [];
3585
3635
  if (chatRenderTimer) { clearTimeout(chatRenderTimer); chatRenderTimer = null; }
3586
3636
  // Reset todo progress bar
@@ -4167,9 +4217,7 @@
4167
4217
  state.selectedId = data.id;
4168
4218
  persistSelectedId();
4169
4219
  state.drafts[data.id] = "";
4170
- state.lastRenderedHash = 0;
4171
- state.lastRenderedMsgCount = 0;
4172
- state.lastRenderedEmpty = null;
4220
+ resetChatRenderCache();
4173
4221
  return refreshAll();
4174
4222
  })
4175
4223
  .then(function() { focusInputBox(true); })
@@ -4212,9 +4260,7 @@
4212
4260
  state.selectedId = data.id;
4213
4261
  persistSelectedId();
4214
4262
  state.drafts[data.id] = "";
4215
- state.lastRenderedHash = 0;
4216
- state.lastRenderedMsgCount = 0;
4217
- state.lastRenderedEmpty = null;
4263
+ resetChatRenderCache();
4218
4264
  closeSessionModal();
4219
4265
  closeSessionsDrawer();
4220
4266
  return refreshAll();
@@ -4644,9 +4690,7 @@
4644
4690
  state.selectedId = data.id;
4645
4691
  persistSelectedId();
4646
4692
  state.drafts[data.id] = "";
4647
- state.lastRenderedHash = 0;
4648
- state.lastRenderedMsgCount = 0;
4649
- state.lastRenderedEmpty = null;
4693
+ resetChatRenderCache();
4650
4694
  switchToSessionView(data.id);
4651
4695
  updateSessionSnapshot(data);
4652
4696
  updateSessionsList();
@@ -4702,9 +4746,7 @@
4702
4746
  state.selectedId = data.id;
4703
4747
  persistSelectedId();
4704
4748
  state.drafts[data.id] = "";
4705
- state.lastRenderedHash = 0;
4706
- state.lastRenderedMsgCount = 0;
4707
- state.lastRenderedEmpty = null;
4749
+ resetChatRenderCache();
4708
4750
  if (inputBox) inputBox.value = "";
4709
4751
  if (welcomeInput) welcomeInput.value = "";
4710
4752
  switchToSessionView(data.id);
@@ -5592,9 +5634,7 @@
5592
5634
 
5593
5635
  function activateSession(data) {
5594
5636
  if (!data || !data.id) return Promise.resolve();
5595
- state.lastRenderedHash = 0;
5596
- state.lastRenderedMsgCount = 0;
5597
- state.lastRenderedEmpty = null;
5637
+ resetChatRenderCache();
5598
5638
  switchToSessionView(data.id);
5599
5639
  updateSessionSnapshot(data);
5600
5640
  updateSessionsList();
@@ -5825,11 +5865,26 @@
5825
5865
 
5826
5866
  function handleInputBoxBlur() {
5827
5867
  resetInputPanelViewportSpacing();
5828
- // Restore app container height when keyboard closes
5829
- var appContainer = document.querySelector('.app-container');
5830
- if (appContainer) {
5831
- appContainer.style.height = '';
5832
- }
5868
+ // Restore app container height when keyboard closes.
5869
+ // Use a short delay because on iOS the visualViewport may not
5870
+ // have updated yet at the moment blur fires.
5871
+ setTimeout(function() {
5872
+ var appContainer = document.querySelector('.app-container');
5873
+ if (appContainer) {
5874
+ // Only clear if keyboard is actually closed now
5875
+ var vv = window.visualViewport;
5876
+ if (vv) {
5877
+ var offsetBottom = window.innerHeight - vv.height - vv.offsetTop;
5878
+ if (offsetBottom <= 50) {
5879
+ appContainer.style.height = '';
5880
+ }
5881
+ } else {
5882
+ appContainer.style.height = '';
5883
+ }
5884
+ }
5885
+ // Scroll the window back to top to fix any residual offset
5886
+ window.scrollTo(0, 0);
5887
+ }, 100);
5833
5888
  }
5834
5889
 
5835
5890
  function adjustInputBoxSelection(inputBox) {
@@ -6542,13 +6597,16 @@
6542
6597
  var isKeyboardOpen = offsetBottom > 50;
6543
6598
  var heightChanged = Math.abs(vv.height - lastHeight) > 8;
6544
6599
 
6545
- // In PWA standalone mode, dynamically resize the app container
6546
- // because 100dvh does NOT shrink when keyboard appears in standalone PWA
6600
+ // Dynamically resize the app container to match visible viewport.
6601
+ // This is needed because 100dvh does NOT shrink when the keyboard
6602
+ // appears in PWA standalone mode, and on some browsers the layout
6603
+ // viewport doesn't update on keyboard dismiss without this.
6547
6604
  var appContainer = document.querySelector('.app-container');
6548
6605
  if (appContainer) {
6549
6606
  if (isKeyboardOpen) {
6550
6607
  appContainer.style.height = vv.height + 'px';
6551
- } else {
6608
+ } else if (keyboardOpen) {
6609
+ // Keyboard just closed — clear forced height
6552
6610
  appContainer.style.height = '';
6553
6611
  }
6554
6612
  }
@@ -6571,6 +6629,9 @@
6571
6629
  }
6572
6630
 
6573
6631
  vv.addEventListener('resize', debouncedUpdate);
6632
+ // Also listen to scroll — on iOS, keyboard dismiss sometimes only
6633
+ // fires a scroll event (viewport scrolls back) without a resize event.
6634
+ vv.addEventListener('scroll', debouncedUpdate);
6574
6635
 
6575
6636
  updateViewport();
6576
6637
  }
@@ -7026,9 +7087,15 @@
7026
7087
  if (msg.data.permissionBlocked === false) {
7027
7088
  statusUpdate.pendingEscalation = null;
7028
7089
  }
7090
+ if (msg.data.approvalStats) {
7091
+ statusUpdate.approvalStats = msg.data.approvalStats;
7092
+ }
7029
7093
  updateSessionSnapshot(statusUpdate);
7030
7094
  if (msg.sessionId === state.selectedId) {
7031
7095
  updateTaskDisplay();
7096
+ if (msg.data.approvalStats) {
7097
+ updateApprovalStats();
7098
+ }
7032
7099
  }
7033
7100
  }
7034
7101
  break;
@@ -7098,6 +7165,39 @@
7098
7165
  }
7099
7166
  }
7100
7167
 
7168
+ function updateApprovalStats() {
7169
+ var container = document.getElementById("approval-stats");
7170
+ if (!container) return;
7171
+ var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
7172
+ var stats = selectedSession && selectedSession.approvalStats;
7173
+ if (!stats || stats.total === 0) {
7174
+ container.className = "approval-stats hidden";
7175
+ container.innerHTML = "";
7176
+ return;
7177
+ }
7178
+ container.className = "approval-stats";
7179
+ container.innerHTML =
7180
+ '<span class="approval-stats-divider"></span>' +
7181
+ '<span class="approval-stats-badge" id="approval-stats-badge" title="本次会话自动批准统计">' +
7182
+ '<svg class="approval-stats-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>' +
7183
+ '<span class="approval-stats-total">' + stats.total + '</span>' +
7184
+ '</span>' +
7185
+ '<span class="approval-stats-popup" id="approval-stats-popup">' +
7186
+ '<span class="approval-stats-popup-title">自动批准统计</span>' +
7187
+ (stats.command > 0 ? '<span class="approval-stats-row"><span class="approval-stats-row-icon">⚡</span><span class="approval-stats-row-label">命令执行</span><span class="approval-stats-row-count">' + stats.command + '</span></span>' : '') +
7188
+ (stats.file > 0 ? '<span class="approval-stats-row"><span class="approval-stats-row-icon">📝</span><span class="approval-stats-row-label">文件写入</span><span class="approval-stats-row-count">' + stats.file + '</span></span>' : '') +
7189
+ (stats.tool > 0 ? '<span class="approval-stats-row"><span class="approval-stats-row-icon">🔧</span><span class="approval-stats-row-label">其他工具</span><span class="approval-stats-row-count">' + stats.tool + '</span></span>' : '') +
7190
+ '<span class="approval-stats-row approval-stats-row-total"><span class="approval-stats-row-icon">∑</span><span class="approval-stats-row-label">合计</span><span class="approval-stats-row-count">' + stats.total + '</span></span>' +
7191
+ '</span>';
7192
+ // Pulse animation on the badge
7193
+ var badge = container.querySelector(".approval-stats-badge");
7194
+ if (badge) {
7195
+ badge.classList.remove("approval-stats-pulse");
7196
+ void badge.offsetWidth;
7197
+ badge.classList.add("approval-stats-pulse");
7198
+ }
7199
+ }
7200
+
7101
7201
  function approvePermission() {
7102
7202
  if (!state.selectedId) return;
7103
7203
  var approveBtn = document.getElementById("approve-permission-btn");
@@ -7856,8 +7956,12 @@
7856
7956
  if (!state.terminalDomView || !state.serializeAddon) return;
7857
7957
 
7858
7958
  try {
7959
+ // Serialize the entire buffer including scrollback history
7960
+ var buf = state.terminal.buffer.active;
7961
+ var totalRows = buf.length;
7859
7962
  var html = state.serializeAddon.serializeAsHTML({
7860
- includeGlobalBackground: true
7963
+ includeGlobalBackground: true,
7964
+ range: { start: 0, end: totalRows }
7861
7965
  });
7862
7966
 
7863
7967
  // Extract the <pre>...</pre> portion
@@ -269,8 +269,9 @@
269
269
  }
270
270
 
271
271
  .floating-sidebar-toggle.active {
272
- background: rgba(197, 101, 61, 0.1);
273
- border-color: rgba(197, 101, 61, 0.2);
272
+ opacity: 0;
273
+ pointer-events: none;
274
+ transition: opacity 0.15s ease;
274
275
  }
275
276
 
276
277
  .floating-sidebar-toggle:active {
@@ -3429,6 +3430,114 @@
3429
3430
  to { opacity: 1; transform: translateX(0); }
3430
3431
  }
3431
3432
 
3433
+ /* Approval stats badge */
3434
+ .approval-stats {
3435
+ display: inline-flex;
3436
+ align-items: center;
3437
+ position: relative;
3438
+ }
3439
+ .approval-stats.hidden {
3440
+ display: none;
3441
+ }
3442
+ .approval-stats-divider {
3443
+ width: 1px;
3444
+ height: 16px;
3445
+ background: var(--border);
3446
+ margin: 0 4px 0 2px;
3447
+ }
3448
+ .approval-stats-badge {
3449
+ display: inline-flex;
3450
+ align-items: center;
3451
+ gap: 3px;
3452
+ padding: 1px 7px 1px 5px;
3453
+ border-radius: 10px;
3454
+ background: rgba(34, 197, 94, 0.1);
3455
+ color: #22c55e;
3456
+ font-size: 0.68rem;
3457
+ font-weight: 600;
3458
+ cursor: default;
3459
+ transition: background 0.15s;
3460
+ line-height: 1.5;
3461
+ }
3462
+ .approval-stats-badge:hover {
3463
+ background: rgba(34, 197, 94, 0.18);
3464
+ }
3465
+ .approval-stats-icon {
3466
+ stroke: #22c55e;
3467
+ flex-shrink: 0;
3468
+ }
3469
+ .approval-stats-total {
3470
+ font-variant-numeric: tabular-nums;
3471
+ }
3472
+ @keyframes approval-pulse {
3473
+ 0% { transform: scale(1); }
3474
+ 50% { transform: scale(1.15); }
3475
+ 100% { transform: scale(1); }
3476
+ }
3477
+ .approval-stats-pulse {
3478
+ animation: approval-pulse 0.3s ease-out;
3479
+ }
3480
+ /* Popup tooltip */
3481
+ .approval-stats-popup {
3482
+ display: none;
3483
+ position: absolute;
3484
+ bottom: calc(100% + 6px);
3485
+ left: 50%;
3486
+ transform: translateX(-50%);
3487
+ background: var(--bg-elevated, #1e1e2e);
3488
+ border: 1px solid var(--border);
3489
+ border-radius: 8px;
3490
+ padding: 8px 10px;
3491
+ min-width: 140px;
3492
+ box-shadow: 0 4px 16px rgba(0,0,0,0.25);
3493
+ z-index: 100;
3494
+ flex-direction: column;
3495
+ gap: 4px;
3496
+ }
3497
+ .approval-stats:hover .approval-stats-popup {
3498
+ display: flex;
3499
+ }
3500
+ .approval-stats-popup-title {
3501
+ font-size: 0.65rem;
3502
+ color: var(--text-muted);
3503
+ font-weight: 500;
3504
+ margin-bottom: 2px;
3505
+ white-space: nowrap;
3506
+ }
3507
+ .approval-stats-row {
3508
+ display: flex;
3509
+ align-items: center;
3510
+ gap: 6px;
3511
+ font-size: 0.7rem;
3512
+ color: var(--text-secondary, #ccc);
3513
+ white-space: nowrap;
3514
+ }
3515
+ .approval-stats-row-icon {
3516
+ width: 16px;
3517
+ text-align: center;
3518
+ flex-shrink: 0;
3519
+ font-size: 0.72rem;
3520
+ }
3521
+ .approval-stats-row-label {
3522
+ flex: 1;
3523
+ }
3524
+ .approval-stats-row-count {
3525
+ font-weight: 600;
3526
+ color: var(--text-primary, #eee);
3527
+ font-variant-numeric: tabular-nums;
3528
+ min-width: 20px;
3529
+ text-align: right;
3530
+ }
3531
+ .approval-stats-row-total {
3532
+ border-top: 1px solid var(--border);
3533
+ padding-top: 4px;
3534
+ margin-top: 2px;
3535
+ color: #22c55e;
3536
+ }
3537
+ .approval-stats-row-total .approval-stats-row-count {
3538
+ color: #22c55e;
3539
+ }
3540
+
3432
3541
  .composer-interactive-toggle {
3433
3542
  display: inline-flex;
3434
3543
  align-items: center;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@co0ontty/wand",
3
- "version": "1.3.3",
3
+ "version": "1.3.6",
4
4
  "description": "A web terminal for local CLI tools like Claude.",
5
5
  "type": "module",
6
6
  "bin": {