@co0ontty/wand 1.31.3 → 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,
@@ -1686,12 +1687,6 @@
1686
1687
  '<span class="file-side-panel-title">文件</span>' +
1687
1688
  '</div>' +
1688
1689
  '<div class="file-side-panel-header-actions">' +
1689
- '<button class="file-side-panel-iconbtn file-explorer-toggle-hidden' +
1690
- (state.fileExplorerShowHidden ? ' active' : '') + '" id="file-explorer-toggle-hidden" type="button" title="' +
1691
- (state.fileExplorerShowHidden ? "隐藏点开头文件" : "显示隐藏文件") + '" aria-pressed="' +
1692
- (state.fileExplorerShowHidden ? "true" : "false") + '" aria-label="切换显示隐藏文件">' +
1693
- wandFileIcon(state.fileExplorerShowHidden ? "eye" : "eye-off", { size: 15 }) +
1694
- '</button>' +
1695
1690
  '<button class="file-side-panel-iconbtn" id="file-explorer-refresh" type="button" title="刷新" aria-label="刷新文件列表">' +
1696
1691
  wandFileIcon("refresh", { size: 15 }) +
1697
1692
  '</button>' +
@@ -1760,6 +1755,10 @@
1760
1755
  '</div>' +
1761
1756
  '</div>' +
1762
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>' +
1763
1762
  '<div class="composer-top-row">' +
1764
1763
  '<div id="todo-progress" class="todo-progress hidden">' +
1765
1764
  '<div class="todo-progress-header" id="todo-progress-toggle">' +
@@ -1780,11 +1779,6 @@
1780
1779
  '<ul class="todo-progress-list" id="todo-progress-list"></ul>' +
1781
1780
  '</div>' +
1782
1781
  '</div>' +
1783
- // 排队条宿主:默认 display:none,updateQueueBar() 在 queuedMessages 非空时
1784
- // 显形。结构上夹在 composer-top-row(todo 进度)和 input-composer(输入框 +
1785
- // 工具栏)之间,位置正好"在输入框上方、对话框右下角"。所有内容由 updater
1786
- // 注入;这里只保留稳定的外层骨架,便于 renderAppShell 全量重建后无缝复位。
1787
- '<div id="queue-bar-host" class="queue-bar-host" hidden></div>' +
1788
1782
  '<div class="input-composer">' +
1789
1783
  '<button id="prompt-optimize-btn" class="prompt-optimize-btn" type="button" title="提示词优化(AI)" aria-label="提示词优化">' +
1790
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">' +
@@ -3990,8 +3984,7 @@
3990
3984
  cwdEl.title = cwd;
3991
3985
  }
3992
3986
  var url = "/api/directory?q=" + encodeURIComponent(cwd) +
3993
- "&gitStatus=true" +
3994
- (state.fileExplorerShowHidden ? "&showHidden=true" : "");
3987
+ "&gitStatus=true";
3995
3988
  fetch(url, { credentials: "same-origin" })
