@co0ontty/wand 1.31.2 → 1.32.0

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.
@@ -1,22 +1,28 @@
1
1
  // Register Service Worker for PWA
2
- // For self-signed certificates, we need to handle certificate errors gracefully
2
+ // 自签证书场景下 SW 注册会被浏览器强拒(规范要求 secure context + 证书可信,
3
+ // 即便用户已"高级 → 继续访问"也不行)。这里只能优雅降级,并把解决路径打到 console。
3
4
  if ('serviceWorker' in navigator) {
4
- // First, try to fetch the service worker script with a custom handler for certificate errors
5
5
  fetch('/sw.js', { cache: 'no-cache' })
6
6
  .then(function(response) {
7
7
  if (response.ok) {
8
8
  return navigator.serviceWorker.register('/sw.js');
9
9
  }
10
- // If fetch fails (e.g., certificate error), skip service worker registration
11
10
  console.log('SW fetch failed, skipping service worker registration');
12
11
  return Promise.reject('Service worker script not available');
13
12
  })
14
13
  .catch(function(e) {
15
- // Distinguish between certificate errors and other failures
16
- if (e.name === 'TypeError' || e.message.includes('certificate')) {
17
- console.log('SW registration skipped: likely self-signed certificate issue');
14
+ var msg = (e && e.message) || String(e || '');
15
+ var isCertIssue = (e && e.name === 'TypeError') || /certificate|SSL|ERR_CERT/i.test(msg);
16
+ if (isCertIssue && location.protocol === 'https:') {
17
+ console.warn(
18
+ '[wand] PWA / Service Worker 因 TLS 证书不可信而跳过。\n' +
19
+ '解决办法(任选一种):\n' +
20
+ ' 1) 从 ' + location.origin + '/cert/server.crt 下载本机自签证书,导入到系统/浏览器"受信任根证书颁发机构"\n' +
21
+ ' 2) 在本机用 mkcert 签发受信任证书,并在 ~/.wand/config.json 配置 tls.certPath / tls.keyPath\n' +
22
+ ' 3) 用内网 CA 或 Let\'s Encrypt 给域名签真证书(同上配置 tls)'
23
+ );
18
24
  } else {
19
- console.log('SW registration failed:', e.message || e);
25
+ console.log('SW registration failed:', msg);
20
26
  }
21
27
  });
22
28
 
@@ -137,11 +143,9 @@
137
143
  })(), // 跨会话排队消息 [{ id, text, cwd, mode, tool }]
138
144
  structuredInputQueue: [], // 结构化会话同会话排队消息
139
145
  // 排队条 UI 局部状态 ——
140
- // queueBarExpanded: 折叠条点击展开成下拉面板
141
- // queueBarItemExpanded: 展开面板里被点开看完整内容的 item 下标集合
146
+ // queueBarHoverIndex: 当前被鼠标悬停的气泡下标(null 时默认展开队首)
142
147
  // queueBarDrag: 拖拽排序进行中时的临时状态(pointer 捕获、起始坐标、参考 rect)
143
- queueBarExpanded: false,
144
- queueBarItemExpanded: {},
148
+ queueBarHoverIndex: null,
145
149
  queueBarDrag: null,
146
150
  drafts: {},
147
151
  isSyncingInputBox: false,
@@ -293,9 +297,6 @@
293
297
  fileExplorerCwd: "",
294
298
  fileExplorerTruncated: false,
295
299
  fileExplorerTotal: 0,
296
- fileExplorerShowHidden: (function() {
297
- try { return localStorage.getItem("wand-file-show-hidden") === "1"; } catch (e) { return false; }
298
- })(),
299
300
  claudeHistory: [],
300
301
  claudeHistoryLoaded: false,
301
302
  claudeHistoryExpanded: true,
@@ -318,6 +319,59 @@
318
319
  })()
319
320
  };
320
321
 
322
+ // ── 统一线性图标库 ──
323
+ // 替代页面里散落的 emoji(🛡 / ⌨ / 📁 / 🔔 …)。这些 emoji 在系统字体里渲染成
324
+ // 彩色卡通形态,与项目温暖米色 + 棕橙的复古主题视觉冲突明显。这里集中维护
325
+ // currentColor 线性 SVG,让图标跟随父级文字颜色变化,hover / active 状态自然继承。
326
+ var ICON_PATHS = {
327
+ // shape sets — 24x24 viewbox, currentColor stroke
328
+ shield: '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>',
329
+ shieldCheck: '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><polyline points="9 12 11 14 15 10"/>',
330
+ keyboard: '<rect x="2" y="6" width="20" height="12" rx="2"/><line x1="6" y1="10" x2="6" y2="10"/><line x1="10" y1="10" x2="10" y2="10"/><line x1="14" y1="10" x2="14" y2="10"/><line x1="18" y1="10" x2="18" y2="10"/><line x1="6" y1="14" x2="6" y2="14"/><line x1="18" y1="14" x2="18" y2="14"/><line x1="9" y1="14" x2="15" y2="14"/>',
331
+ cloud: '<path d="M17.5 19a4.5 4.5 0 1 0-1-8.9 6 6 0 0 0-11.5 1.7A4 4 0 0 0 6 19h11.5z"/>',
332
+ terminal: '<polyline points="4 7 9 12 4 17"/><line x1="12" y1="17" x2="20" y2="17"/>',
333
+ chat: '<path d="M21 12a8 8 0 0 1-12.9 6.3L3 20l1.7-5.1A8 8 0 1 1 21 12z"/>',
334
+ folder: '<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>',
335
+ folderOpen:'<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2"/><path d="M3 9h18l-2 8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>',
336
+ trash: '<polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 13a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>',
337
+ slash: '<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>',
338
+ chevronDown: '<polyline points="6 9 12 15 18 9"/>',
339
+ chevronUp: '<polyline points="6 15 12 9 18 15"/>',
340
+ chevronRight:'<polyline points="9 6 15 12 9 18"/>',
341
+ bell: '<path d="M18 16v-5a6 6 0 1 0-12 0v5l-2 2h16z"/><path d="M10 21a2 2 0 0 0 4 0"/>',
342
+ music: '<path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/>',
343
+ vibrate: '<rect x="9" y="4" width="6" height="16" rx="1"/><path d="M5 8v8"/><path d="M3 10v4"/><path d="M19 8v8"/><path d="M21 10v4"/>',
344
+ globe: '<circle cx="12" cy="12" r="9"/><line x1="3" y1="12" x2="21" y2="12"/><path d="M12 3a14 14 0 0 1 0 18"/><path d="M12 3a14 14 0 0 0 0 18"/>',
345
+ smartphone:'<rect x="6" y="2" width="12" height="20" rx="2.5"/><line x1="11" y1="18" x2="13" y2="18"/>',
346
+ desktop: '<rect x="3" y="4" width="18" height="12" rx="2"/><line x1="8" y1="20" x2="16" y2="20"/><line x1="12" y1="16" x2="12" y2="20"/>',
347
+ link: '<path d="M10 14a4.5 4.5 0 0 0 6.36 0l3-3a4.5 4.5 0 1 0-6.36-6.36l-1.42 1.41"/><path d="M14 10a4.5 4.5 0 0 0-6.36 0l-3 3a4.5 4.5 0 1 0 6.36 6.36l1.42-1.41"/>',
348
+ palette: '<circle cx="13.5" cy="6.5" r="1"/><circle cx="17.5" cy="10.5" r="1"/><circle cx="8.5" cy="7.5" r="1"/><circle cx="6.5" cy="12.5" r="1"/><path d="M12 3a9 9 0 1 0 0 18 1.5 1.5 0 0 0 1.1-2.5 1.5 1.5 0 0 1 1.1-2.5h2.3A4.5 4.5 0 0 0 21 11.5C21 6.8 16.97 3 12 3z"/>',
349
+ play: '<polygon points="6 4 20 12 6 20 6 4"/>',
350
+ inbox: '<polyline points="22 13 16 13 14 16 10 16 8 13 2 13"/><path d="M5 5h14l3 8v6a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-6z"/>',
351
+ zap: '<polygon points="13 2 4 14 11 14 10 22 20 9 13 9 13 2"/>',
352
+ wrench: '<path d="M14.7 6.3a4 4 0 1 1 4 4l-9 9-3.5 1 1-3.5 7.5-7.5z"/>',
353
+ edit: '<path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4z"/>',
354
+ check: '<polyline points="5 12 10 17 19 7"/>',
355
+ signal: '<path d="M2 12a15 15 0 0 1 20 0"/><path d="M5 16a10 10 0 0 1 14 0"/><path d="M9 20a4 4 0 0 1 6 0"/><circle cx="12" cy="20" r="0.5" fill="currentColor"/>',
356
+ file: '<path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><polyline points="14 3 14 9 20 9"/>',
357
+ image: '<rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="9" cy="10" r="1.5"/><polyline points="3 18 9 12 14 17 21 12"/>',
358
+ sigma: '<polyline points="18 4 6 4 13 12 6 20 18 20"/>'
359
+ };
360
+ // 渲染 SVG 字符串。size 默认 14,strokeWidth 默认 1.8(与现有 send/stop 按钮线宽接近)。
361
+ // cls 用于添加额外 class(如 .composer-pill-icon),便于 CSS 微调。
362
+ function iconSvg(name, opts) {
363
+ var path = ICON_PATHS[name];
364
+ if (!path) return "";
365
+ opts = opts || {};
366
+ var size = opts.size || 14;
367
+ var stroke = opts.strokeWidth || 1.8;
368
+ var cls = opts.cls ? ' class="' + opts.cls + '"' : "";
369
+ var fill = opts.fill || "none";
370
+ return '<svg' + cls + ' width="' + size + '" height="' + size + '" viewBox="0 0 24 24"' +
371
+ ' fill="' + fill + '" stroke="currentColor" stroke-width="' + stroke + '"' +
372
+ ' stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' + path + '</svg>';
373
+ }
374
+
321
375
  // ── Structured session status bar (in-flight timer) ──
