@co0ontty/wand 1.29.1 → 1.29.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.
package/dist/server.js CHANGED
@@ -976,6 +976,10 @@ export async function startServer(config, configPath) {
976
976
  res.setHeader("Content-Disposition", `attachment; filename="${encodeURIComponent(macosDmg.fileName)}"`);
977
977
  createReadStream(macosDmg.filePath).pipe(res);
978
978
  });
979
+ // Public probe so the unauthenticated browser does not log a 401 on /api/config
980
+ app.get("/api/session-check", (req, res) => {
981
+ res.json({ authed: validateSession(readSessionCookie(req)) });
982
+ });
979
983
  app.use("/api", requireAuth);
980
984
  // ── Config & Session info ──
981
985
  app.get("/api/config", async (_req, res) => {
@@ -1148,14 +1148,24 @@
1148
1148
  }
1149
1149
 
1150
1150
  function restoreLoginSession() {
1151
- fetch("/api/config", { credentials: "same-origin" })
1152
- .then(function(res) {
1153
- if (!res.ok) {
1151
+ // Probe an unauthenticated endpoint first so an anonymous visit
1152
+ // does not leave a noisy 401 on /api/config in DevTools.
1153
+ fetch("/api/session-check", { credentials: "same-origin" })
1154
+ .then(function(res) { return res.ok ? res.json() : { authed: false }; })
1155
+ .then(function(info) {
1156
+ if (!info || !info.authed) {
1154
1157
  state.loginChecked = true;
1155
1158
  render();
1156
1159
  return null;
1157
1160
  }
1158
- return res.json();
1161
+ return fetch("/api/config", { credentials: "same-origin" }).then(function(res) {
1162
+ if (!res.ok) {
1163
+ state.loginChecked = true;
1164
+ render();
1165
+ return null;
1166
+ }
1167
+ return res.json();
1168
+ });
1159
1169
  })
1160
1170
  .then(function(config) {
1161
1171
  if (!config) return;
@@ -1286,6 +1296,7 @@
1286
1296
  '<span class="shortcut-sep">·</span>' +
1287
1297
  '<button class="shortcut-key" data-key="enter" type="button">↵</button>' +
1288
1298
  '<button class="shortcut-key" data-key="ctrl_enter" type="button">C-↵</button>' +
1299
+ '<button class="shortcut-key" data-key="shift_tab" type="button" title="Shift+Tab(切换 plan / 自动接受 模式)">⇧⇥</button>' +
1289
1300
  '<button class="shortcut-key" data-key="escape" type="button">Esc</button>';
1290
1301
  }
1291
1302
 
@@ -1365,7 +1376,8 @@
1365
1376
  '</div>' +
1366
1377
  '<div class="login-subtitle">在浏览器中运行本机终端</div>' +
1367
1378
  '</div>' +
1368
- '<div class="login-body">' +
1379
+ '<form id="login-form" class="login-body" autocomplete="on">' +
1380
+ '<input type="text" name="username" autocomplete="username" value="wand" tabindex="-1" aria-hidden="true" style="position:absolute;left:-9999px;width:1px;height:1px;opacity:0;pointer-events:none" readonly />' +
1369
1381
  '<p class="login-hint">输入 Wand 访问密码以进入控制台。</p>' +
1370
1382
  '<div class="field">' +
1371
1383
  '<label class="field-label" for="password">密码</label>' +
@@ -1376,7 +1388,7 @@
1376
1388
  '<p id="password-hint" class="hint">使用你在 Wand 中设置的访问密码。</p>' +
1377
1389
  '<p id="login-error" class="error-message hidden" role="alert"></p>' +
1378
1390
  '</div>' +
1379
- '<button id="login-button" class="btn btn-primary btn-block">进入控制台</button>' +
1391
+ '<button id="login-button" type="submit" class="btn btn-primary btn-block">进入控制台</button>' +
1380
1392
  (hasNativeSwitchServer() ?
1381
1393
  '<button id="login-switch-server-button" class="btn btn-ghost btn-block login-switch-server" type="button">' +
1382
1394
  '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="8" rx="2"/><rect x="2" y="13" width="20" height="8" rx="2"/><line x1="6" y1="7" x2="6.01" y2="7"/><line x1="6" y1="17" x2="6.01" y2="17"/></svg>' +
@@ -1384,7 +1396,7 @@
1384
1396
  '</button>'
1385
1397
  : ''
1386
1398
  ) +
1387
- '</div>' +
1399
+ '</form>' +
1388
1400
  '</div>' +
1389
1401
  '</div>';
1390
1402
  }
@@ -1399,10 +1411,14 @@
1399
1411
  var preferredTool = getComposerTool();
1400
1412
  var composerMode = getSafeModeForTool(preferredTool, state.chatMode);
1401
1413
 
1414
+ var isDesktopPinned = state.sidebarPinned && !isMobileLayout();
1415
+ var isCollapsed = isDesktopPinned && state.sidebarCollapsed;
1416
+ var collapsedCls = isCollapsed ? ' sidebar-collapsed' : '';
1417
+ var sidebarCollapsedCls = isCollapsed ? ' collapsed' : '';
1402
1418
  return '<div class="app-container">' +
1403
1419
  '<div id="sessions-drawer-backdrop" class="drawer-backdrop' + drawerClass + '"></div>' +
1404
- '<div class="main-layout' + (state.sessionsDrawerOpen ? ' sidebar-open' : '') + (state.sidebarPinned && !isMobileLayout() ? ' sidebar-pinned' : '') + '">' +
1405
- '<aside id="sessions-drawer" class="sidebar' + drawerClass + (state.sidebarPinned && !isMobileLayout() ? ' pinned' : '') + '">' +
1420
+ '<div class="main-layout' + (state.sessionsDrawerOpen ? ' sidebar-open' : '') + (isDesktopPinned ? ' sidebar-pinned' : '') + collapsedCls + '">' +
1421
+ '<aside id="sessions-drawer" class="sidebar' + drawerClass + (isDesktopPinned ? ' pinned' : '') + sidebarCollapsedCls + '">' +
1406
1422
  '<div class="sidebar-header">' +
1407
1423
  '<div class="sidebar-header-main">' +
1408
1424
  '<div class="topbar-logo-icon">W</div>' +
@@ -1428,15 +1444,17 @@
1428
1444
  '<button id="sidebar-pin-btn" class="btn btn-ghost btn-sm sidebar-pin-toggle' + (state.sidebarPinned ? ' pinned' : '') + '" type="button" title="' + (state.sidebarPinned ? '取消固定侧栏' : '固定侧栏') + '">' +
1429
1445
  '<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>' +
1430
1446
  '</button>' +
1431
- '<button id="sidebar-collapse-btn" class="btn btn-ghost btn-sm sidebar-collapse-toggle" type="button" title="收起为窄条" aria-label="收起为窄条">' +
1432
- '<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>' +
1447
+ '<button id="sidebar-collapse-btn" class="btn btn-ghost btn-sm sidebar-collapse-toggle' + (isCollapsed ? ' collapsed' : '') + '" type="button" title="' + (isCollapsed ? '展开侧栏' : '收起为窄条') + '" aria-label="' + (isCollapsed ? '展开侧栏' : '收起为窄条') + '">' +
1448
+ (isCollapsed
1449
+ ? '<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>'
1450
+ : '<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>') +
1433
1451
  '</button>' +
1434
1452
  '<button id="close-drawer-button" class="btn btn-ghost btn-icon sidebar-close drawer-close-btn" type="button" aria-label="关闭菜单"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/></svg></button>' +
1435
1453
  '</div>' +
1436
1454
  '</div>' +
1437
1455
  '<div class="sidebar-body">' +
1438
1456
  '<div id="sessions-panel">' +
1439
- '<div class="sessions-list" id="sessions-list">' + renderSessions() + '</div>' +
1457
+ '<div class="sessions-list" id="sessions-list">' + renderSessionsListContent() + '</div>' +
1440
1458
  '</div>' +
1441
1459
  '</div>' +
1442
1460
  '<div class="sidebar-footer">' +
@@ -2937,19 +2955,22 @@
2937
2955
  '<p class="settings-card-desc">至少 6 个字符;保存后下次登录生效。</p>' +
2938
2956
  '</div>' +
2939
2957
  '</div>' +
2940
- '<div class="field">' +
2941
- '<label class="field-label" for="new-password">新密码</label>' +
2942
- '<input id="new-password" type="password" class="field-input" placeholder="输入新密码(至少 6 个字符)" autocomplete="new-password" />' +
2943
- '</div>' +
2944
- '<div class="field">' +
2945
- '<label class="field-label" for="confirm-password">确认密码</label>' +
2946
- '<input id="confirm-password" type="password" class="field-input" placeholder="再次输入新密码" autocomplete="new-password" />' +
2947
- '</div>' +
2948
- '<div class="settings-card-actions">' +
2949
- '<button id="save-password-button" class="btn btn-primary">保存密码</button>' +
2950
- '</div>' +
2951
- '<p id="settings-error" class="error-message hidden"></p>' +
2952
- '<p id="settings-success" class="hint settings-success-message hidden"></p>' +
2958
+ '<form id="change-password-form" autocomplete="on" onsubmit="return false;">' +
2959
+ '<input type="text" name="username" autocomplete="username" value="wand" tabindex="-1" aria-hidden="true" style="position:absolute;left:-9999px;width:1px;height:1px;opacity:0;pointer-events:none" readonly />' +
2960
+ '<div class="field">' +
2961
+ '<label class="field-label" for="new-password">新密码</label>' +
2962
+ '<input id="new-password" type="password" class="field-input" placeholder="输入新密码(至少 6 个字符)" autocomplete="new-password" />' +
2963
+ '</div>' +
2964
+ '<div class="field">' +
2965
+ '<label class="field-label" for="confirm-password">确认密码</label>' +
2966
+ '<input id="confirm-password" type="password" class="field-input" placeholder="再次输入新密码" autocomplete="new-password" />' +
2967
+ '</div>' +
2968
+ '<div class="settings-card-actions">' +
2969
+ '<button id="save-password-button" class="btn btn-primary" type="submit">保存密码</button>' +
2970
+ '</div>' +
2971
+ '<p id="settings-error" class="error-message hidden"></p>' +
2972
+ '<p id="settings-success" class="hint settings-success-message hidden"></p>' +
2973
+ '</form>' +
2953
2974
  '</div>' +
2954
2975
  '<div class="settings-card">' +
2955
2976
  '<div class="settings-card-head">' +
@@ -3085,6 +3106,35 @@
3085
3106
  return groups.join("");
3086
3107
  }
3087
3108
 
3109
+ function isSidebarNarrow() {
3110
+ return !!state.sidebarPinned && !isMobileLayout() && !!state.sidebarCollapsed;
3111
+ }
3112
+
3113
+ function renderCollapsedSessionTiles() {
3114
+ var running = state.sessions.filter(function(s) {
3115
+ return !s.archived && s.status === "running";
3116
+ });
3117
+ running.sort(function(a, b) {
3118
+ var ta = a.startedAt ? new Date(a.startedAt).getTime() : 0;
3119
+ var tb = b.startedAt ? new Date(b.startedAt).getTime() : 0;
3120
+ return ta - tb;
3121
+ });
3122
+ if (running.length === 0) {
3123
+ return '<div class="sidebar-collapsed-empty" title="无运行中的会话">—</div>';
3124
+ }
3125
+ return '<div class="sidebar-collapsed-tiles">' +
3126
+ running.map(function(s, i) {
3127
+ var activeCls = s.id === state.selectedId ? " active" : "";
3128
+ var title = s.summary || s.command || ("会话 " + (i + 1));
3129
+ return '<button class="sidebar-collapsed-tile' + activeCls + '" type="button" data-collapsed-session-id="' + escapeHtml(s.id) + '" title="' + escapeHtml(title) + '">' + (i + 1) + '</button>';
3130
+ }).join("") +
3131
+ '</div>';
3132
+ }
3133
+
3134
+ function renderSessionsListContent() {
3135
+ return isSidebarNarrow() ? renderCollapsedSessionTiles() : renderSessions();
3136
+ }
3137
+
3088
3138
  function renderSessionManageBar() {
3089
3139
  if (!state.sessionsManageMode) {
3090
3140
  return '<div class="session-manage-bar">' +
@@ -5372,6 +5422,11 @@
5372
5422
  var loginButton = document.getElementById("login-button");
5373
5423
  if (loginButton) {
5374
5424
  loginButton.addEventListener("click", login);
5425
+ var loginForm = document.getElementById("login-form");
5426
+ if (loginForm) loginForm.addEventListener("submit", function(e) {
5427
+ e.preventDefault();
5428
+ login();
5429
+ });
5375
5430
  var loginSwitchServerBtn = document.getElementById("login-switch-server-button");
5376
5431
  if (loginSwitchServerBtn) loginSwitchServerBtn.addEventListener("click", switchServer);
5377
5432
  var passwordEl = document.getElementById("password");
@@ -5511,6 +5566,8 @@
5511
5566
  if (closeDrawerBtn) closeDrawerBtn.addEventListener("click", closeSessionsDrawer);
5512
5567
  var pinBtn = document.getElementById("sidebar-pin-btn");
5513
5568
  if (pinBtn) pinBtn.addEventListener("click", toggleSidebarPin);
5569
+ var collapseBtn = document.getElementById("sidebar-collapse-btn");
5570
+ if (collapseBtn) collapseBtn.addEventListener("click", toggleSidebarCollapsed);
5514
5571
  var sidebarMoreBtn = document.getElementById("sidebar-more-btn");
5515
5572
  var sidebarOverflow = document.getElementById("sidebar-overflow-menu");
5516
5573
  if (sidebarMoreBtn && sidebarOverflow) {
@@ -6532,6 +6589,14 @@
6532
6589
  var target = event.target;
6533
6590
  if (!target || !(target instanceof Element)) return;
6534
6591
 
6592
+ var collapsedTile = target.closest(".sidebar-collapsed-tile");
6593
+ if (collapsedTile && collapsedTile instanceof HTMLElement && collapsedTile.dataset.collapsedSessionId) {
6594
+ event.preventDefault();
6595
+ event.stopPropagation();
6596
+ activateSessionItem(collapsedTile.dataset.collapsedSessionId);
6597
+ return;
6598
+ }
6599
+
6535
6600
  var historyToggle = target.closest("#claude-history-toggle");
6536
6601
  if (historyToggle) {
6537
6602
  event.preventDefault();
@@ -8301,7 +8366,7 @@
8301
8366
  updateSessionsList();
8302
8367
  } else {
8303
8368
  var listEl = document.getElementById("sessions-list");
8304
- var rendered = renderSessions();
8369
+ var rendered = renderSessionsListContent();
8305
8370
  if (listEl && listEl.innerHTML === rendered) {
8306
8371
  var countEl = document.getElementById("session-count");
8307
8372
  if (countEl) countEl.textContent = String(state.sessions.length);
@@ -8360,7 +8425,7 @@
8360
8425
  function updateSessionsList() {
8361
8426
  var listEl = document.getElementById("sessions-list");
8362
8427
  var countEl = document.getElementById("session-count");
8363
- if (listEl) listEl.innerHTML = renderSessions();
8428
+ if (listEl) listEl.innerHTML = renderSessionsListContent();
8364
8429
  if (countEl) countEl.textContent = String(state.sessions.length);
8365
8430
  updateShellChrome();
8366
8431
  // Re-render cross-session queue (container may have been destroyed by DOM rebuild)
@@ -8571,11 +8636,15 @@
8571
8636
  var drawer = document.getElementById("sessions-drawer");
8572
8637
  var mainLayout = document.querySelector(".main-layout");
8573
8638
  var pinBtn = document.getElementById("sidebar-pin-btn");
8639
+ var isDesktopPinned = state.sidebarPinned && !isMobileLayout();
8640
+ var isCollapsed = isDesktopPinned && state.sidebarCollapsed;
8574
8641
  if (drawer) {
8575
- drawer.classList.toggle("pinned", state.sidebarPinned && !isMobileLayout());
8642
+ drawer.classList.toggle("pinned", isDesktopPinned);
8643
+ drawer.classList.toggle("collapsed", isCollapsed);
8576
8644
  }
8577
8645
  if (mainLayout) {
8578
- mainLayout.classList.toggle("sidebar-pinned", state.sidebarPinned && !isMobileLayout());
8646
+ mainLayout.classList.toggle("sidebar-pinned", isDesktopPinned);
8647
+ mainLayout.classList.toggle("sidebar-collapsed", isCollapsed);
8579
8648
  }
8580
8649
  if (pinBtn) {
8581
8650
  pinBtn.classList.toggle("pinned", state.sidebarPinned);
@@ -8623,6 +8692,35 @@
8623
8692
  updateLayoutState();
8624
8693
  }
8625
8694
 
8695
+ function toggleSidebarCollapsed() {
8696
+ if (isMobileLayout()) return;
8697
+ // 在 drawer 模式(未 pin)下点 collapse 视为「先固定、再收起为窄条」——
8698
+ // 用户直觉是「点了就该看到窄条」,过去这里 early return 让按钮看上去没反应。
8699
+ if (!state.sidebarPinned) {
8700
+ state.sidebarPinned = true;
8701
+ state.sessionsDrawerOpen = true;
8702
+ try {
8703
+ localStorage.setItem("wand-sidebar-pinned", "true");
8704
+ } catch (e) {}
8705
+ }
8706
+ state.sidebarCollapsed = !state.sidebarCollapsed;
8707
+ try {
8708
+ localStorage.setItem("wand-sidebar-collapsed", String(state.sidebarCollapsed));
8709
+ } catch (e) {}
8710
+ render();
8711
+ var mainLayout = document.querySelector(".main-layout");
8712
+ if (mainLayout) {
8713
+ var onEnd = function(e) {
8714
+ if (e.propertyName === "padding-left") {
8715
+ mainLayout.removeEventListener("transitionend", onEnd);
8716
+ scheduleTerminalResize(true);
8717
+ }
8718
+ };
8719
+ mainLayout.addEventListener("transitionend", onEnd);
8720
+ }
8721
+ setTimeout(function() { scheduleTerminalResize(true); }, 350);
8722
+ }
8723
+
8626
8724
  function toggleSidebarPin() {
8627
8725
  if (isMobileLayout()) return;
8628
8726
  state.sidebarPinned = !state.sidebarPinned;
@@ -11936,6 +12034,7 @@
11936
12034
  var ptySpecialKeyMap = {
11937
12035
  space: " ",
11938
12036
  tab: String.fromCharCode(9),
12037
+ shift_tab: String.fromCharCode(27) + "[Z",
11939
12038
  backspace: String.fromCharCode(127),
11940
12039
  home: String.fromCharCode(27) + "[H",
11941
12040
  end: String.fromCharCode(27) + "[F",
@@ -11993,7 +12092,9 @@
11993
12092
  return {
11994
12093
  ctrl: event.ctrlKey,
11995
12094
  alt: event.altKey,
11996
- shift: event.shiftKey && key.length === 1,
12095
+ // 仅对单字符键保留 shift(控制 toUpperCase 路径),
12096
+ // 但 Tab 特例:物理 Shift+Tab 要走 buildPtySequence 的 back-tab 分支。
12097
+ shift: event.shiftKey && (key.length === 1 || key === "tab"),
11997
12098
  meta: event.metaKey
11998
12099
  };
11999
12100
  }
@@ -12216,6 +12317,8 @@
12216
12317
  function buildPtySequence(key, modifiers) {
12217
12318
  var mods = modifiers || { ctrl: false, alt: false, shift: false };
12218
12319
  if (isModifierKey(key)) return "";
12320
+ // Shift+Tab → CSI Z (back-tab)。Claude Code 用它在 plan / 自动接受 模式间切换。
12321
+ if (key === "tab" && mods.shift) return String.fromCharCode(27) + "[Z";
12219
12322
  var specialSequence = getPtySpecialSequence(key);
12220
12323
  if (specialSequence) return specialSequence;
12221
12324
  if (key.indexOf("ctrl_") === 0) {
@@ -13903,6 +14006,9 @@
13903
14006
  function sendTerminalResize(cols, rows) {
13904
14007
  if (!state.selectedId) return;
13905
14008
  var selectedSess = state.sessions.find(function(s) { return s.id === state.selectedId; });
14009
+ // 会话已被清除(如服务重启后 localStorage 还残留旧 id),后端 resize 会
14010
+ // 直接 400/404,console 留一条红色错误;这里提前剪掉,避免噪音。
14011
+ if (!selectedSess || selectedSess.status !== "running") return;
13906
14012
  if (isStructuredSession(selectedSess)) return;
13907
14013
  var nextSize = { cols: cols, rows: rows };
13908
14014
  if (state.lastResize.cols !== nextSize.cols || state.lastResize.rows !== nextSize.rows) {