3996
3989
  .then(function(res) {
3997
3990
  if (!res.ok) throw new Error("Failed to load directory.");
@@ -4154,8 +4147,7 @@
4154
4147
  var iconEl2 = item.querySelector(".tree-icon");
4155
4148
  if (iconEl2) iconEl2.textContent = "📂";
4156
4149
  var url = "/api/directory?q=" + encodeURIComponent(p) +
4157
- "&gitStatus=true" +
4158
- (state.fileExplorerShowHidden ? "&showHidden=true" : "");
4150
+ "&gitStatus=true";
4159
4151
  fetch(url, { credentials: "same-origin" })
4160
4152
  .then(function(res) { return res.json(); })
4161
4153
  .then(function(payload) {
@@ -4187,20 +4179,6 @@
4187
4179
  refreshFileExplorer({ cwd: parent });
4188
4180
  }
4189
4181
 
4190
- // Toggle the show-hidden flag and persist it.
4191
- function toggleExplorerHidden() {
4192
- state.fileExplorerShowHidden = !state.fileExplorerShowHidden;
4193
- try { localStorage.setItem("wand-file-show-hidden", state.fileExplorerShowHidden ? "1" : "0"); } catch (e) {}
4194
- var btn = document.getElementById("file-explorer-toggle-hidden");
4195
- if (btn) {
4196
- btn.classList.toggle("active", state.fileExplorerShowHidden);
4197
- btn.setAttribute("aria-pressed", state.fileExplorerShowHidden ? "true" : "false");
4198
- btn.innerHTML = wandFileIcon(state.fileExplorerShowHidden ? "eye" : "eye-off", { size: 15 });
4199
- btn.title = state.fileExplorerShowHidden ? "隐藏点开头文件" : "显示隐藏文件";
4200
- }
4201
- refreshFileExplorer();
4202
- }
4203
-
4204
4182
  function appendToComposer(text) {
4205
4183
  var inputBox = document.getElementById("input-box");
4206
4184
  if (!inputBox) return false;
@@ -6390,8 +6368,6 @@
6390
6368
  if (fileRefresh) fileRefresh.addEventListener("click", function() { refreshFileExplorer(); });
6391
6369
  var fileUp = document.getElementById("file-explorer-up");
6392
6370
  if (fileUp) fileUp.addEventListener("click", navigateExplorerUp);
6393
- var fileToggleHidden = document.getElementById("file-explorer-toggle-hidden");
6394
- if (fileToggleHidden) fileToggleHidden.addEventListener("click", toggleExplorerHidden);
6395
6371
 
6396
6372
  // 路径输入框:支持点击修改路径,回车跳转,Esc 撤销。
6397
6373
  var fileCwdInput = document.getElementById("file-explorer-cwd");
@@ -6840,14 +6816,8 @@
6840
6816
  setupVisualViewportHandlers();
6841
6817
 
6842
6818
  // 排队条:每次 shell 重渲后,重新挂事件代理 + 刷新内容。
6843
- // document-level 的 ESC / 外点击 handler 只挂一次(state.__queueBarGlobalAttached 守门)。
6844
6819
  attachQueueBarDelegates();
6845
6820
  updateQueueBar();
6846
- if (!state.__queueBarGlobalAttached) {
6847
- state.__queueBarGlobalAttached = true;
6848
- document.addEventListener("pointerdown", handleQueueBarOutsideClick, true);
6849
- document.addEventListener("keydown", handleQueueBarKeydown, true);
6850
- }
6851
6821
  }
6852
6822
 
6853
6823
  function saveWorkingDir(path) {
@@ -12483,104 +12453,79 @@
12483
12453
  }
12484
12454
 
12485
12455
  // ──────────────────────────────────────────────────────────────────────────
12486
- // 排队条(.queue-bar)—— 输入框上方独立浮条,承担三个事情:
12487
- // 1) 折叠态:● 排队 N + 队尾预览 + ⌃ chevron + ⚡ 立即 按钮
12488
- // 2) 展开面板:列出所有排队消息,支持拖拽换序 / 单条删除 / 一键清空
12489
- // 3) 立即按钮:中断当前回复,把队首作为新消息插队发出去(剩余队列保留)
12490
- // 数据源:session.queuedMessages(由后端 WS 推送 + postStructuredInput 乐观更新)。
12456
+ // 排队气泡条(.queue-bar)—— 垂直堆叠,浮在 "回复中" 状态条上方。
12457
+ // 交互参考 iOS 通讯录右侧的字母选择条:
12458
+ // · 默认只展开队首(即下一个要发的那条),显示编号 + 文本 + × 删除
12459
+ // · 其他消息收起成一根小横杠(指示存在但不占空间)
12460
+ // · 鼠标悬到任意小横杠 该条展开、原本展开的那条收回小横杠
12461
+ // · 悬停期间可以按住展开的那条向上 / 向下拖拽 → 换序
12462
+ // 末尾跟一个 ⚡ "立即" 按钮:中断当前回复、把队首作为新输入插队发出去。
12463
+ // 数据源:session.queuedMessages(后端 WS + postStructuredInput 乐观更新)。
12491
12464
  // ──────────────────────────────────────────────────────────────────────────
12492
12465
 
12493
- var QUEUE_BAR_MAX = 10; // 后端硬上限
12466
+ var QUEUE_BAR_MAX = 10; // 后端硬上限
12467
+ var QUEUE_CHIP_MAX_TEXT = 24; // 单个气泡展开时显示的字数上限
12494
12468
 
12495
- function queueBarTruncatePreview(text) {
12469
+ function queueChipTruncate(text) {
12496
12470
  if (typeof text !== "string") return "";
12497
12471
  var s = text.replace(/\s+/g, " ").trim();
12498
- if (s.length <= 48) return s;
12499
- return s.slice(0, 46) + "…";
12472
+ if (s.length <= QUEUE_CHIP_MAX_TEXT) return s;
12473
+ return s.slice(0, QUEUE_CHIP_MAX_TEXT) + "…";
12500
12474
  }
12501
12475
 
12502
- function renderQueueBarSkeleton(count, latestPreview, inFlight, atCapacity, immediateLabel) {
12503
- // 折叠条 + 展开面板的 HTML 一次性渲染好,靠 .queue-bar.expanded class 切换可见性。
12504
- // 这样展开/收起不需要拼字符串,纯 class toggle,动画也好做。
12505
- var dotClass = inFlight ? "queue-bar-dot queue-bar-dot-pulse" : "queue-bar-dot";
12506
- var barClass = "queue-bar";
12507
- if (state.queueBarExpanded) barClass += " expanded";
12508
- if (atCapacity) barClass += " queue-bar-capacity";
12509
- if (inFlight) barClass += " queue-bar-inflight";
12510
- var html =
12511
- '<div class="' + barClass + '" data-queue-bar="1">' +
12512
- '<button type="button" class="queue-bar-toggle" data-action="toggle"' +
12513
- ' aria-expanded="' + (state.queueBarExpanded ? "true" : "false") + '"' +
12514
- ' title="点击查看 / 收起排队消息">' +
12515
- '<span class="' + dotClass + '" aria-hidden="true"></span>' +
12516
- '<span class="queue-bar-count">' + (atCapacity ? "队列已满 " : "排队 ") + count + '</span>' +
12517
- '<span class="queue-bar-sep" aria-hidden="true">·</span>' +
12518
- '<span class="queue-bar-preview">' + escapeHtml(latestPreview) + '</span>' +
12519
- '<svg class="queue-bar-chevron" width="11" height="11" viewBox="0 0 24 24"' +
12520
- ' fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round"' +
12521
- ' stroke-linejoin="round" aria-hidden="true"><polyline points="6 15 12 9 18 15"/></svg>' +
12522
- '</button>' +
12523
- '<span class="queue-bar-divider" aria-hidden="true"></span>' +
12524
- '<button type="button" class="queue-bar-promote" data-action="promote"' +
12525
- ' title="中断当前回复,立刻发送队首这条" aria-label="立即发送队首">' +
12526
- '<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">' +
12527
- '<path d="M13 2 L4 14 L11 14 L10 22 L20 9 L13 9 Z"/>' +
12528
- '</svg>' +
12529
- '<span class="queue-bar-promote-label">' + escapeHtml(immediateLabel) + '</span>' +
12530
- '</button>' +
12531
- '<div class="queue-bar-panel" data-queue-panel="1" role="region" aria-label="排队消息列表">' +
12532
- '<div class="queue-bar-panel-header">' +
12533
- '<span class="queue-bar-panel-title">' + iconSvg("inbox", { size: 13, strokeWidth: 1.7, cls: "queue-bar-panel-title-icon" }) + '<span>排队中 (' + count + ')</span></span>' +
12534
- '<button type="button" class="queue-bar-clear" data-action="clear"' +
12535
- (count === 0 ? " disabled" : "") + '>清空</button>' +
12536
- '<button type="button" class="queue-bar-collapse" data-action="collapse" aria-label="收起">' +
12537
- '收起' +
12538
- '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor"' +
12539
- ' stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
12540
- '<polyline points="6 9 12 15 18 9"/></svg>' +
12541
- '</button>' +
12542
- '</div>' +
12543
- '<ol class="queue-bar-list" data-queue-list="1"></ol>' +
12544
- '</div>' +
12545
- '</div>';
12546
- 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;
12547
12487
  }
12548
12488
 
12549
- function renderQueueBarItems(listEl, items) {
12550
- // ol 内容单独 render —— 拖拽 / 删除 / 展开会频繁动它,外层骨架不重建避免抖动。
12489
+ function renderQueueBarHtml(items, inFlight, atCapacity, immediateLabel) {
12551
12490
  var single = items.length <= 1;
12552
- 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 = "";
12553
12496
  for (var i = 0; i < items.length; i++) {
12554
12497
  var raw = items[i] == null ? "" : String(items[i]);
12555
- var expanded = !!state.queueBarItemExpanded[i];
12498
+ var isExpanded = i === expandedIdx;
12556
12499
  var itemClass = "queue-bar-item";
12557
- if (expanded) itemClass += " expanded";
12500
+ if (isExpanded) itemClass += " expanded";
12558
12501
  if (single) itemClass += " queue-bar-item-single";
12559
- html +=
12560
- '<li class="' + itemClass + '" data-index="' + i + '">' +
12561
- '<button type="button" class="queue-bar-item-drag" data-action="drag" aria-label="拖动调整顺序"' +
12562
- ' title="按住拖动调整顺序"' + (single ? " disabled" : "") + '>' +
12563
- '<svg width="10" height="14" viewBox="0 0 10 14" fill="currentColor" aria-hidden="true">' +
12564
- '<circle cx="2.2" cy="2.2" r="1.2"/><circle cx="7.8" cy="2.2" r="1.2"/>' +
12565
- '<circle cx="2.2" cy="7" r="1.2"/><circle cx="7.8" cy="7" r="1.2"/>' +
12566
- '<circle cx="2.2" cy="11.8" r="1.2"/><circle cx="7.8" cy="11.8" r="1.2"/>' +
12567
- '</svg>' +
12568
- '</button>' +
12569
- '<span class="queue-bar-item-index">#' + (i + 1) + '</span>' +
12570
- '<button type="button" class="queue-bar-item-text" data-action="expand-text"' +
12571
- ' aria-expanded="' + (expanded ? "true" : "false") + '"' +
12572
- ' title="点击展开 / 收起完整内容">' +
12573
- escapeHtml(raw) +
12574
- '</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>' +
12575
12509
  '<button type="button" class="queue-bar-item-delete" data-action="delete"' +
12576
- ' aria-label="删除这条排队消息" title="删除">' +
12577
- '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor"' +
12578
- ' 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">' +
12579
12513
  '<line x1="6" y1="6" x2="18" y2="18"/><line x1="6" y1="18" x2="18" y2="6"/></svg>' +
12580
12514
  '</button>' +
12581
12515
  '</li>';
12582
12516
  }
12583
- 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
+ );
12584
12529
  }
12585
12530
 
12586
12531
  function updateQueueBar() {
@@ -12592,64 +12537,50 @@
12592
12537
  queue = Array.isArray(queue) ? queue : [];
12593
12538
 
12594
12539
  if (!isStructured || queue.length === 0) {
12595
- // 队列空 / 非结构化会话:整条隐藏,并清掉展开/逐条展开的本地态。
12596
12540
  host.hidden = true;
12597
12541
  host.innerHTML = "";
12598
- state.queueBarExpanded = false;
12599
- state.queueBarItemExpanded = {};
12542
+ state.queueBarHoverIndex = null;
12600
12543
  return;
12601
12544
  }
12602
12545
 
12546
+ // 拖拽进行中绝不重建 DOM,否则 pointer capture 丢失、气泡闪屏。
12547
+ if (state.queueBarDrag) return;
12548
+
12603
12549
  host.hidden = false;
12604
12550
  var inFlight = !!(session.structuredState && session.structuredState.inFlight && session.status === "running");
12605
12551
  var atCapacity = queue.length >= QUEUE_BAR_MAX;
12606
- var latest = queueBarTruncatePreview(queue[queue.length - 1]);
12607
- // inFlight=false 时按钮语义从"插队"退化为"立刻发";文案一并切换让用户不疑惑。
12608
12552
  var immediateLabel = inFlight ? "立即" : "发送";
12609
12553
 
12610
- // 拖拽进行中绝不重建骨架,否则 pointer capture 丢失、items 闪屏。
12611
- // 只更新列表内容(且如果数量不变也跳过整段重排)。
12612
- var existing = host.querySelector(".queue-bar");
12613
- if (state.queueBarDrag && existing) {
12614
- var listInDrag = existing.querySelector('[data-queue-list="1"]');
12615
- if (listInDrag && listInDrag.children.length !== queue.length) {
12616
- renderQueueBarItems(listInDrag, queue);
12617
- }
12618
- return;
12619
- }
12620
-
12621
- host.innerHTML = renderQueueBarSkeleton(queue.length, latest, inFlight, atCapacity, immediateLabel);
12622
- var listEl = host.querySelector('[data-queue-list="1"]');
12623
- if (listEl) renderQueueBarItems(listEl, queue);
12554
+ host.innerHTML = renderQueueBarHtml(queue, inFlight, atCapacity, immediateLabel);
12624
12555
  }
12625
12556
 
12626
- // ── 折叠 / 展开 ──
12627
- function setQueueBarExpanded(expanded) {
12628
- var next = !!expanded;
12629
- if (state.queueBarExpanded === next) return;
12630
- state.queueBarExpanded = next;
12631
- if (!next) state.queueBarItemExpanded = {};
12632
- updateQueueBar();
12633
- }
12634
- function toggleQueueBar() { setQueueBarExpanded(!state.queueBarExpanded); }
12635
-
12636
- function handleQueueBarOutsideClick(ev) {
12637
- if (!state.queueBarExpanded) return;
12557
+ // 只切换 .expanded class,不重建 DOM —— 避免鼠标移过去触发的重建
12558
+ // 让拖拽/输入框焦点等丢失。所有同步状态(hoverIndex / drag)的改变都通过这里反映到 DOM。
12559
+ function reflectQueueBarExpansion() {
12638
12560
  var host = document.getElementById("queue-bar-host");
12639
- if (!host) return;
12640
- if (host.contains(ev.target)) return;
12641
- setQueueBarExpanded(false);
12642
- }
12643
- function handleQueueBarKeydown(ev) {
12644
- if (!state.queueBarExpanded) return;
12645
- if (ev.key === "Escape" || ev.key === "Esc") {
12646
- setQueueBarExpanded(false);
12647
- // 焦点回到 toggle 按钮,方便键盘党
12648
- var toggle = document.querySelector(".queue-bar-toggle");
12649
- 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
+ }
12650
12574
  }
12651
12575
  }
12652
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
+
12653
12584
  // ── 单条删除 / 全部清空 / 队首插队 ──
12654
12585
  function rollbackQueueOptimistic(session, prevQueue) {
12655
12586
  updateSessionSnapshot({ id: session.id, queuedMessages: prevQueue });
@@ -12666,15 +12597,11 @@
12666
12597
  if (index < 0 || index >= queue.length) return;
12667
12598
  var prev = queue.slice();
12668
12599
  var next = queue.slice(0, index).concat(queue.slice(index + 1));
12669
- // 调整 queueBarItemExpanded 的下标偏移
12670
- var nextExpanded = {};
12671
- Object.keys(state.queueBarItemExpanded).forEach(function(k) {
12672
- var i = Number(k);
12673
- if (i === index) return;
12674
- if (i > index) nextExpanded[i - 1] = state.queueBarItemExpanded[k];
12675
- else nextExpanded[i] = state.queueBarItemExpanded[k];
12676
- });
12677
- 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
+ }
12678
12605
  updateSessionSnapshot({ id: session.id, queuedMessages: next });
12679
12606
  var refreshed = state.sessions.find(function(s) { return s.id === session.id; }) || session;
12680
12607
  state.currentMessages = buildMessagesForRender(refreshed, getPreferredMessages(refreshed, refreshed.output, false));
@@ -12702,7 +12629,7 @@
12702
12629
  if (!session) return;
12703
12630
  var prev = Array.isArray(session.queuedMessages) ? session.queuedMessages.slice() : [];
12704
12631
  if (prev.length === 0) return;
12705
- state.queueBarItemExpanded = {};
12632
+ state.queueBarHoverIndex = null;
12706
12633
  updateSessionSnapshot({ id: session.id, queuedMessages: [] });
12707
12634
  var refreshed = state.sessions.find(function(s) { return s.id === session.id; }) || session;
12708
12635
  state.currentMessages = buildMessagesForRender(refreshed, getPreferredMessages(refreshed, refreshed.output, false));
@@ -12736,21 +12663,13 @@
12736
12663
  var prev = queue.slice();
12737
12664
  var inFlight = !!(session.structuredState && session.structuredState.inFlight && session.status === "running");
12738
12665
 
12739
- // 乐观:剥掉队首
12740
- state.queueBarItemExpanded = (function() {
12741
- var out = {};
12742
- Object.keys(state.queueBarItemExpanded).forEach(function(k) {
12743
- var i = Number(k);
12744
- if (i === 0) return;
12745
- out[i - 1] = state.queueBarItemExpanded[k];
12746
- });
12747
- return out;
12748
- })();
12666
+ // 乐观:剥掉队首;hover 下标随之收缩
12667
+ if (typeof state.queueBarHoverIndex === "number") {
12668
+ if (state.queueBarHoverIndex === 0) state.queueBarHoverIndex = null;
12669
+ else state.queueBarHoverIndex -= 1;
12670
+ }
12749
12671
  updateSessionSnapshot({ id: session.id, queuedMessages: rest });
12750
12672
 
12751
- // 收起面板,让用户视线回到 chat(新消息马上要进 user turn)
12752
- setQueueBarExpanded(false);
12753
-
12754
12673
  var idempotencyKey = (typeof crypto !== "undefined" && crypto.randomUUID)
12755
12674
  ? crypto.randomUUID()
12756
12675
  : (Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 10));
@@ -12795,53 +12714,78 @@
12795
12714
  });
12796
12715
  }
