@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.
- package/dist/auth.d.ts +20 -0
- package/dist/auth.js +31 -0
- package/dist/cert.d.ts +17 -2
- package/dist/cert.js +124 -68
- package/dist/config.js +12 -0
- package/dist/server.js +60 -51
- package/dist/tui/commands.js +51 -5
- package/dist/types.d.ts +9 -0
- package/dist/web-ui/content/scripts.js +181 -242
- package/dist/web-ui/content/styles.css +153 -316
- package/dist/ws-broadcast.d.ts +2 -2
- package/dist/ws-broadcast.js +5 -10
- package/package.json +1 -1
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
// Register Service Worker for PWA
|
|
2
|
-
//
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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:',
|
|
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
|
-
//
|
|
141
|
-
// queueBarItemExpanded: 展开面板里被点开看完整内容的 item 下标集合
|
|
146
|
+
// queueBarHoverIndex: 当前被鼠标悬停的气泡下标(null 时默认展开队首)
|
|
142
147
|
// queueBarDrag: 拖拽排序进行中时的临时状态(pointer 捕获、起始坐标、参考 rect)
|
|
143
|
-
|
|
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
|
-
//
|
|
12487
|
-
//
|
|
12488
|
-
//
|
|
12489
|
-
//
|
|
12490
|
-
//
|
|
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
|
|
12469
|
+
function queueChipTruncate(text) {
|
|
12496
12470
|
if (typeof text !== "string") return "";
|
|
12497
12471
|
var s = text.replace(/\s+/g, " ").trim();
|
|
12498
|
-
if (s.length <=
|
|
12499
|
-
return s.slice(0,
|
|
12472
|
+
if (s.length <= QUEUE_CHIP_MAX_TEXT) return s;
|
|
12473
|
+
return s.slice(0, QUEUE_CHIP_MAX_TEXT) + "…";
|
|
12500
12474
|
}
|
|
12501
12475
|
|
|
12502
|
-
|
|
12503
|
-
|
|
12504
|
-
|
|
12505
|
-
|
|
12506
|
-
|
|
12507
|
-
if (state.
|
|
12508
|
-
|
|
12509
|
-
|
|
12510
|
-
|
|
12511
|
-
|
|
12512
|
-
|
|
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
|
|
12550
|
-
// ol 内容单独 render —— 拖拽 / 删除 / 展开会频繁动它,外层骨架不重建避免抖动。
|
|
12489
|
+
function renderQueueBarHtml(items, inFlight, atCapacity, immediateLabel) {
|
|
12551
12490
|
var single = items.length <= 1;
|
|
12552
|
-
var
|
|
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
|
|
12498
|
+
var isExpanded = i === expandedIdx;
|
|
12556
12499
|
var itemClass = "queue-bar-item";
|
|
12557
|
-
if (
|
|
12500
|
+
if (isExpanded) itemClass += " expanded";
|
|
12558
12501
|
if (single) itemClass += " queue-bar-item-single";
|
|
12559
|
-
|
|
12560
|
-
|
|
12561
|
-
|
|
12562
|
-
|
|
12563
|
-
'
|
|
12564
|
-
|
|
12565
|
-
|
|
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
|
-
|
|
12577
|
-
'<svg width="
|
|
12578
|
-
' stroke-width="
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
12628
|
-
|
|
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
|
-
|
|
12641
|
-
|
|
12642
|
-
|
|
12643
|
-
|
|
12644
|
-
|
|
12645
|
-
|
|
12646
|
-
|
|
12647
|
-
|
|
12648
|
-
|
|
12649
|
-
|
|
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
|
-
//
|
|
12670
|
-
|
|
12671
|
-
|
|
12672
|
-
|
|
12673
|
-
|
|
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.
|
|
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.
|
|
12741
|
-
|
|
12742
|
-
|
|
12743
|
-
|
|
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 +
|
|
12799
|
-
function queueBarDragStart(ev,
|
|
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
|
-
|
|
12805
|
-
|
|
12806
|
-
var listEl = itemEl.parentElement;
|
|
12723
|
+
if (!chipEl) return;
|
|
12724
|
+
var listEl = chipEl.parentElement;
|
|
12807
12725
|
if (!listEl) return;
|
|
12808
|
-
var origIndex = Number(
|
|
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
|
-
|
|
12812
|
-
var
|
|
12813
|
-
|
|
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 {
|
|
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:
|
|
12822
|
-
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
|
-
|
|
12751
|
+
chipEl.classList.add("dragging");
|
|
12752
|
+
// 让被拖元素保持 expanded(即便鼠标已经离开它)
|
|
12753
|
+
reflectQueueBarExpansion();
|
|
12835
12754
|
// 把所有兄弟先标记为"参与平滑动画"
|
|
12836
|
-
siblings.forEach(function(el) { if (el !==
|
|
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
|
-
|
|
12843
|
-
|
|
12844
|
-
|
|
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
|
-
//
|
|
12866
|
-
var
|
|
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 =
|
|
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
|
-
//
|
|
12912
|
-
|
|
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
|
-
|
|
12978
|
-
|
|
12979
|
-
|
|
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
|
|