322
376
  var _statusBarTimerId = null;
323
377
  var _statusBarStartTime = 0;
@@ -1313,7 +1367,7 @@
1313
1367
  app.innerHTML =
1314
1368
  '<div class="boot-loading">' +
1315
1369
  '<div class="boot-loading-card">' +
1316
- '<div class="boot-loading-text" style="font-size:1.3em;margin-bottom:12px">📡 无法连接到服务器</div>' +
1370
+ '<div class="boot-loading-text" style="font-size:1.3em;margin-bottom:12px;display:flex;align-items:center;justify-content:center;gap:8px">' + iconSvg("signal", { size: 20, strokeWidth: 1.8 }) + '<span>无法连接到服务器</span></div>' +
1317
1371
  '<div class="boot-loading-text" style="opacity:0.7;font-size:0.95em">请检查网络连接或确认 Wand 服务正在运行。</div>' +
1318
1372
  '<button onclick="location.reload()" style="margin-top:18px;padding:8px 24px;border-radius:8px;border:1px solid rgba(150,118,85,0.3);background:rgba(255,255,255,0.8);cursor:pointer;font-size:0.95em">重试</button>' +
1319
1373
  '</div>' +
@@ -1418,10 +1472,10 @@
1418
1472
  '</span>' +
1419
1473
  '<span class="approval-stats-popup" id="approval-stats-popup">' +
1420
1474
  '<span class="approval-stats-popup-title">自动批准统计</span>' +