12797
12716
 
12798
- // ── 拖拽排序(Pointer Events + 简化版 sort/animate)──
12799
- function queueBarDragStart(ev, handleEl) {
12717
+ // ── 拖拽排序(Pointer Events + 真实高度的 sort/animate)──
12718
+ function queueBarDragStart(ev, chipEl) {
12800
12719
  var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
12801
12720
  if (!session) return;
12802
12721
  var queue = Array.isArray(session.queuedMessages) ? session.queuedMessages.slice() : [];
12803
12722
  if (queue.length <= 1) return;
12804
- var itemEl = handleEl.closest(".queue-bar-item");
12805
- if (!itemEl) return;
12806
- var listEl = itemEl.parentElement;
12723
+ if (!chipEl) return;
12724
+ var listEl = chipEl.parentElement;
12807
12725
  if (!listEl) return;
12808
- var origIndex = Number(itemEl.getAttribute("data-index"));
12726
+ var origIndex = Number(chipEl.getAttribute("data-index"));
12809
12727
  var siblings = Array.prototype.slice.call(listEl.children);
12810
12728
  var rects = siblings.map(function(el) { return el.getBoundingClientRect(); });
12811
- var rect0 = rects[origIndex];
12812
- var itemHeight = rect0.height;
12813
- 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);
12814
12732
 
12815
12733
  ev.preventDefault();
12816
- try { handleEl.setPointerCapture(ev.pointerId); } catch (_e) {}
12734
+ try { chipEl.setPointerCapture(ev.pointerId); } catch (_e) {}
12817
12735
  if (navigator && navigator.vibrate) { try { navigator.vibrate(8); } catch (_e2) {} }
12818
12736
 
12819
12737
  state.queueBarDrag = {
12820
12738
  pointerId: ev.pointerId,
12821
- handleEl: handleEl,
12822
- itemEl: itemEl,
12739
+ handleEl: chipEl,
12740
+ itemEl: chipEl,
12823
12741
  listEl: listEl,
12824
12742
  siblings: siblings,
12825
12743
  rects: rects,
12826
12744
  origIndex: origIndex,
12827
12745
  targetIndex: origIndex,
12828
12746
  startY: ev.clientY,
12829
- itemHeight: itemHeight,
12830
12747
  gap: gap,
12831
12748
  queueSnapshot: queue,
12832
12749
  };
12833
12750
 
12834
- itemEl.classList.add("dragging");
12751
+ chipEl.classList.add("dragging");
12752
+ // 让被拖元素保持 expanded(即便鼠标已经离开它)
12753
+ reflectQueueBarExpansion();
12835
12754
  // 把所有兄弟先标记为"参与平滑动画"
12836
- 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"); });
12837
12756
 
12838
12757
  var move = function(e) { queueBarDragMove(e); };
12839
12758
  var up = function(e) { queueBarDragEnd(e); };
12840
12759
  state.queueBarDrag.moveHandler = move;
12841
12760
  state.queueBarDrag.upHandler = up;
12842
- handleEl.addEventListener("pointermove", move);
12843
- handleEl.addEventListener("pointerup", up);
12844
- 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;
12845
12789
  }
