@co0ontty/wand 1.29.0 → 1.29.3

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/cli.js CHANGED
@@ -78,14 +78,20 @@ async function main() {
78
78
  break;
79
79
  }
80
80
  case "config:show": {
81
- // 展示合并后的视图(JSON 部署字段 + DB 偏好字段)。
81
+ // 展示合并后的视图(JSON 部署字段 + DB 偏好字段)。password 脱敏:
82
+ // 显示是否已自定义("<set>" / "change-me"),避免误把真密码截图分享出去;
83
+ // 想看真值就直接读 DB(sqlite3 wand.db "SELECT * FROM app_config WHERE key='password'")。
82
84
  const { ensureDatabaseFile, resolveDatabasePath, WandStorage } = await import("./storage.js");
83
85
  const dbPath = resolveDatabasePath(configPath);
84
86
  ensureDatabaseFile(dbPath);
85
87
  const storage = new WandStorage(dbPath);
86
88
  try {
87
89
  const config = await loadConfigWithStorage(configPath, storage);
88
- process.stdout.write(`${JSON.stringify(config, null, 2)}\n`);
90
+ const display = {
91
+ ...config,
92
+ password: config.password === "change-me" ? "change-me" : "<set>",
93
+ };
94
+ process.stdout.write(`${JSON.stringify(display, null, 2)}\n`);
89
95
  }
90
96
  finally {
91
97
  storage.close();
@@ -109,6 +115,16 @@ async function main() {
109
115
  writePreferenceToStorage(config, storage, key, value);
110
116
  process.stdout.write(`[wand] Updated preference ${key} in ${dbPath}\n`);
111
117
  }
118
+ else if (key === "password") {
119
+ // password 走 SQLite,和 Web UI 设置面板保持同一个源。
120
+ // 历史上 setConfigValue("password") 只写 config.json,但登录用 dbPassword ?? config.password,
121
+ // 一旦 DB 里有值,写 JSON 完全不生效,命令静默返回成功 → 用户以为改了密码其实没改。
122
+ if (typeof value !== "string" || value.length < 6) {
123
+ throw new Error("password 长度至少为 6 个字符");
124
+ }
125
+ storage.setPassword(value);
126
+ process.stdout.write(`[wand] Updated password in ${dbPath}\n`);
127
+ }
112
128
  else {
113
129
  const nextConfig = setConfigValue(config, key, value);
114
130
  await saveConfig(configPath, nextConfig);
@@ -296,10 +312,10 @@ async function runAttach(live, configPath, useTui) {
296
312
  process.on("SIGTERM", onSignal);
297
313
  }
298
314
  function setConfigValue(config, key, value) {
299
- // 偏好字段(defaultMode/defaultCwd/...)由调用方分流到 storage,这里只处理 JSON 字段。
315
+ // 偏好字段(defaultMode/defaultCwd/...)由调用方分流到 storage,password 也走 DB
316
+ // (由 case "config:set" 直接处理),这里只剩纯 JSON 字段。
300
317
  switch (key) {
301
318
  case "host":
302
- case "password":
303
319
  case "shell":
304
320
  return {
305
321
  ...config,
package/dist/config.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import crypto from "node:crypto";
2
2
  import { existsSync } from "node:fs";
3
- import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import process from "node:process";
6
6
  const DEFAULT_CONFIG_DIR = ".wand";
@@ -86,6 +86,27 @@ export function resolveConfigDir(configPath) {
86
86
  export function hasConfigFile(configPath) {
87
87
  return existsSync(configPath);
88
88
  }
89
+ /**
90
+ * 原子写入:先写 `<dir>/.<file>.tmp-<rand>`,再 rename 覆盖目标。
91
+ * 防止 kill -9 / 断电 / 磁盘满导致 config.json 半截损坏 → 下次启动 catch 路径
92
+ * 把 defaults 写回去 → appSecret 重生成 → 已分发的 APK appToken 全部作废。
93
+ */
94
+ async function atomicWriteFile(filePath, content) {
95
+ const dir = path.dirname(filePath);
96
+ const base = path.basename(filePath);
97
+ const tmpPath = path.join(dir, `.${base}.tmp-${crypto.randomBytes(6).toString("hex")}`);
98
+ await writeFile(tmpPath, content, "utf8");
99
+ try {
100
+ await rename(tmpPath, filePath);
101
+ }
102
+ catch (err) {
103
+ try {
104
+ await unlink(tmpPath);
105
+ }
106
+ catch { /* noop */ }
107
+ throw err;
108
+ }
109
+ }
89
110
  export async function ensureConfig(configPath) {
90
111
  const dir = path.dirname(configPath);
91
112
  await mkdir(dir, { recursive: true });
@@ -95,20 +116,20 @@ export async function ensureConfig(configPath) {
95
116
  const normalized = `${JSON.stringify(merged, null, 2)}\n`;
96
117
  // Only write if the file content actually changed
97
118
  if (raw.trimEnd() !== normalized.trimEnd()) {
98
- await writeFile(configPath, normalized, "utf8");
119
+ await atomicWriteFile(configPath, normalized);
99
120
  }
100
121
  return merged;
101
122
  }
102
123
  catch {
103
124
  const config = defaultConfig();
104
- await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
125
+ await atomicWriteFile(configPath, `${JSON.stringify(config, null, 2)}\n`);
105
126
  return config;
106
127
  }
107
128
  }
108
129
  /** saveConfig 写出时去掉偏好字段——这些已经移到 SQLite。 */
109
130
  export async function saveConfig(configPath, config) {
110
131
  await mkdir(path.dirname(configPath), { recursive: true });
111
- await writeFile(configPath, `${JSON.stringify(stripPreferenceFields(config), null, 2)}\n`, "utf8");
132
+ await atomicWriteFile(configPath, `${JSON.stringify(stripPreferenceFields(config), null, 2)}\n`);
112
133
  }
113
134
  /**
114
135
  * 启动期合并 JSON + DB 偏好。语义:
@@ -129,11 +150,21 @@ export async function loadConfigWithStorage(configPath, storage) {
129
150
  hadFile = true;
130
151
  }
131
152
  catch {
153
+ // 文件缺失 或 JSON 损坏:保留空 rawInput;reconcileAppSecret 会优先用 DB 里那份,
154
+ // 避免已分发 APK 被踢下线。
132
155
  rawInput = {};
133
156
  }
134
157
  migrateLegacyPreferencesToDb(rawInput, storage);
158
+ migrateLegacyPasswordToDb(rawInput, storage);
135
159
  const config = mergeWithDefaults(rawInput);
136
160
  applyStoragePreferences(config, storage);
161
+ // appSecret: DB 是权威源。
162
+ // - DB 有 → 直接覆盖 runtime config(即使 mergeWithDefaults 临时生成了新的,也以 DB 为准)
163
+ // - DB 无、config 有 → 老用户首次升级,把 config.json 里的 appSecret 落到 DB
164
+ // - 都没 → 用 config 当前那个(mergeWithDefaults 已经生成),落到 DB
165
+ reconcileAppSecret(config, storage);
166
+ // password: DB 优先映射到 runtime config,让 config:show 与 server.ts 都看到真值
167
+ applyStoragePassword(config, storage);
137
168
  // 如果 JSON 里有偏好字段(说明是老版本配置或刚迁移),重写一次干净版本
138
169
  const hasLegacyPrefs = PREFERENCE_KEYS.some((key) => key in rawInput);
139
170
  if (!hadFile || hasLegacyPrefs) {
@@ -141,6 +172,46 @@ export async function loadConfigWithStorage(configPath, storage) {
141
172
  }
142
173
  return config;
143
174
  }
175
+ /**
176
+ * 把 DB 里的 appSecret 同步到 runtime config,缺失时反向回填,保证 DB 永远有备份。
177
+ * 这条路径修掉了之前的 bug:mergeWithDefaults 在缺失 appSecret 时会随机生成一个新的,
178
+ * 一旦 config.json 因为任何原因丢字段(损坏/手动编辑/catch fallback),所有 APK appToken 立刻作废。
179
+ */
180
+ function reconcileAppSecret(config, storage) {
181
+ const dbSecret = storage.getAppSecret();
182
+ if (dbSecret && dbSecret.length >= 32) {
183
+ config.appSecret = dbSecret;
184
+ return;
185
+ }
186
+ if (config.appSecret && config.appSecret.length >= 32) {
187
+ storage.setAppSecret(config.appSecret);
188
+ return;
189
+ }
190
+ const fresh = crypto.randomBytes(32).toString("hex");
191
+ config.appSecret = fresh;
192
+ storage.setAppSecret(fresh);
193
+ }
194
+ /** 把 DB 里设置过的 password 映射到 runtime config 字段,让 config:show 不再展示 "change-me" 假象。 */
195
+ function applyStoragePassword(config, storage) {
196
+ const dbPassword = storage.getPassword();
197
+ if (dbPassword !== null) {
198
+ config.password = dbPassword;
199
+ }
200
+ }
201
+ /**
202
+ * 老用户如果曾经手动编辑 config.json 设过非默认密码,但从没用 Web UI 改过密码
203
+ * (DB 里没有 password 行),那个 JSON 字段就是当前生效密码。升级到 DB 权威源之前
204
+ * 必须把它搬到 DB,否则后续如果 saveConfig 路径剥离 password 字段或 JSON 被截断,
205
+ * 用户会被锁在门外。DB 已经有 password 行的情况下不覆盖。
206
+ */
207
+ function migrateLegacyPasswordToDb(rawInput, storage) {
208
+ if (storage.hasCustomPassword())
209
+ return;
210
+ const legacy = rawInput.password;
211
+ if (typeof legacy === "string" && legacy.length >= 6 && legacy !== "change-me") {
212
+ storage.setPassword(legacy);
213
+ }
214
+ }
144
215
  /** Build a JSON-safe view of WandConfig that excludes preference fields (which live in DB). */
145
216
  function stripPreferenceFields(config) {
146
217
  const out = { ...config };
package/dist/storage.d.ts CHANGED
@@ -28,6 +28,11 @@ export declare class WandStorage {
28
28
  setPassword(password: string): void;
29
29
  /** Check if password has been set (not default) */
30
30
  hasCustomPassword(): boolean;
31
+ /** Get appSecret from database (used to mint Android appTokens) */
32
+ getAppSecret(): string | null;
33
+ /** Persist appSecret in database (DB is the authoritative source after first migration) */
34
+ setAppSecret(value: string): void;
35
+ hasAppSecret(): boolean;
31
36
  saveAuthSession(token: string, expiresAt: number): void;
32
37
  getAuthSession(token: string): PersistedAuthSession | null;
33
38
  deleteAuthSession(token: string): void;
package/dist/storage.js CHANGED
@@ -302,6 +302,17 @@ export class WandStorage {
302
302
  hasCustomPassword() {
303
303
  return this.getPassword() !== null;
304
304
  }
305
+ /** Get appSecret from database (used to mint Android appTokens) */
306
+ getAppSecret() {
307
+ return this.getConfigValue("appSecret");
308
+ }
309
+ /** Persist appSecret in database (DB is the authoritative source after first migration) */
310
+ setAppSecret(value) {
311
+ this.setConfigValue("appSecret", value);
312
+ }
313
+ hasAppSecret() {
314
+ return this.getAppSecret() !== null;
315
+ }
305
316
  // ============ Auth Session Methods ============
306
317
  saveAuthSession(token, expiresAt) {
307
318
  this.db
@@ -1399,10 +1399,14 @@
1399
1399
  var preferredTool = getComposerTool();
1400
1400
  var composerMode = getSafeModeForTool(preferredTool, state.chatMode);
1401
1401
 
1402
+ var isDesktopPinned = state.sidebarPinned && !isMobileLayout();
1403
+ var isCollapsed = isDesktopPinned && state.sidebarCollapsed;
1404
+ var collapsedCls = isCollapsed ? ' sidebar-collapsed' : '';
1405
+ var sidebarCollapsedCls = isCollapsed ? ' collapsed' : '';
1402
1406
  return '<div class="app-container">' +
1403
1407
  '<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' : '') + '">' +
1408
+ '<div class="main-layout' + (state.sessionsDrawerOpen ? ' sidebar-open' : '') + (isDesktopPinned ? ' sidebar-pinned' : '') + collapsedCls + '">' +
1409
+ '<aside id="sessions-drawer" class="sidebar' + drawerClass + (isDesktopPinned ? ' pinned' : '') + sidebarCollapsedCls + '">' +
1406
1410
  '<div class="sidebar-header">' +
1407
1411
  '<div class="sidebar-header-main">' +
1408
1412
  '<div class="topbar-logo-icon">W</div>' +
@@ -1428,8 +1432,10 @@
1428
1432
  '<button id="sidebar-pin-btn" class="btn btn-ghost btn-sm sidebar-pin-toggle' + (state.sidebarPinned ? ' pinned' : '') + '" type="button" title="' + (state.sidebarPinned ? '取消固定侧栏' : '固定侧栏') + '">' +
1429
1433
  '<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
1434
  '</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>' +
1435
+ '<button id="sidebar-collapse-btn" class="btn btn-ghost btn-sm sidebar-collapse-toggle' + (isCollapsed ? ' collapsed' : '') + '" type="button" title="' + (isCollapsed ? '展开侧栏' : '收起为窄条') + '" aria-label="' + (isCollapsed ? '展开侧栏' : '收起为窄条') + '">' +
1436
+ (isCollapsed
1437
+ ? '<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>'
1438
+ : '<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
1439
  '</button>' +
1434
1440
  '<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
1441
  '</div>' +
@@ -1467,15 +1473,6 @@
1467
1473
  '</button>' +
1468
1474
  '</div>' +
1469
1475
  '</div>' +
1470
- '<div class="sidebar-rail" id="sidebar-rail" aria-hidden="true">' +
1471
- '<button class="sidebar-rail-expand" id="sidebar-rail-expand" type="button" title="展开侧栏" aria-label="展开侧栏">' +
1472
- '<svg width="16" height="16" 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>' +
1473
- '</button>' +
1474
- '<div class="sidebar-rail-list" id="sidebar-rail-list">' + renderRailSessions() + '</div>' +
1475
- '<button class="sidebar-rail-new" id="sidebar-rail-new" type="button" title="新会话" aria-label="新会话">' +
1476
- '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>' +
1477
- '</button>' +
1478
- '</div>' +
1479
1476
  '</aside>' +
1480
1477
  '<main class="main-content">' +
1481
1478
  '<div class="main-header-row">' +
@@ -5520,6 +5517,8 @@
5520
5517
  if (closeDrawerBtn) closeDrawerBtn.addEventListener("click", closeSessionsDrawer);
5521
5518
  var pinBtn = document.getElementById("sidebar-pin-btn");
5522
5519
  if (pinBtn) pinBtn.addEventListener("click", toggleSidebarPin);
5520
+ var collapseBtn = document.getElementById("sidebar-collapse-btn");
5521
+ if (collapseBtn) collapseBtn.addEventListener("click", toggleSidebarCollapsed);
5523
5522
  var sidebarMoreBtn = document.getElementById("sidebar-more-btn");
5524
5523
  var sidebarOverflow = document.getElementById("sidebar-overflow-menu");
5525
5524
  if (sidebarMoreBtn && sidebarOverflow) {
@@ -8580,11 +8579,15 @@
8580
8579
  var drawer = document.getElementById("sessions-drawer");
8581
8580
  var mainLayout = document.querySelector(".main-layout");
8582
8581
  var pinBtn = document.getElementById("sidebar-pin-btn");
8582
+ var isDesktopPinned = state.sidebarPinned && !isMobileLayout();
8583
+ var isCollapsed = isDesktopPinned && state.sidebarCollapsed;
8583
8584
  if (drawer) {
8584
- drawer.classList.toggle("pinned", state.sidebarPinned && !isMobileLayout());
8585
+ drawer.classList.toggle("pinned", isDesktopPinned);
8586
+ drawer.classList.toggle("collapsed", isCollapsed);
8585
8587
  }
8586
8588
  if (mainLayout) {
8587
- mainLayout.classList.toggle("sidebar-pinned", state.sidebarPinned && !isMobileLayout());
8589
+ mainLayout.classList.toggle("sidebar-pinned", isDesktopPinned);
8590
+ mainLayout.classList.toggle("sidebar-collapsed", isCollapsed);
8588
8591
  }
8589
8592
  if (pinBtn) {
8590
8593
  pinBtn.classList.toggle("pinned", state.sidebarPinned);
@@ -8632,6 +8635,27 @@
8632
8635
  updateLayoutState();
8633
8636
  }
8634
8637
 
8638
+ function toggleSidebarCollapsed() {
8639
+ if (isMobileLayout()) return;
8640
+ if (!state.sidebarPinned) return;
8641
+ state.sidebarCollapsed = !state.sidebarCollapsed;
8642
+ try {
8643
+ localStorage.setItem("wand-sidebar-collapsed", String(state.sidebarCollapsed));
8644
+ } catch (e) {}
8645
+ render();
8646
+ var mainLayout = document.querySelector(".main-layout");
8647
+ if (mainLayout) {
8648
+ var onEnd = function(e) {
8649
+ if (e.propertyName === "padding-left") {
8650
+ mainLayout.removeEventListener("transitionend", onEnd);
8651
+ scheduleTerminalResize(true);
8652
+ }
8653
+ };
8654
+ mainLayout.addEventListener("transitionend", onEnd);
8655
+ }
8656
+ setTimeout(function() { scheduleTerminalResize(true); }, 350);
8657
+ }
8658
+
8635
8659
  function toggleSidebarPin() {
8636
8660
  if (isMobileLayout()) return;
8637
8661
  state.sidebarPinned = !state.sidebarPinned;
@@ -99,6 +99,7 @@
99
99
 
100
100
  /* ===== 布局尺寸 ===== */
101
101
  --sidebar-width: 300px;
102
+ --sidebar-collapsed-width: 56px;
102
103
  --file-panel-width: 320px;
103
104
  --safe-bottom: 0px;
104
105
 
@@ -447,10 +448,14 @@
447
448
  /* ===== 侧边栏常驻 ===== */
448
449
  .main-layout.sidebar-pinned {
449
450
  padding-left: var(--sidebar-width);
451
+ transition: padding-left 0.3s var(--ease-out-expo);
450
452
  }
451
453
  .main-layout.sidebar-pinned .floating-sidebar-toggle {
452
454
  display: none;
453
455
  }
456
+ .main-layout.sidebar-pinned.sidebar-collapsed {
457
+ padding-left: var(--sidebar-collapsed-width);
458
+ }
454
459
 
455
460
  /* ===== 抽屉背景遮罩 ===== */
456
461
  .drawer-backdrop {
@@ -507,12 +512,65 @@
507
512
  pointer-events: auto;
508
513
  opacity: 1;
509
514
  box-shadow: none;
515
+ transition: width 0.3s var(--ease-out-expo), transform 0.35s var(--ease-out-expo), box-shadow 0.35s ease, opacity 0.25s ease;
510
516
  }
511
517
 
512
518
  .sidebar.pinned .sidebar-close {
513
519
  display: none;
514
520
  }
515
521
 
522
+ /* ===== 侧栏窄条模式(仅 desktop pin 模式生效)===== */
523
+ .sidebar.pinned.collapsed {
524
+ width: var(--sidebar-collapsed-width);
525
+ }
526
+ .sidebar.pinned.collapsed .sidebar-header {
527
+ padding: 14px 8px;
528
+ justify-content: center;
529
+ gap: 0;
530
+ }
531
+ .sidebar.pinned.collapsed .sidebar-header-main,
532
+ .sidebar.pinned.collapsed .sidebar-header-more,
533
+ .sidebar.pinned.collapsed .sidebar-pin-toggle,
534
+ .sidebar.pinned.collapsed .sidebar-body {
535
+ display: none;
536
+ }
537
+ .sidebar.pinned.collapsed .sidebar-header-actions {
538
+ gap: 0;
539
+ width: 100%;
540
+ justify-content: center;
541
+ }
542
+ .sidebar.pinned.collapsed .sidebar-footer {
543
+ padding: 10px 6px;
544
+ }
545
+ .sidebar.pinned.collapsed #drawer-new-session-button {
546
+ display: none;
547
+ }
548
+ .sidebar.pinned.collapsed .sidebar-footer-actions {
549
+ flex-direction: column;
550
+ gap: 6px;
551
+ align-items: stretch;
552
+ }
553
+ .sidebar.pinned.collapsed .sidebar-footer-actions .btn {
554
+ width: 100%;
555
+ min-width: 0;
556
+ padding: 8px 0;
557
+ justify-content: center;
558
+ }
559
+ .sidebar.pinned.collapsed .sidebar-footer-actions .btn span,
560
+ .sidebar.pinned.collapsed .sidebar-meta {
561
+ display: none;
562
+ }
563
+ .sidebar-collapse-toggle {
564
+ flex-shrink: 0;
565
+ transition: color var(--transition-fast);
566
+ }
567
+ .sidebar-collapse-toggle.collapsed {
568
+ color: var(--primary);
569
+ }
570
+ .sidebar:not(.pinned) .sidebar-collapse-toggle {
571
+ display: none;
572
+ }
573
+
516
574
  /* ===== 图钉按钮 ===== */
517
575
  .sidebar-pin-toggle {
518
576
  flex-shrink: 0;
@@ -8338,6 +8396,7 @@
8338
8396
  /* 平板适配 */
8339
8397
  @media (max-width: 768px) {
8340
8398
  .sidebar-pin-toggle { display: none; }
8399
+ .sidebar-collapse-toggle { display: none; }
8341
8400
  .sidebar.pinned:not(.open) {
8342
8401
  transform: translateX(-100%);
8343
8402
  pointer-events: none;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@co0ontty/wand",
3
- "version": "1.29.0",
3
+ "version": "1.29.3",
4
4
  "description": "A web terminal for local CLI tools like Claude.",
5
5
  "type": "module",
6
6
  "bin": {