1421
- (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>' : '') +
1422
- (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>' : '') +
1423
- (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>' : '') +
1424
- '<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>' +
1475
+ (stats.command > 0 ? '<span class="approval-stats-row"><span class="approval-stats-row-icon">' + iconSvg("terminal", { size: 12, strokeWidth: 1.8 }) + '</span><span class="approval-stats-row-label">命令执行</span><span class="approval-stats-row-count">' + stats.command + '</span></span>' : '') +
1476
+ (stats.file > 0 ? '<span class="approval-stats-row"><span class="approval-stats-row-icon">' + iconSvg("edit", { size: 12, strokeWidth: 1.8 }) + '</span><span class="approval-stats-row-label">文件写入</span><span class="approval-stats-row-count">' + stats.file + '</span></span>' : '') +
1477
+ (stats.tool > 0 ? '<span class="approval-stats-row"><span class="approval-stats-row-icon">' + iconSvg("wrench", { size: 12, strokeWidth: 1.8 }) + '</span><span class="approval-stats-row-label">其他工具</span><span class="approval-stats-row-count">' + stats.tool + '</span></span>' : '') +
1478
+ '<span class="approval-stats-row approval-stats-row-total"><span class="approval-stats-row-icon">' + iconSvg("sigma", { size: 12, strokeWidth: 1.8 }) + '</span><span class="approval-stats-row-label">合计</span><span class="approval-stats-row-count">' + stats.total + '</span></span>' +
1425
1479
  '</span>' +
1426
1480
  '</span>';
1427
1481
  }
@@ -1633,12 +1687,6 @@
1633
1687
  '<span class="file-side-panel-title">文件</span>' +
1634
1688
  '</div>' +
1635
1689
  '<div class="file-side-panel-header-actions">' +
1636
- '<button class="file-side-panel-iconbtn file-explorer-toggle-hidden' +
1637
- (state.fileExplorerShowHidden ? ' active' : '') + '" id="file-explorer-toggle-hidden" type="button" title="' +
1638
- (state.fileExplorerShowHidden ? "隐藏点开头文件" : "显示隐藏文件") + '" aria-pressed="' +
1639
- (state.fileExplorerShowHidden ? "true" : "false") + '" aria-label="切换显示隐藏文件">' +
1640
- wandFileIcon(state.fileExplorerShowHidden ? "eye" : "eye-off", { size: 15 }) +
1641
- '</button>' +
1642
1690
  '<button class="file-side-panel-iconbtn" id="file-explorer-refresh" type="button" title="刷新" aria-label="刷新文件列表">' +
1643
1691
  wandFileIcon("refresh", { size: 15 }) +
1644
1692
  '</button>' +
@@ -1687,26 +1735,30 @@
1687
1735
  '<p class="blank-chat-subtitle">支持终端 PTY 会话与结构化 chat 会话,两种模式可并存。</p>' +
1688
1736
  '<div class="blank-chat-tools">' +
1689
1737
  '<button class="blank-chat-tool-btn" id="welcome-tool-claude" type="button">' +
1690
- '<span class="tool-icon">🤖</span>新建终端会话' +
1738
+ '<span class="tool-icon">' + iconSvg("terminal", { size: 16, strokeWidth: 1.8 }) + '</span>新建终端会话' +
1691
1739
  '</button>' +
1692
1740
  '<button class="blank-chat-tool-btn" id="welcome-tool-codex" type="button">' +
1693
- '<span class="tool-icon">⌘</span>新建 Codex 会话' +
1741
+ '<span class="tool-icon tool-icon-text">⌘</span>新建 Codex 会话' +
1694
1742
  '</button>' +
1695
1743
  '<button class="blank-chat-tool-btn" id="welcome-tool-structured" type="button">' +
1696
- '<span class="tool-icon">💬</span>新建结构化会话' +
1744
+ '<span class="tool-icon">' + iconSvg("chat", { size: 16, strokeWidth: 1.8 }) + '</span>新建结构化会话' +
1697
1745
  '</button>' +
1698
1746
  '</div>' +
1699
1747
  '<div class="blank-chat-cwd-wrap">' +
1700
1748
  '<div class="blank-chat-cwd" id="blank-chat-cwd" role="button" tabindex="0" title="点击切换工作目录">' +
1701
- '<span class="blank-chat-cwd-icon">📁</span>' +
1749
+ '<span class="blank-chat-cwd-icon">' + iconSvg("folder", { size: 13, strokeWidth: 1.8 }) + '</span>' +
1702
1750
  '<span class="blank-chat-cwd-path" id="blank-chat-cwd-path">' + escapeHtml(getEffectiveCwd()) + '</span>' +
1703
- '<span class="blank-chat-cwd-arrow" id="blank-chat-cwd-arrow">▼</span>' +
1751
+ '<span class="blank-chat-cwd-arrow" id="blank-chat-cwd-arrow">' + iconSvg("chevronDown", { size: 11, strokeWidth: 2 }) + '</span>' +
1704
1752
  '</div>' +
1705
1753
  '<div class="blank-chat-cwd-dropdown hidden" id="blank-chat-cwd-dropdown"></div>' +
1706
1754
  '</div>' +
1707
1755
  '</div>' +
1708
1756
  '</div>' +
1709
1757
  '<div class="input-panel' + (state.selectedId ? "" : " hidden") + '">' +
1758
+ // 排队气泡宿主:默认 display:none,updateQueueBar() 在 queuedMessages 非空时
1759
+ // 显形。位置在 composer-top-row(含 "回复中" 状态条)之上,对话框右下角,
1760
+ // 不进入输入框内部。所有内容由 updater 注入;这里只保留稳定的外层骨架。
1761
+ '<div id="queue-bar-host" class="queue-bar-host" hidden></div>' +
1710
1762
  '<div class="composer-top-row">' +
1711
1763
  '<div id="todo-progress" class="todo-progress hidden">' +
1712
1764
  '<div class="todo-progress-header" id="todo-progress-toggle">' +
@@ -1727,11 +1779,6 @@
1727
1779
  '<ul class="todo-progress-list" id="todo-progress-list"></ul>' +
1728
1780
  '</div>' +
1729
1781
  '</div>' +
1730
- // 排队条宿主:默认 display:none,updateQueueBar() 在 queuedMessages 非空时
1731
- // 显形。结构上夹在 composer-top-row(todo 进度)和 input-composer(输入框 +
1732
- // 工具栏)之间,位置正好"在输入框上方、对话框右下角"。所有内容由 updater
1733
- // 注入;这里只保留稳定的外层骨架,便于 renderAppShell 全量重建后无缝复位。
1734
- '<div id="queue-bar-host" class="queue-bar-host" hidden></div>' +
1735
1782
  '<div class="input-composer">' +
1736
1783
  '<button id="prompt-optimize-btn" class="prompt-optimize-btn" type="button" title="提示词优化(AI)" aria-label="提示词优化">' +
1737
1784
  '<svg class="prompt-optimize-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
@@ -1778,7 +1825,7 @@
1778
1825
  '</span>' +
1779
1826
  '</span>' +
1780
1827
  renderAutoApproveChip(selectedSession) +
1781
- '<button id="terminal-interactive-toggle-top" class="composer-pill composer-pill-chip composer-interactive-toggle' + (state.terminalInteractive ? " active" : "") + '" type="button" title="切换终端交互模式">⌨</button>' +
1828
+ '<button id="terminal-interactive-toggle-top" class="composer-pill composer-pill-chip composer-interactive-toggle' + (state.terminalInteractive ? " active" : "") + '" type="button" title="切换终端交互模式" aria-label="切换终端交互模式">' + iconSvg("keyboard", { size: 13, strokeWidth: 1.7, cls: "composer-pill-icon" }) + '</button>' +
1782
1829
  '<span class="permission-actions hidden" id="permission-actions">' +
1783
1830
  '<span class="permission-actions-divider"></span>' +
1784
1831
  '<span class="permission-actions-label" id="permission-actions-label">等待授权</span>' +
@@ -1813,7 +1860,7 @@
1813
1860
  ? (function() {
1814
1861
  var bits = "";
1815
1862
  if (selectedSession.provider === "claude" && selectedSession.claudeSessionId) {
1816
- bits += '<span id="claude-session-id-badge" class="claude-session-id-badge" data-claude-id="' + escapeHtml(selectedSession.claudeSessionId) + '" title="点击复制 Claude 会话 ID">☁ ' + escapeHtml(selectedSession.claudeSessionId.slice(0, 8)) + '</span>';
1863
+ bits += '<span id="claude-session-id-badge" class="claude-session-id-badge" data-claude-id="' + escapeHtml(selectedSession.claudeSessionId) + '" title="点击复制 Claude 会话 ID">' + iconSvg("cloud", { size: 11, strokeWidth: 1.7, cls: "claude-session-id-icon" }) + '<span class="claude-session-id-text">' + escapeHtml(selectedSession.claudeSessionId.slice(0, 8)) + '</span></span>';
1817
1864
  }
1818
1865
  if (!isStructuredSession(selectedSession) && selectedSession.exitCode !== undefined && selectedSession.exitCode !== null) {
1819
1866
  if (bits) bits += '<span class="session-info-separator">|</span>';
@@ -1834,12 +1881,12 @@
1834
1881
  '</div>' +
1835
1882
  '<div class="modal-body">' +
1836
1883
  '<div class="folder-picker-quick-row">' +
1837
- '<button class="folder-picker-quick-btn" data-path="/tmp">🗑️ 临时目录</button>' +
1838
- '<button class="folder-picker-quick-btn" data-path="/">📁 根目录</button>' +
1884
+ '<button class="folder-picker-quick-btn btn-with-icon" data-path="/tmp">' + iconSvg("trash", { size: 13, strokeWidth: 1.7 }) + '<span>临时目录</span></button>' +
1885
+ '<button class="folder-picker-quick-btn btn-with-icon" data-path="/">' + iconSvg("folder", { size: 13, strokeWidth: 1.7 }) + '<span>根目录</span></button>' +
1839
1886
  '</div>' +
1840
1887
  '<div id="folder-breadcrumb" class="folder-breadcrumb"></div>' +
1841
1888
  '<div class="folder-picker">' +
1842
- '<span class="folder-picker-icon">📁</span>' +
1889
+ '<span class="folder-picker-icon">' + iconSvg("folder", { size: 15, strokeWidth: 1.7 }) + '</span>' +
1843
1890
  '<input type="text" id="folder-picker-input" class="folder-picker-input" value="" placeholder="输入或选择工作目录..." autocomplete="off" />' +
1844
1891
  '</div>' +
1845
1892
  '<div id="folder-picker-dropdown" class="folder-picker-dropdown hidden"></div>' +
@@ -2751,7 +2798,7 @@
2751
2798
  '</div>' +
2752
2799
  '<div class="settings-update-section" id="web-update-section">' +
2753
2800
  '<div class="settings-section-head">' +
2754
- '<span class="settings-section-icon">🌐</span>' +
2801
+ '<span class="settings-section-icon">' + iconSvg("globe", { size: 18, strokeWidth: 1.7 }) + '</span>' +
2755
2802
  '<div class="settings-section-head-text">' +
2756
2803
  '<h4 class="settings-section-heading">Web 端</h4>' +
2757
2804
  '<p class="settings-section-sub">浏览器访问的服务版本</p>' +
@@ -2780,7 +2827,7 @@
2780
2827
  '</div>' +
2781
2828
  '<div class="settings-update-section hidden" id="android-apk-section">' +
2782
2829
  '<div class="settings-section-head">' +
2783
- '<span class="settings-section-icon">📱</span>' +
2830
+ '<span class="settings-section-icon">' + iconSvg("smartphone", { size: 18, strokeWidth: 1.7 }) + '</span>' +
2784
2831
  '<div class="settings-section-head-text">' +
2785
2832
  '<h4 class="settings-section-heading">Android App</h4>' +
2786
2833
  '<p class="settings-section-sub">原生客户端版本与 APK 下载</p>' +
@@ -2814,7 +2861,7 @@
2814
2861
  '</div>' +
2815
2862
  '<div class="settings-update-section hidden" id="macos-dmg-section">' +
2816
2863
  '<div class="settings-section-head">' +
2817
- '<span class="settings-section-icon">🖥️</span>' +
2864
+ '<span class="settings-section-icon">' + iconSvg("desktop", { size: 18, strokeWidth: 1.7 }) + '</span>' +
2818
2865
  '<div class="settings-section-head-text">' +
2819
2866
  '<h4 class="settings-section-heading">macOS App</h4>' +
2820
2867
  '<p class="settings-section-sub">原生客户端版本与 DMG 下载</p>' +
@@ -2848,7 +2895,7 @@
2848
2895
  '</div>' +
2849
2896
  '<div class="settings-update-section" id="android-connect-section">' +
2850
2897
  '<div class="settings-section-head">' +
2851
- '<span class="settings-section-icon">🔗</span>' +
2898
+ '<span class="settings-section-icon">' + iconSvg("link", { size: 18, strokeWidth: 1.7 }) + '</span>' +
2852
2899
  '<div class="settings-section-head-text">' +
2853
2900
  '<h4 class="settings-section-heading">App 连接码</h4>' +
2854
2901
  '<p class="settings-section-sub">粘贴到 Android App 即可自动连接,无需密码;改密码后失效。</p>' +
@@ -2876,7 +2923,7 @@
2876
2923
  '</div>' +
2877
2924
  '<div class="settings-notification-section">' +
2878
2925
  '<div class="settings-section-head">' +
2879
- '<span class="settings-section-icon">🔔</span>' +
2926
+ '<span class="settings-section-icon">' + iconSvg("bell", { size: 18, strokeWidth: 1.7 }) + '</span>' +
2880
2927
  '<div class="settings-section-head-text">' +
2881
2928
  '<h4 class="settings-section-heading">通知偏好</h4>' +
2882
2929
  '<p class="settings-section-sub">提示音与应用内通知气泡</p>' +
@@ -2910,7 +2957,7 @@
2910
2957
  '</div>' +
2911
2958
  '<div id="native-sound-section" class="settings-notification-section hidden">' +
2912
2959
  '<div class="settings-section-head">' +
2913
- '<span class="settings-section-icon">🎵</span>' +
2960
+ '<span class="settings-section-icon">' + iconSvg("music", { size: 18, strokeWidth: 1.7 }) + '</span>' +
2914
2961
  '<div class="settings-section-head-text">' +
2915
2962
  '<h4 class="settings-section-heading">系统通知铃声</h4>' +
2916
2963
  '<p class="settings-section-sub">选择 Android 系统通知使用的铃声</p>' +
@@ -2918,12 +2965,12 @@
2918
2965
  '</div>' +
2919
2966
  '<div class="settings-row-with-action">' +
2920
2967
  '<select id="native-sound-select" class="field-input field-select"></select>' +
2921
- '<button id="native-sound-preview" class="btn btn-secondary btn-sm" type="button">▶ 试听</button>' +
2968
+ '<button id="native-sound-preview" class="btn btn-secondary btn-sm btn-with-icon" type="button">' + iconSvg("play", { size: 11, strokeWidth: 1.8, fill: "currentColor" }) + '<span>试听</span></button>' +
2922
2969
  '</div>' +
2923
2970
  '</div>' +
2924
2971
  '<div id="native-haptic-section" class="settings-notification-section hidden">' +
2925
2972
  '<div class="settings-section-head">' +
2926
- '<span class="settings-section-icon">📳</span>' +
2973
+ '<span class="settings-section-icon">' + iconSvg("vibrate", { size: 18, strokeWidth: 1.7 }) + '</span>' +
2927
2974
  '<div class="settings-section-head-text">' +
2928
2975
  '<h4 class="settings-section-heading">触感反馈</h4>' +
2929
2976
  '<p class="settings-section-sub">按钮操作和任务完成时提供振动反馈</p>' +
@@ -2941,7 +2988,7 @@
2941
2988
  '</div>' +
2942
2989
  '<div class="settings-notification-section">' +
2943
2990
  '<div class="settings-section-head">' +
2944
- '<span class="settings-section-icon">🌐</span>' +
2991
+ '<span class="settings-section-icon">' + iconSvg("globe", { size: 18, strokeWidth: 1.7 }) + '</span>' +
2945
2992
  '<div class="settings-section-head-text">' +
2946
2993
  '<h4 class="settings-section-heading">浏览器通知</h4>' +
2947
2994
  '<p class="settings-section-sub">来自系统通知中心的弹窗</p>' +
@@ -3059,7 +3106,7 @@
3059
3106
  (typeof WandNative !== "undefined" && typeof WandNative.getAppIcon === "function" ?
3060
3107
  '<div class="settings-app-icon-block">' +
3061
3108
  '<div class="settings-section-head">' +
3062
- '<span class="settings-section-icon">🎨</span>' +
3109
+ '<span class="settings-section-icon">' + iconSvg("palette", { size: 18, strokeWidth: 1.7 }) + '</span>' +
3063
3110
  '<div class="settings-section-head-text">' +
3064
3111
  '<h4 class="settings-section-heading">应用图标</h4>' +
3065
3112
  '<p class="settings-section-sub">选择 App 启动器图标,返回桌面后生效</p>' +
@@ -3937,8 +3984,7 @@
3937
3984
  cwdEl.title = cwd;
3938
3985
  }
3939
3986
  var url = "/api/directory?q=" + encodeURIComponent(cwd) +
3940
- "&gitStatus=true" +
3941
- (state.fileExplorerShowHidden ? "&showHidden=true" : "");
3987
+ "&gitStatus=true";
3942
3988
  fetch(url, { credentials: "same-origin" })
3943
3989
  .then(function(res) {
3944
3990
  if (!res.ok) throw new Error("Failed to load directory.");
@@ -4101,8 +4147,7 @@
4101
4147
  var iconEl2 = item.querySelector(".tree-icon");
4102
4148
  if (iconEl2) iconEl2.textContent = "📂";
4103
4149
  var url = "/api/directory?q=" + encodeURIComponent(p) +
4104
- "&gitStatus=true" +
4105
- (state.fileExplorerShowHidden ? "&showHidden=true" : "");
4150
+ "&gitStatus=true";
4106
4151
  fetch(url, { credentials: "same-origin" })
4107
4152
  .then(function(res) { return res.json(); })
4108
4153
  .then(function(payload) {
@@ -4134,20 +4179,6 @@
4134
4179
  refreshFileExplorer({ cwd: parent });
4135
4180
  }
4136
4181
 
4137
- // Toggle the show-hidden flag and persist it.
4138
- function toggleExplorerHidden() {
4139
- state.fileExplorerShowHidden = !state.fileExplorerShowHidden;
4140
- try { localStorage.setItem("wand-file-show-hidden", state.fileExplorerShowHidden ? "1" : "0"); } catch (e) {}
4141
- var btn = document.getElementById("file-explorer-toggle-hidden");
4142
- if (btn) {
4143
- btn.classList.toggle("active", state.fileExplorerShowHidden);
4144
- btn.setAttribute("aria-pressed", state.fileExplorerShowHidden ? "true" : "false");
4145
- btn.innerHTML = wandFileIcon(state.fileExplorerShowHidden ? "eye" : "eye-off", { size: 15 });
4146
- btn.title = state.fileExplorerShowHidden ? "隐藏点开头文件" : "显示隐藏文件";
4147
- }
4148
- refreshFileExplorer();
4149
- }
4150
-
4151
4182
  function appendToComposer(text) {
4152
4183
  var inputBox = document.getElementById("input-box");
4153
4184
  if (!inputBox) return false;
@@ -4982,9 +5013,9 @@
4982
5013
  // 新建会话时显示简化的目录选择器(单行紧凑设计)
4983
5014
  return '<div class="folder-picker-compact" id="folder-picker-container">' +
4984
5015
  '<div class="folder-picker-compact-row">' +
4985
- '<span class="folder-picker-compact-icon">📁</span>' +
5016
+ '<span class="folder-picker-compact-icon">' + iconSvg("folder", { size: 13, strokeWidth: 1.7 }) + '</span>' +
4986
5017
  '<input type="text" id="folder-picker-input" class="folder-picker-compact-input" value="' + escapeHtml(currentDir) + '" placeholder="工作目录" autocomplete="off" />' +
4987
- '<button type="button" id="folder-picker-toggle" class="folder-picker-toggle" title="选择目录">▼</button>' +
5018
+ '<button type="button" id="folder-picker-toggle" class="folder-picker-toggle" title="选择目录" aria-label="选择目录">' + iconSvg("chevronDown", { size: 11, strokeWidth: 2 }) + '</button>' +
4988
5019
  '</div>' +
4989
5020
  '<div id="folder-picker-dropdown" class="folder-picker-dropdown hidden">' +
4990
5021
  '<div class="folder-picker-quick-row">' +
@@ -5014,7 +5045,7 @@
5014
5045
  }
5015
5046
 
5016
5047
  return '<div class="working-dir-indicator" id="working-dir-indicator" title="' + escapeHtml(displayDir) + '" data-path="' + escapeHtml(displayDir) + '">' +
5017
- '<span class="working-dir-indicator-icon">📁</span>' +
5048
+ '<span class="working-dir-indicator-icon">' + iconSvg("folder", { size: 12, strokeWidth: 1.7 }) + '</span>' +
5018
5049
  '<span class="working-dir-indicator-path">' + escapeHtml(displayPath) + '</span>' +
5019
5050
  '</div>';
5020
5051
  }
@@ -6337,8 +6368,6 @@
6337
6368
  if (fileRefresh) fileRefresh.addEventListener("click", function() { refreshFileExplorer(); });
6338
6369
  var fileUp = document.getElementById("file-explorer-up");
6339
6370
  if (fileUp) fileUp.addEventListener("click", navigateExplorerUp);
6340
- var fileToggleHidden = document.getElementById("file-explorer-toggle-hidden");
6341
- if (fileToggleHidden) fileToggleHidden.addEventListener("click", toggleExplorerHidden);
6342
6371
 
6343
6372
  // 路径输入框:支持点击修改路径,回车跳转,Esc 撤销。
6344
6373
  var fileCwdInput = document.getElementById("file-explorer-cwd");
@@ -6787,14 +6816,8 @@
6787
6816
  setupVisualViewportHandlers();
6788
6817
 
6789
6818
  // 排队条:每次 shell 重渲后,重新挂事件代理 + 刷新内容。
6790
- // document-level 的 ESC / 外点击 handler 只挂一次(state.__queueBarGlobalAttached 守门)。
6791
6819
  attachQueueBarDelegates();
6792
6820
  updateQueueBar();
6793
- if (!state.__queueBarGlobalAttached) {
6794
- state.__queueBarGlobalAttached = true;
6795
- document.addEventListener("pointerdown", handleQueueBarOutsideClick, true);
6796
- document.addEventListener("keydown", handleQueueBarKeydown, true);
6797
- }
6798
6821
  }
6799
6822
 
6800
6823
  function saveWorkingDir(path) {
@@ -8417,8 +8440,8 @@
8417
8440
  if (isAutoApproveImpliedByMode(session)) return "";
8418
8441
  var enabled = !!session.autoApprovePermissions;
8419
8442
  return enabled
8420
- ? '<span id="auto-approve-toggle" class="composer-pill composer-pill-chip auto-approve-indicator active" title="自动批准已启用 — 点击关闭">🛡 自动</span>'
8421
- : '<span id="auto-approve-toggle" class="composer-pill composer-pill-chip auto-approve-indicator" title="自动批准已关闭 — 点击开启">🛡 手动</span>';
8443
+ ? '<span id="auto-approve-toggle" class="composer-pill composer-pill-chip auto-approve-indicator active" title="自动批准已启用 — 点击关闭">' + iconSvg("shieldCheck", { size: 12, strokeWidth: 1.7, cls: "composer-pill-icon" }) + '<span class="composer-pill-label">自动</span></span>'
8444
+ : '<span id="auto-approve-toggle" class="composer-pill composer-pill-chip auto-approve-indicator" title="自动批准已关闭 — 点击开启">' + iconSvg("shield", { size: 12, strokeWidth: 1.7, cls: "composer-pill-icon" }) + '<span class="composer-pill-label">手动</span></span>';
8422
8445
  }
8423
8446
 
8424
8447
  function fetchAvailableModels() {
@@ -11478,7 +11501,7 @@
11478
11501
  var a = items[i];
11479
11502
  var thumb = a.previewUrl
11480
11503
  ? '<img src="' + escapeHtml(a.previewUrl) + '" alt="">'
11481
- : '<span class="att-icon">📄</span>';
11504
+ : '<span class="att-icon">' + iconSvg("file", { size: 13, strokeWidth: 1.7 }) + '</span>';
11482
11505
  html += '<span class="attachment-pill" data-index="' + i + '">' +
11483
11506
  thumb +
11484
11507
  '<span class="att-name" title="' + escapeHtml(a.name) + '">' + escapeHtml(a.name) + '</span>' +
@@ -12430,104 +12453,79 @@
12430
12453
  }
12431
12454
 
12432
12455
  // ──────────────────────────────────────────────────────────────────────────
12433
- // 排队条(.queue-bar)—— 输入框上方独立浮条,承担三个事情:
12434
- // 1) 折叠态:● 排队 N + 队尾预览 + ⌃ chevron + ⚡ 立即 按钮
12435
- // 2) 展开面板:列出所有排队消息,支持拖拽换序 / 单条删除 / 一键清空
12436
- // 3) 立即按钮:中断当前回复,把队首作为新消息插队发出去(剩余队列保留)
12437
- // 数据源:session.queuedMessages(由后端 WS 推送 + postStructuredInput 乐观更新)。
12456
+ // 排队气泡条(.queue-bar)—— 垂直堆叠,浮在 "回复中" 状态条上方。
12457
+ // 交互参考 iOS 通讯录右侧的字母选择条:
12458
+ // · 默认只展开队首(即下一个要发的那条),显示编号 + 文本 + × 删除
12459
+ // · 其他消息收起成一根小横杠(指示存在但不占空间)
12460
+ // · 鼠标悬到任意小横杠 该条展开、原本展开的那条收回小横杠
12461
+ // · 悬停期间可以按住展开的那条向上 / 向下拖拽 → 换序
12462
+ // 末尾跟一个 ⚡ "立即" 按钮:中断当前回复、把队首作为新输入插队发出去。
12463
+ // 数据源:session.queuedMessages(后端 WS + postStructuredInput 乐观更新)。
12438
12464
  // ──────────────────────────────────────────────────────────────────────────
12439
12465
 
12440
- var QUEUE_BAR_MAX = 10; // 后端硬上限
12466
+ var QUEUE_BAR_MAX = 10; // 后端硬上限
12467
+ var QUEUE_CHIP_MAX_TEXT = 24; // 单个气泡展开时显示的字数上限
12441
12468
 
12442
- function queueBarTruncatePreview(text) {
12469
+ function queueChipTruncate(text) {
12443
12470
  if (typeof text !== "string") return "";
12444
12471
  var s = text.replace(/\s+/g, " ").trim();
12445
- if (s.length <= 48) return s;
12446
- return s.slice(0, 46) + "…";
12472
+ if (s.length <= QUEUE_CHIP_MAX_TEXT) return s;
12473
+ return s.slice(0, QUEUE_CHIP_MAX_TEXT) + "…";
12447
12474
  }
12448
12475
 
12449
- function renderQueueBarSkeleton(count, latestPreview, inFlight, atCapacity, immediateLabel) {
12450
- // 折叠条 + 展开面板的 HTML 一次性渲染好,靠 .queue-bar.expanded class 切换可见性。
12451
- // 这样展开/收起不需要拼字符串,纯 class toggle,动画也好做。
12452
- var dotClass = inFlight ? "queue-bar-dot queue-bar-dot-pulse" : "queue-bar-dot";
12453
- var barClass = "queue-bar";
12454
- if (state.queueBarExpanded) barClass += " expanded";
12455
- if (atCapacity) barClass += " queue-bar-capacity";
12456
- if (inFlight) barClass += " queue-bar-inflight";
12457
- var html =
12458
- '<div class="' + barClass + '" data-queue-bar="1">' +
12459
- '<button type="button" class="queue-bar-toggle" data-action="toggle"' +
12460
- ' aria-expanded="' + (state.queueBarExpanded ? "true" : "false") + '"' +
12461
- ' title="点击查看 / 收起排队消息">' +
12462
- '<span class="' + dotClass + '" aria-hidden="true"></span>' +
12463
- '<span class="queue-bar-count">' + (atCapacity ? "队列已满 " : "排队 ") + count + '</span>' +
12464
- '<span class="queue-bar-sep" aria-hidden="true">·</span>' +
12465
- '<span class="queue-bar-preview">' + escapeHtml(latestPreview) + '</span>' +
12466
- '<svg class="queue-bar-chevron" width="11" height="11" viewBox="0 0 24 24"' +
12467
- ' fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round"' +
12468
- ' stroke-linejoin="round" aria-hidden="true"><polyline points="6 15 12 9 18 15"/></svg>' +
12469
- '</button>' +
12470
- '<span class="queue-bar-divider" aria-hidden="true"></span>' +
12471
- '<button type="button" class="queue-bar-promote" data-action="promote"' +
12472
- ' title="中断当前回复,立刻发送队首这条" aria-label="立即发送队首">' +
12473
- '<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">' +
12474
- '<path d="M13 2 L4 14 L11 14 L10 22 L20 9 L13 9 Z"/>' +
12475
- '</svg>' +
12476
- '<span class="queue-bar-promote-label">' + escapeHtml(immediateLabel) + '</span>' +
12477
- '</button>' +
12478
- '<div class="queue-bar-panel" data-queue-panel="1" role="region" aria-label="排队消息列表">' +
12479
- '<div class="queue-bar-panel-header">' +
12480
- '<span class="queue-bar-panel-title">📥 排队中 (' + count + ')</span>' +
12481
- '<button type="button" class="queue-bar-clear" data-action="clear"' +
12482
- (count === 0 ? " disabled" : "") + '>清空</button>' +
12483
- '<button type="button" class="queue-bar-collapse" data-action="collapse" aria-label="收起">' +
12484
- '收起' +
12485
- '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor"' +
12486
- ' stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
12487
- '<polyline points="6 9 12 15 18 9"/></svg>' +
12488
- '</button>' +
12489
- '</div>' +
12490
- '<ol class="queue-bar-list" data-queue-list="1"></ol>' +
12491
- '</div>' +
12492
- '</div>';
12493
- return html;
12476
+ // 当前应该展开的下标:拖拽中 被拖的那条(data-index 不变);hover 被 hover 的;否则 → 第 0 项
12477
+ function queueBarExpandedIndex(itemsLength) {
12478
+ if (state.queueBarDrag && typeof state.queueBarDrag.origIndex === "number") {
12479
+ return state.queueBarDrag.origIndex;
12480
+ }
12481
+ if (typeof state.queueBarHoverIndex === "number"
12482
+ && state.queueBarHoverIndex >= 0
12483
+ && state.queueBarHoverIndex < itemsLength) {
12484
+ return state.queueBarHoverIndex;
12485
+ }
12486
+ return 0;
12494
12487
  }
12495
12488
 
12496
- function renderQueueBarItems(listEl, items) {
12497
- // ol 内容单独 render —— 拖拽 / 删除 / 展开会频繁动它,外层骨架不重建避免抖动。
12489
+ function renderQueueBarHtml(items, inFlight, atCapacity, immediateLabel) {
12498
12490
  var single = items.length <= 1;
12499
- var html = "";
12491
+ var barClass = "queue-bar";
12492
+ if (atCapacity) barClass += " queue-bar-capacity";
12493
+ if (inFlight) barClass += " queue-bar-inflight";
12494
+ var expandedIdx = queueBarExpandedIndex(items.length);
12495
+ var chips = "";
12500
12496
  for (var i = 0; i < items.length; i++) {
12501
12497
  var raw = items[i] == null ? "" : String(items[i]);
12502
- var expanded = !!state.queueBarItemExpanded[i];
12498
+ var isExpanded = i === expandedIdx;
12503
12499
  var itemClass = "queue-bar-item";
12504
- if (expanded) itemClass += " expanded";
12500
+ if (isExpanded) itemClass += " expanded";
12505
12501
  if (single) itemClass += " queue-bar-item-single";
12506
- html +=
12507
- '<li class="' + itemClass + '" data-index="' + i + '">' +
12508
- '<button type="button" class="queue-bar-item-drag" data-action="drag" aria-label="拖动调整顺序"' +
12509
- ' title="按住拖动调整顺序"' + (single ? " disabled" : "") + '>' +
12510
- '<svg width="10" height="14" viewBox="0 0 10 14" fill="currentColor" aria-hidden="true">' +
12511
- '<circle cx="2.2" cy="2.2" r="1.2"/><circle cx="7.8" cy="2.2" r="1.2"/>' +
12512
- '<circle cx="2.2" cy="7" r="1.2"/><circle cx="7.8" cy="7" r="1.2"/>' +
12513
- '<circle cx="2.2" cy="11.8" r="1.2"/><circle cx="7.8" cy="11.8" r="1.2"/>' +
12514
- '</svg>' +
12515
- '</button>' +
12516
- '<span class="queue-bar-item-index">#' + (i + 1) + '</span>' +
12517
- '<button type="button" class="queue-bar-item-text" data-action="expand-text"' +
12518
- ' aria-expanded="' + (expanded ? "true" : "false") + '"' +
12519
- ' title="点击展开 / 收起完整内容">' +
12520
- escapeHtml(raw) +
12521
- '</button>' +
12502
+ // 拖拽起手区是整个 chip,但 delete 按钮要独占点击。
12503
+ var titleAttr = isExpanded ? raw + "(按住可拖动调整顺序)" : raw;
12504
+ chips +=
12505
+ '<li class="' + itemClass + '" data-index="' + i + '" data-action="drag"' +
12506
+ ' title="' + escapeHtml(titleAttr) + '">' +
12507
+ '<span class="queue-bar-item-index" aria-hidden="true">' + (i + 1) + '</span>' +
12508
+ '<span class="queue-bar-item-text">' + escapeHtml(queueChipTruncate(raw)) + '</span>' +
12522
12509
  '<button type="button" class="queue-bar-item-delete" data-action="delete"' +
12523
- ' aria-label="删除这条排队消息" title="删除">' +
12524
- '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor"' +
12525
- ' stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
12510
+ ' aria-label="删除这条排队消息" title="删除" tabindex="' + (isExpanded ? "0" : "-1") + '">' +
12511
+ '<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor"' +
12512
+ ' stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
12526
12513
  '<line x1="6" y1="6" x2="18" y2="18"/><line x1="6" y1="18" x2="18" y2="6"/></svg>' +
12527
12514
  '</button>' +
12528
12515
  '</li>';
12529
12516
  }
12530
- listEl.innerHTML = html;
12517
+ return (
12518
+ '<div class="' + barClass + '" data-queue-bar="1">' +
12519
+ '<ol class="queue-bar-list" data-queue-list="1">' + chips + '</ol>' +
12520
+ '<button type="button" class="queue-bar-promote" data-action="promote"' +
12521
+ ' title="中断当前回复,立刻发送队首这条"' +
12522
+ ' aria-label="' + escapeHtml(immediateLabel) + '队首">' +
12523
+ '<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">' +
12524
+ '<path d="M13 2 L4 14 L11 14 L10 22 L20 9 L13 9 Z"/>' +
12525
+ '</svg>' +
12526
+ '</button>' +
12527
+ '</div>'
12528
+ );
12531
12529
  }
12532
12530
 
12533
12531
  function updateQueueBar() {
@@ -12539,64 +12537,50 @@
12539
12537
  queue = Array.isArray(queue) ? queue : [];
12540
12538
 
12541
12539
  if (!isStructured || queue.length === 0) {
12542
- // 队列空 / 非结构化会话:整条隐藏,并清掉展开/逐条展开的本地态。
12543
12540
  host.hidden = true;
12544
12541
  host.innerHTML = "";
12545
- state.queueBarExpanded = false;
12546
- state.queueBarItemExpanded = {};
12542
+ state.queueBarHoverIndex = null;
12547
12543
  return;
12548
12544
  }
12549
12545
 
12546
+ // 拖拽进行中绝不重建 DOM,否则 pointer capture 丢失、气泡闪屏。
12547
+ if (state.queueBarDrag) return;
12548
+
12550
12549
  host.hidden = false;
12551
12550
  var inFlight = !!(session.structuredState && session.structuredState.inFlight && session.status === "running");
12552
12551
  var atCapacity = queue.length >= QUEUE_BAR_MAX;
12553
- var latest = queueBarTruncatePreview(queue[queue.length - 1]);
12554
- // inFlight=false 时按钮语义从"插队"退化为"立刻发";文案一并切换让用户不疑惑。
12555
12552
  var immediateLabel = inFlight ? "立即" : "发送";
12556
12553
 
12557
- // 拖拽进行中绝不重建骨架,否则 pointer capture 丢失、items 闪屏。
12558
- // 只更新列表内容(且如果数量不变也跳过整段重排)。
12559
- var existing = host.querySelector(".queue-bar");
12560
- if (state.queueBarDrag && existing) {
12561
- var listInDrag = existing.querySelector('[data-queue-list="1"]');
12562
- if (listInDrag && listInDrag.children.length !== queue.length) {
12563
- renderQueueBarItems(listInDrag, queue);
12564
- }
12565
- return;
12566
- }
12567
-
12568
- host.innerHTML = renderQueueBarSkeleton(queue.length, latest, inFlight, atCapacity, immediateLabel);
12569
- var listEl = host.querySelector('[data-queue-list="1"]');
12570
- if (listEl) renderQueueBarItems(listEl, queue);
12554
+ host.innerHTML = renderQueueBarHtml(queue, inFlight, atCapacity, immediateLabel);
12571
12555
  }
12572
12556
 
12573
- // ── 折叠 / 展开 ──
12574
- function setQueueBarExpanded(expanded) {
12575
- var next = !!expanded;
12576
- if (state.queueBarExpanded === next) return;
12577
- state.queueBarExpanded = next;
12578
- if (!next) state.queueBarItemExpanded = {};
12579
- updateQueueBar();
12580
- }
12581
- function toggleQueueBar() { setQueueBarExpanded(!state.queueBarExpanded); }
12582
-
12583
- function handleQueueBarOutsideClick(ev) {
12584
- if (!state.queueBarExpanded) return;
12557
+ // 只切换 .expanded class,不重建 DOM —— 避免鼠标移过去触发的重建
12558
+ // 让拖拽/输入框焦点等丢失。所有同步状态(hoverIndex / drag)的改变都通过这里反映到 DOM。
12559
+ function reflectQueueBarExpansion() {
12585
12560
  var host = document.getElementById("queue-bar-host");
12586
- if (!host) return;
12587
- if (host.contains(ev.target)) return;
12588
- setQueueBarExpanded(false);
12589
- }
12590
- function handleQueueBarKeydown(ev) {
12591
- if (!state.queueBarExpanded) return;
12592
- if (ev.key === "Escape" || ev.key === "Esc") {
12593
- setQueueBarExpanded(false);
12594
- // 焦点回到 toggle 按钮,方便键盘党
12595
- var toggle = document.querySelector(".queue-bar-toggle");
12596
- if (toggle) toggle.focus();
12561
+ if (!host || host.hidden) return;
12562
+ var list = host.querySelector('[data-queue-list="1"]');
12563
+ if (!list) return;
12564
+ var children = list.children;
12565
+ var expandedIdx = queueBarExpandedIndex(children.length);
12566
+ for (var i = 0; i < children.length; i++) {
12567
+ var el = children[i];
12568
+ var should = i === expandedIdx;
12569
+ if (el.classList.contains("expanded") !== should) {
12570
+ el.classList.toggle("expanded", should);
12571
+ var del = el.querySelector('.queue-bar-item-delete');
12572
+ if (del) del.tabIndex = should ? 0 : -1;
12573
+ }
12597
12574
  }
12598
12575
  }
12599
12576
 
12577
+ function setQueueBarHoverIndex(idx) {
12578
+ var next = (idx == null ? null : Number(idx));
12579
+ if (state.queueBarHoverIndex === next) return;
12580
+ state.queueBarHoverIndex = next;
12581
+ reflectQueueBarExpansion();
12582
+ }
12583
+
12600
12584
  // ── 单条删除 / 全部清空 / 队首插队 ──
12601
12585
  function rollbackQueueOptimistic(session, prevQueue) {
12602
12586
  updateSessionSnapshot({ id: session.id, queuedMessages: prevQueue });
@@ -12613,15 +12597,11 @@
12613
12597
  if (index < 0 || index >= queue.length) return;
12614
12598
  var prev = queue.slice();
12615
12599
  var next = queue.slice(0, index).concat(queue.slice(index + 1));
12616
- // 调整 queueBarItemExpanded 的下标偏移
12617
- var nextExpanded = {};
12618
- Object.keys(state.queueBarItemExpanded).forEach(function(k) {
12619
- var i = Number(k);
12620
- if (i === index) return;
12621
- if (i > index) nextExpanded[i - 1] = state.queueBarItemExpanded[k];
12622
- else nextExpanded[i] = state.queueBarItemExpanded[k];
12623
- });
12624
- state.queueBarItemExpanded = nextExpanded;
12600
+ // hover 下标也要随之收缩,否则删完后展开的是错位的那条
12601
+ if (typeof state.queueBarHoverIndex === "number") {
12602
+ if (state.queueBarHoverIndex === index) state.queueBarHoverIndex = null;
12603
+ else if (state.queueBarHoverIndex > index) state.queueBarHoverIndex -= 1;
12604
+ }
12625
12605
  updateSessionSnapshot({ id: session.id, queuedMessages: next });
12626
12606
  var refreshed = state.sessions.find(function(s) { return s.id === session.id; }) || session;
12627
12607
  state.currentMessages = buildMessagesForRender(refreshed, getPreferredMessages(refreshed, refreshed.output, false));
@@ -12649,7 +12629,7 @@
12649
12629
  if (!session) return;
12650
12630
  var prev = Array.isArray(session.queuedMessages) ? session.queuedMessages.slice() : [];
12651
12631
  if (prev.length === 0) return;
12652
- state.queueBarItemExpanded = {};
12632
+ state.queueBarHoverIndex = null;
12653
12633
  updateSessionSnapshot({ id: session.id, queuedMessages: [] });
12654
12634
  var refreshed = state.sessions.find(function(s) { return s.id === session.id; }) || session;
12655
12635
  state.currentMessages = buildMessagesForRender(refreshed, getPreferredMessages(refreshed, refreshed.output, false));
@@ -12683,21 +12663,13 @@
12683
12663
  var prev = queue.slice();
12684
12664
  var inFlight = !!(session.structuredState && session.structuredState.inFlight && session.status === "running");
12685
12665
 
12686
- // 乐观:剥掉队首
12687
- state.queueBarItemExpanded = (function() {
12688
- var out = {};
12689
- Object.keys(state.queueBarItemExpanded).forEach(function(k) {
12690
- var i = Number(k);
12691
- if (i === 0) return;
12692
- out[i - 1] = state.queueBarItemExpanded[k];
12693
- });
12694
- return out;
12695
- })();
12666
+ // 乐观:剥掉队首;hover 下标随之收缩
12667
+ if (typeof state.queueBarHoverIndex === "number") {
12668
+ if (state.queueBarHoverIndex === 0) state.queueBarHoverIndex = null;
12669
+ else state.queueBarHoverIndex -= 1;
12670
+ }
12696
12671
  updateSessionSnapshot({ id: session.id, queuedMessages: rest });
12697
12672
 
12698
- // 收起面板,让用户视线回到 chat(新消息马上要进 user turn)
12699
- setQueueBarExpanded(false);
12700
-
12701
12673
  var idempotencyKey = (typeof crypto !== "undefined" && crypto.randomUUID)
12702
12674
  ? crypto.randomUUID()
12703
12675
  : (Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 10));
@@ -12742,53 +12714,78 @@
12742
12714
  });
12743
12715
  }
12744
12716
 
12745
- // ── 拖拽排序(Pointer Events + 简化版 sort/animate)──
12746
- function queueBarDragStart(ev, handleEl) {
12717
+ // ── 拖拽排序(Pointer Events + 真实高度的 sort/animate)──
12718
+ function queueBarDragStart(ev, chipEl) {
12747
12719
  var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
12748
12720
  if (!session) return;
12749
12721
  var queue = Array.isArray(session.queuedMessages) ? session.queuedMessages.slice() : [];
12750
12722
  if (queue.length <= 1) return;
12751
- var itemEl = handleEl.closest(".queue-bar-item");
12752
- if (!itemEl) return;
12753
- var listEl = itemEl.parentElement;
12723
+ if (!chipEl) return;
12724
+ var listEl = chipEl.parentElement;
12754
12725
  if (!listEl) return;
12755
- var origIndex = Number(itemEl.getAttribute("data-index"));
12726
+ var origIndex = Number(chipEl.getAttribute("data-index"));
12756
12727
  var siblings = Array.prototype.slice.call(listEl.children);
12757
12728
  var rects = siblings.map(function(el) { return el.getBoundingClientRect(); });
12758
- var rect0 = rects[origIndex];
12759
- var itemHeight = rect0.height;
12760
- var gap = 6; // CSS .queue-bar-list gap 保持一致
12729
+ // 真实间距:相邻两个 chip 的 top 差减去前一个高度(容错 hover 状态变化后的高度切换)
12730
+ var gap = 3;
12731
+ if (rects.length >= 2) gap = Math.max(0, rects[1].top - rects[0].top - rects[0].height);
12761
12732
 
12762
12733
  ev.preventDefault();
12763
- try { handleEl.setPointerCapture(ev.pointerId); } catch (_e) {}
12734
+ try { chipEl.setPointerCapture(ev.pointerId); } catch (_e) {}
12764
12735
  if (navigator && navigator.vibrate) { try { navigator.vibrate(8); } catch (_e2) {} }
12765
12736
 
12766
12737
  state.queueBarDrag = {
12767
12738
  pointerId: ev.pointerId,
12768
- handleEl: handleEl,
12769
- itemEl: itemEl,
12739
+ handleEl: chipEl,
12740
+ itemEl: chipEl,
12770
12741
  listEl: listEl,
12771
12742
  siblings: siblings,
12772
12743
  rects: rects,
12773
12744
  origIndex: origIndex,
12774
12745
  targetIndex: origIndex,
12775
12746
  startY: ev.clientY,
12776
- itemHeight: itemHeight,
12777
12747
  gap: gap,
12778
12748
  queueSnapshot: queue,
12779
12749
  };
12780
12750
 
12781
- itemEl.classList.add("dragging");
12751
+ chipEl.classList.add("dragging");
12752
+ // 让被拖元素保持 expanded(即便鼠标已经离开它)
12753
+ reflectQueueBarExpansion();
12782
12754
  // 把所有兄弟先标记为"参与平滑动画"
12783
- siblings.forEach(function(el) { if (el !== itemEl) el.classList.add("queue-bar-item-sliding"); });
12755
+ siblings.forEach(function(el) { if (el !== chipEl) el.classList.add("queue-bar-item-sliding"); });
12784
12756
 
12785
12757
  var move = function(e) { queueBarDragMove(e); };
12786
12758
  var up = function(e) { queueBarDragEnd(e); };
12787
12759
  state.queueBarDrag.moveHandler = move;
12788
12760
  state.queueBarDrag.upHandler = up;
12789
- handleEl.addEventListener("pointermove", move);
12790
- handleEl.addEventListener("pointerup", up);
12791
- handleEl.addEventListener("pointercancel", up);
12761
+ chipEl.addEventListener("pointermove", move);
12762
+ chipEl.addEventListener("pointerup", up);
12763
+ chipEl.addEventListener("pointercancel", up);
12764
+ }
12765
+
12766
+ // 给定 origIndex / target / 真实 rects,算出新排列下每个 sibling 的目标 top。
12767
+ // 用真实高度而不是固定 shift,因为 expanded chip 比 collapsed 高很多。
12768
+ function queueBarComputeNewTops(origIndex, target, rects, gap) {
12769
+ var n = rects.length;
12770
+ var order = [];
12771
+ for (var i = 0; i < n; i++) order.push(i);
12772
+ order.splice(origIndex, 1);
12773
+ order.splice(target, 0, origIndex);
12774
+ var top = rects[0].top;
12775
+ // list 是右对齐 column flex,所有元素相对 list 左边对齐 — 我们只关心 top
12776
+ // 用第一个 rect 的 top 作为锚点累加。
12777
+ // 但 list 起始位置不一定是 rects[0].top(rects[0] 现在变到 order[0] 的位置)
12778
+ // 这里需要找原本的 list top —— 取 rects 里最小 top 即可。
12779
+ var listTop = rects[0].top;
12780
+ for (var k = 1; k < n; k++) if (rects[k].top < listTop) listTop = rects[k].top;
12781
+ var newTops = {};
12782
+ var cursor = listTop;
12783
+ for (var newPos = 0; newPos < n; newPos++) {
12784
+ var oldIdx = order[newPos];
12785
+ newTops[oldIdx] = cursor;
12786
+ cursor += rects[oldIdx].height + gap;
12787
+ }
12788
+ return newTops;
12792
12789
  }
12793
12790
 
12794
12791
  function queueBarDragMove(ev) {
@@ -12809,13 +12806,11 @@
12809
12806
  }
12810
12807
  if (target !== d.targetIndex) {
12811
12808
  d.targetIndex = target;
12812
- // 重排兄弟元素的 translateY
12813
- var shift = d.itemHeight + d.gap;
12809
+ // 按真实高度精确算每个 sibling 的新 top
12810
+ var newTops = queueBarComputeNewTops(d.origIndex, target, d.rects, d.gap);
12814
12811
  d.siblings.forEach(function(el, idx) {
12815
12812
  if (idx === d.origIndex) return;
12816
- var move = 0;
12817
- if (d.origIndex < target && idx > d.origIndex && idx <= target) move = -shift;
12818
- else if (d.origIndex > target && idx < d.origIndex && idx >= target) move = shift;
12813
+ var move = newTops[idx] - d.rects[idx].top;
12819
12814
  el.style.transform = move ? "translateY(" + move + "px)" : "";
12820
12815
  });
12821
12816
  }
@@ -12855,14 +12850,8 @@
12855
12850
  order.splice(targetIndex, 0, origIndex);
12856
12851
  var nextQueue = order.map(function(i) { return queueSnapshot[i]; });
12857
12852
 
12858
- // 同步迁移 queueBarItemExpanded 下标
12859
- var nextExpanded = {};
12860
- Object.keys(state.queueBarItemExpanded).forEach(function(k) {
12861
- var oldI = Number(k);
12862
- var newI = order.indexOf(oldI);
12863
- if (newI >= 0) nextExpanded[newI] = state.queueBarItemExpanded[k];
12864
- });
12865
- state.queueBarItemExpanded = nextExpanded;
12853
+ // hover 下标迁移到新位置(拖拽放手时鼠标停在 targetIndex 上)
12854
+ state.queueBarHoverIndex = targetIndex;
12866
12855
 
12867
12856
  var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
12868
12857
  if (!session) { updateQueueBar(); return; }
@@ -12897,33 +12886,36 @@
12897
12886
  var actionEl = ev.target && ev.target.closest ? ev.target.closest("[data-action]") : null;
12898
12887
  if (!actionEl || !host.contains(actionEl)) return;
12899
12888
  var action = actionEl.getAttribute("data-action");
12900
- if (action === "drag") return; // 拖拽由 pointerdown 处理,吞掉点击避免误触发
12889
+ if (action === "drag") return; // 拖拽由 pointerdown 处理,吞掉 click
12901
12890
  ev.preventDefault();
12902
12891
  ev.stopPropagation();
12903
- if (action === "toggle") { toggleQueueBar(); return; }
12904
- if (action === "collapse") { setQueueBarExpanded(false); return; }
12905
12892
  if (action === "promote") { queueBarPromoteHead(); return; }
12906
- if (action === "clear") { queueBarClearAll(); return; }
12907
12893
  if (action === "delete") {
12908
12894
  var itemEl = actionEl.closest(".queue-bar-item");
12909
12895
  if (itemEl) queueBarDeleteItem(Number(itemEl.getAttribute("data-index")));
12910
12896
  return;
12911
12897
  }
12912
- if (action === "expand-text") {
12913
- var item = actionEl.closest(".queue-bar-item");
12914
- if (!item) return;
12915
- var idx = Number(item.getAttribute("data-index"));
12916
- state.queueBarItemExpanded[idx] = !state.queueBarItemExpanded[idx];
12917
- item.classList.toggle("expanded", !!state.queueBarItemExpanded[idx]);
12918
- actionEl.setAttribute("aria-expanded", state.queueBarItemExpanded[idx] ? "true" : "false");
12919
- return;
12920
- }
12921
12898
  });
12899
+ // hover 跟随:鼠标移到哪一条,哪一条就展开(拖拽进行中不响应,免得抢拖拽)
12900
+ host.addEventListener("mouseover", function(ev) {
12901
+ if (state.queueBarDrag) return;
12902
+ var chip = ev.target && ev.target.closest ? ev.target.closest(".queue-bar-item") : null;
12903
+ if (!chip || !host.contains(chip)) return;
12904
+ setQueueBarHoverIndex(Number(chip.getAttribute("data-index")));
12905
+ });
12906
+ host.addEventListener("mouseleave", function() {
12907
+ if (state.queueBarDrag) return;
12908
+ setQueueBarHoverIndex(null);
12909
+ });
12910
+ // 整个气泡都是拖拽起手区。delete / promote 按钮通过 closest 检查跳过
12922
12911
  host.addEventListener("pointerdown", function(ev) {
12923
12912
  if (ev.button !== undefined && ev.button !== 0) return;
12924
- var handle = ev.target && ev.target.closest ? ev.target.closest('[data-action="drag"]') : null;
12925
- if (!handle || handle.disabled) return;
12926
- queueBarDragStart(ev, handle);
12913
+ if (ev.target && ev.target.closest && ev.target.closest('[data-action="delete"], [data-action="promote"]')) return;
12914
+ var chip = ev.target && ev.target.closest ? ev.target.closest('.queue-bar-item') : null;
12915
+ if (!chip) return;
12916
+ // 拖拽前先把这条切到 expanded(鼠标按下时通常已经 hovered,但触屏没 hover)
12917
+ setQueueBarHoverIndex(Number(chip.getAttribute("data-index")));
12918
+ queueBarDragStart(ev, chip);
12927
12919
  });
12928
12920
  }
12929
12921
 
@@ -16173,10 +16165,10 @@
16173
16165
  '</span>' +
16174
16166
  '<span class="approval-stats-popup" id="approval-stats-popup">' +
16175
16167
  '<span class="approval-stats-popup-title">自动批准统计</span>' +
16176
- (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>' : '') +
16177
- (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>' : '') +
16178
- (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>' : '') +
16179
- '<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>' +
16168
+ (stats.command > 0 ? '<span class="approval-stats-row"><span class="approval-stats-row-icon">' + iconSvg("terminal", { size: 12, strokeWidth: 1.8 }) + '</span><span class="approval-stats-row-label">命令执行</span><span class="approval-stats-row-count">' + stats.command + '</span></span>' : '') +
16169
+ (stats.file > 0 ? '<span class="approval-stats-row"><span class="approval-stats-row-icon">' + iconSvg("edit", { size: 12, strokeWidth: 1.8 }) + '</span><span class="approval-stats-row-label">文件写入</span><span class="approval-stats-row-count">' + stats.file + '</span></span>' : '') +
16170
+ (stats.tool > 0 ? '<span class="approval-stats-row"><span class="approval-stats-row-icon">' + iconSvg("wrench", { size: 12, strokeWidth: 1.8 }) + '</span><span class="approval-stats-row-label">其他工具</span><span class="approval-stats-row-count">' + stats.tool + '</span></span>' : '') +
16171
+ '<span class="approval-stats-row approval-stats-row-total"><span class="approval-stats-row-icon">' + iconSvg("sigma", { size: 12, strokeWidth: 1.8 }) + '</span><span class="approval-stats-row-label">合计</span><span class="approval-stats-row-count">' + stats.total + '</span></span>' +
16180
16172
  '</span>';
16181
16173
  // Pulse animation on the badge
16182
16174
  var badge = container.querySelector(".approval-stats-badge");
@@ -16292,11 +16284,11 @@
16292
16284
  if (enabled) {
16293
16285
  toggle.className = base + " active";
16294
16286
  toggle.title = "自动批准已启用 — 点击关闭";
16295
- toggle.textContent = "🛡 自动";
16287
+ toggle.innerHTML = iconSvg("shieldCheck", { size: 12, strokeWidth: 1.7, cls: "composer-pill-icon" }) + '<span class="composer-pill-label">自动</span>';
16296
16288
  } else {
16297
16289
  toggle.className = base;
16298
16290
  toggle.title = "自动批准已关闭 — 点击开启";
16299
- toggle.textContent = "🛡 手动";
16291
+ toggle.innerHTML = iconSvg("shield", { size: 12, strokeWidth: 1.7, cls: "composer-pill-icon" }) + '<span class="composer-pill-label">手动</span>';
16300
16292
  }
16301
16293
  }
16302
16294