12846
12790
 
12847
12791
  function queueBarDragMove(ev) {
@@ -12862,13 +12806,11 @@
12862
12806
  }
12863
12807
  if (target !== d.targetIndex) {
12864
12808
  d.targetIndex = target;
12865
- // 重排兄弟元素的 translateY
12866
- var shift = d.itemHeight + d.gap;
12809
+ // 按真实高度精确算每个 sibling 的新 top
12810
+ var newTops = queueBarComputeNewTops(d.origIndex, target, d.rects, d.gap);
12867
12811
  d.siblings.forEach(function(el, idx) {
12868
12812
  if (idx === d.origIndex) return;
12869
- var move = 0;
12870
- if (d.origIndex < target && idx > d.origIndex && idx <= target) move = -shift;
12871
- else if (d.origIndex > target && idx < d.origIndex && idx >= target) move = shift;
12813
+ var move = newTops[idx] - d.rects[idx].top;
12872
12814
  el.style.transform = move ? "translateY(" + move + "px)" : "";
12873
12815
  });
12874
12816
  }
@@ -12908,14 +12850,8 @@
12908
12850
  order.splice(targetIndex, 0, origIndex);
12909
12851
  var nextQueue = order.map(function(i) { return queueSnapshot[i]; });
12910
12852
 
12911
- // 同步迁移 queueBarItemExpanded 下标
12912
- var nextExpanded = {};
12913
- Object.keys(state.queueBarItemExpanded).forEach(function(k) {
12914
- var oldI = Number(k);
12915
- var newI = order.indexOf(oldI);
12916
- if (newI >= 0) nextExpanded[newI] = state.queueBarItemExpanded[k];
12917
- });
12918
- state.queueBarItemExpanded = nextExpanded;
12853
+ // hover 下标迁移到新位置(拖拽放手时鼠标停在 targetIndex 上)
12854
+ state.queueBarHoverIndex = targetIndex;
12919
12855
 
12920
12856
  var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
12921
12857
  if (!session) { updateQueueBar(); return; }
@@ -12950,33 +12886,36 @@
12950
12886
  var actionEl = ev.target && ev.target.closest ? ev.target.closest("[data-action]") : null;
12951
12887
  if (!actionEl || !host.contains(actionEl)) return;
12952
12888
  var action = actionEl.getAttribute("data-action");
12953
- if (action === "drag") return; // 拖拽由 pointerdown 处理,吞掉点击避免误触发
12889
+ if (action === "drag") return; // 拖拽由 pointerdown 处理,吞掉 click
12954
12890
  ev.preventDefault();
12955
12891
  ev.stopPropagation();
12956
- if (action === "toggle") { toggleQueueBar(); return; }
12957
- if (action === "collapse") { setQueueBarExpanded(false); return; }
12958
12892
  if (action === "promote") { queueBarPromoteHead(); return; }
12959
- if (action === "clear") { queueBarClearAll(); return; }
12960
12893
  if (action === "delete") {
12961
12894
  var itemEl = actionEl.closest(".queue-bar-item");
12962
12895
  if (itemEl) queueBarDeleteItem(Number(itemEl.getAttribute("data-index")));
12963
12896
  return;
12964
12897
  }
12965
- if (action === "expand-text") {
12966
- var item = actionEl.closest(".queue-bar-item");
12967
- if (!item) return;
12968
- var idx = Number(item.getAttribute("data-index"));
12969
- state.queueBarItemExpanded[idx] = !state.queueBarItemExpanded[idx];
12970
- item.classList.toggle("expanded", !!state.queueBarItemExpanded[idx]);
12971
- actionEl.setAttribute("aria-expanded", state.queueBarItemExpanded[idx] ? "true" : "false");
12972
- return;
12973
- }
12974
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 检查跳过
12975
12911
  host.addEventListener("pointerdown", function(ev) {
12976
12912
  if (ev.button !== undefined && ev.button !== 0) return;
12977
- var handle = ev.target && ev.target.closest ? ev.target.closest('[data-action="drag"]') : null;
12978
- if (!handle || handle.disabled) return;
12979
- 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);
12980
12919
  });
12981
12920
  }
12982
12921