@co0ontty/wand 1.31.3 → 1.32.1
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 +348 -343
- package/dist/web-ui/content/styles.css +339 -401
- 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
|
|
|
@@ -74,6 +80,33 @@
|
|
|
74
80
|
var configPath = "${escapeHtml(configPath)}";
|
|
75
81
|
var CHAT_EXPAND_STATE_STORAGE_KEY = "wand-chat-expand-state-v1";
|
|
76
82
|
|
|
83
|
+
// ===== 一次性 localStorage 迁移 =====
|
|
84
|
+
// 用 schema 版本号确保每个 migration 只跑一次。每加一项就 ++LS_SCHEMA_VERSION
|
|
85
|
+
// 并在 LS_MIGRATIONS append 一个函数。已升级用户的 wand-ls-schema 大于等于
|
|
86
|
+
// 当前长度时整段跳过;新用户首次加载会一口气把所有 migration 都跑完再写
|
|
87
|
+
// schema 号 —— 因此每个 migration 函数对「key 不存在」的输入也必须是无害的。
|
|
88
|
+
var LS_MIGRATIONS = [
|
|
89
|
+
// v1(2026-05)取消独立的「图钉」按钮,呼出侧栏即常驻。旧版残留的
|
|
90
|
+
// wand-sidebar-pinned=false 会让老用户继续走 drawer 模式看不到新行为,
|
|
91
|
+
// 这里直接清掉,让 state 初始化回退到默认 true。
|
|
92
|
+
function migrateSidebarPinDefault() {
|
|
93
|
+
try { localStorage.removeItem("wand-sidebar-pinned"); } catch (e) {}
|
|
94
|
+
}
|
|
95
|
+
];
|
|
96
|
+
(function runLocalStorageMigrations() {
|
|
97
|
+
try {
|
|
98
|
+
var raw = localStorage.getItem("wand-ls-schema");
|
|
99
|
+
var applied = raw == null ? 0 : parseInt(raw, 10);
|
|
100
|
+
if (!(applied >= 0)) applied = 0;
|
|
101
|
+
for (var i = applied; i < LS_MIGRATIONS.length; i++) {
|
|
102
|
+
try { LS_MIGRATIONS[i](); } catch (e) {}
|
|
103
|
+
}
|
|
104
|
+
if (applied < LS_MIGRATIONS.length) {
|
|
105
|
+
localStorage.setItem("wand-ls-schema", String(LS_MIGRATIONS.length));
|
|
106
|
+
}
|
|
107
|
+
} catch (e) { /* localStorage 不可用就跳过,按默认行为运行 */ }
|
|
108
|
+
})();
|
|
109
|
+
|
|
77
110
|
var state = {
|
|
78
111
|
selectedId: (function() {
|
|
79
112
|
try { return localStorage.getItem("wand-selected-session") || null; } catch (e) { return null; }
|
|
@@ -137,11 +170,9 @@
|
|
|
137
170
|
})(), // 跨会话排队消息 [{ id, text, cwd, mode, tool }]
|
|
138
171
|
structuredInputQueue: [], // 结构化会话同会话排队消息
|
|
139
172
|
// 排队条 UI 局部状态 ——
|
|
140
|
-
//
|
|
141
|
-
// queueBarItemExpanded: 展开面板里被点开看完整内容的 item 下标集合
|
|
173
|
+
// queueBarHoverIndex: 当前被鼠标悬停的气泡下标(null 时默认展开队首)
|
|
142
174
|
// queueBarDrag: 拖拽排序进行中时的临时状态(pointer 捕获、起始坐标、参考 rect)
|
|
143
|
-
|
|
144
|
-
queueBarItemExpanded: {},
|
|
175
|
+
queueBarHoverIndex: null,
|
|
145
176
|
queueBarDrag: null,
|
|
146
177
|
drafts: {},
|
|
147
178
|
isSyncingInputBox: false,
|
|
@@ -150,7 +181,12 @@
|
|
|
150
181
|
bootstrapping: true,
|
|
151
182
|
sessionsDrawerOpen: false,
|
|
152
183
|
sidebarPinned: (function() {
|
|
153
|
-
|
|
184
|
+
// 新交互:桌面默认呼出即常驻;只有用户主动 X 关闭过才记 "false"。
|
|
185
|
+
// 老用户的旧值("true"/"false")继续生效,没存过 key 时回退到 true。
|
|
186
|
+
try {
|
|
187
|
+
var v = localStorage.getItem("wand-sidebar-pinned");
|
|
188
|
+
return v === null ? true : v !== "false";
|
|
189
|
+
} catch (e) { return true; }
|
|
154
190
|
})(),
|
|
155
191
|
sidebarCollapsed: (function() {
|
|
156
192
|
try { return localStorage.getItem("wand-sidebar-collapsed") === "true"; } catch (e) { return false; }
|
|
@@ -293,9 +329,6 @@
|
|
|
293
329
|
fileExplorerCwd: "",
|
|
294
330
|
fileExplorerTruncated: false,
|
|
295
331
|
fileExplorerTotal: 0,
|
|
296
|
-
fileExplorerShowHidden: (function() {
|
|
297
|
-
try { return localStorage.getItem("wand-file-show-hidden") === "1"; } catch (e) { return false; }
|
|
298
|
-
})(),
|
|
299
332
|
claudeHistory: [],
|
|
300
333
|
claudeHistoryLoaded: false,
|
|
301
334
|
claudeHistoryExpanded: true,
|
|
@@ -1379,6 +1412,36 @@
|
|
|
1379
1412
|
});
|
|
1380
1413
|
}
|
|
1381
1414
|
|
|
1415
|
+
// ===== 桌面:点 sidebar 外的空白处自动收起 =====
|
|
1416
|
+
// 旧版 drawer 模式下点 backdrop 关闭的便利性,在「呼出即常驻」之后用
|
|
1417
|
+
// document 级捕获 handler 续上。
|
|
1418
|
+
// - 仅 desktop + 全尺寸(非窄条)+ 已打开 时生效
|
|
1419
|
+
// - 窄条态不触发(窄条本来就是稳定常驻形态)
|
|
1420
|
+
// - 手机端由 .drawer-backdrop 元素自己接住点击,不在这里重复处理
|
|
1421
|
+
// - 各类弹层(modal / topbar-more / overflow 菜单 / 文件夹下拉等)不算
|
|
1422
|
+
// 「sidebar 外的空白」,否则点弹层会顺带把 sidebar 关掉
|
|
1423
|
+
// 用 capture 阶段是为了绕过下游按钮自己的 stopPropagation。
|
|
1424
|
+
document.addEventListener("click", function(e) {
|
|
1425
|
+
if (isMobileLayout()) return;
|
|
1426
|
+
if (!state.sidebarPinned) return;
|
|
1427
|
+
if (state.sidebarCollapsed) return;
|
|
1428
|
+
if (!state.sessionsDrawerOpen) return;
|
|
1429
|
+
var target = e.target;
|
|
1430
|
+
if (!target || !(target instanceof Element)) return;
|
|
1431
|
+
if (target.closest("#sessions-drawer")) return;
|
|
1432
|
+
if (target.closest("#sessions-toggle-button")) return;
|
|
1433
|
+
if (target.closest(".floating-sidebar-toggle")) return;
|
|
1434
|
+
if (target.closest(".sidebar-tile-bubble")) return;
|
|
1435
|
+
if (target.closest(
|
|
1436
|
+
".modal-backdrop, .modal-overlay, .modal-container, " +
|
|
1437
|
+
"[role='dialog'], [role='menu'], " +
|
|
1438
|
+
".topbar-more-menu, .sidebar-header-overflow, " +
|
|
1439
|
+
".folder-picker-dropdown, .path-suggestions, " +
|
|
1440
|
+
".permission-prompt-overlay, .restart-overlay"
|
|
1441
|
+
)) return;
|
|
1442
|
+
closeSessionsDrawer();
|
|
1443
|
+
}, true);
|
|
1444
|
+
|
|
1382
1445
|
renderBootLoading();
|
|
1383
1446
|
restoreLoginSession();
|
|
1384
1447
|
|
|
@@ -1603,9 +1666,6 @@
|
|
|
1603
1666
|
'</button>' +
|
|
1604
1667
|
'</div>' +
|
|
1605
1668
|
'</div>' +
|
|
1606
|
-
'<button id="sidebar-pin-btn" class="btn btn-ghost btn-sm sidebar-pin-toggle' + (state.sidebarPinned ? ' pinned' : '') + '" type="button" title="' + (state.sidebarPinned ? '取消固定侧栏' : '固定侧栏') + '">' +
|
|
1607
|
-
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="17" x2="12" y2="22"/><path d="M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24z"/></svg>' +
|
|
1608
|
-
'</button>' +
|
|
1609
1669
|
'<button id="sidebar-collapse-btn" class="btn btn-ghost btn-sm sidebar-collapse-toggle' + (isCollapsed ? ' collapsed' : '') + '" type="button" title="' + (isCollapsed ? '展开侧栏' : '收起为窄条') + '" aria-label="' + (isCollapsed ? '展开侧栏' : '收起为窄条') + '">' +
|
|
1610
1670
|
(isCollapsed
|
|
1611
1671
|
? '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="10 6 16 12 10 18"/><line x1="20" y1="5" x2="20" y2="19"/></svg>'
|
|
@@ -1686,12 +1746,6 @@
|
|
|
1686
1746
|
'<span class="file-side-panel-title">文件</span>' +
|
|
1687
1747
|
'</div>' +
|
|
1688
1748
|
'<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
1749
|
'<button class="file-side-panel-iconbtn" id="file-explorer-refresh" type="button" title="刷新" aria-label="刷新文件列表">' +
|
|
1696
1750
|
wandFileIcon("refresh", { size: 15 }) +
|
|
1697
1751
|
'</button>' +
|
|
@@ -1760,6 +1814,10 @@
|
|
|
1760
1814
|
'</div>' +
|
|
1761
1815
|
'</div>' +
|
|
1762
1816
|
'<div class="input-panel' + (state.selectedId ? "" : " hidden") + '">' +
|
|
1817
|
+
// 排队气泡宿主:默认 display:none,updateQueueBar() 在 queuedMessages 非空时
|
|
1818
|
+
// 显形。位置在 composer-top-row(含 "回复中" 状态条)之上,对话框右下角,
|
|
1819
|
+
// 不进入输入框内部。所有内容由 updater 注入;这里只保留稳定的外层骨架。
|
|
1820
|
+
'<div id="queue-bar-host" class="queue-bar-host" hidden></div>' +
|
|
1763
1821
|
'<div class="composer-top-row">' +
|
|
1764
1822
|
'<div id="todo-progress" class="todo-progress hidden">' +
|
|
1765
1823
|
'<div class="todo-progress-header" id="todo-progress-toggle">' +
|
|
@@ -1780,11 +1838,6 @@
|
|
|
1780
1838
|
'<ul class="todo-progress-list" id="todo-progress-list"></ul>' +
|
|
1781
1839
|
'</div>' +
|
|
1782
1840
|
'</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
1841
|
'<div class="input-composer">' +
|
|
1789
1842
|
'<button id="prompt-optimize-btn" class="prompt-optimize-btn" type="button" title="提示词优化(AI)" aria-label="提示词优化">' +
|
|
1790
1843
|
'<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">' +
|
|
@@ -1847,13 +1900,8 @@
|
|
|
1847
1900
|
'<button id="stop-button" class="btn-circle btn-circle-stop' + (state.selectedId ? "" : " hidden") + '" title="停止">' +
|
|
1848
1901
|
'<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><rect x="3" y="3" width="10" height="10" rx="2"/></svg>' +
|
|
1849
1902
|
'</button>' +
|
|
1850
|
-
//
|
|
1851
|
-
//
|
|
1852
|
-
// 用 pill 形态 + 文字 + 脉动,让用户一眼就看到「立即发送」这条快捷路径。
|
|
1853
|
-
'<button id="interrupt-send-button" class="btn-pill btn-pill-interrupt hidden" type="button" title="中断当前回复并立即发送新输入(Cmd/Ctrl+Enter)" aria-label="立即发送">' +
|
|
1854
|
-
'<svg class="btn-pill-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="13 17 18 12 13 7"/><polyline points="6 17 11 12 6 7"/></svg>' +
|
|
1855
|
-
'<span class="btn-pill-label">立即</span>' +
|
|
1856
|
-
'</button>' +
|
|
1903
|
+
// 「立即发送」按钮已下线 —— 默认行为永远是排队(气泡),想插队
|
|
1904
|
+
// 请点输入框上方那条气泡(chip)。Cmd/Ctrl+Enter 快捷键仍保留。
|
|
1857
1905
|
'<button id="send-input-button" class="btn-circle btn-circle-send" title="发送">' +
|
|
1858
1906
|
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>' +
|
|
1859
1907
|
'</button>' +
|
|
@@ -3990,8 +4038,7 @@
|
|
|
3990
4038
|
cwdEl.title = cwd;
|
|
3991
4039
|
}
|
|
3992
4040
|
var url = "/api/directory?q=" + encodeURIComponent(cwd) +
|
|
3993
|
-
"&gitStatus=true"
|
|
3994
|
-
(state.fileExplorerShowHidden ? "&showHidden=true" : "");
|
|
4041
|
+
"&gitStatus=true";
|
|
3995
4042
|
fetch(url, { credentials: "same-origin" })
|
|
3996
4043
|
.then(function(res) {
|
|
3997
4044
|
if (!res.ok) throw new Error("Failed to load directory.");
|
|
@@ -4154,8 +4201,7 @@
|
|
|
4154
4201
|
var iconEl2 = item.querySelector(".tree-icon");
|
|
4155
4202
|
if (iconEl2) iconEl2.textContent = "📂";
|
|
4156
4203
|
var url = "/api/directory?q=" + encodeURIComponent(p) +
|
|
4157
|
-
"&gitStatus=true"
|
|
4158
|
-
(state.fileExplorerShowHidden ? "&showHidden=true" : "");
|
|
4204
|
+
"&gitStatus=true";
|
|
4159
4205
|
fetch(url, { credentials: "same-origin" })
|
|
4160
4206
|
.then(function(res) { return res.json(); })
|
|
4161
4207
|
.then(function(payload) {
|
|
@@ -4187,20 +4233,6 @@
|
|
|
4187
4233
|
refreshFileExplorer({ cwd: parent });
|
|
4188
4234
|
}
|
|
4189
4235
|
|
|
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
4236
|
function appendToComposer(text) {
|
|
4205
4237
|
var inputBox = document.getElementById("input-box");
|
|
4206
4238
|
if (!inputBox) return false;
|
|
@@ -5845,8 +5877,6 @@
|
|
|
5845
5877
|
if (drawerBackdrop) drawerBackdrop.addEventListener("click", closeSessionsDrawer);
|
|
5846
5878
|
var closeDrawerBtn = document.getElementById("close-drawer-button");
|
|
5847
5879
|
if (closeDrawerBtn) closeDrawerBtn.addEventListener("click", closeSessionsDrawer);
|
|
5848
|
-
var pinBtn = document.getElementById("sidebar-pin-btn");
|
|
5849
|
-
if (pinBtn) pinBtn.addEventListener("click", toggleSidebarPin);
|
|
5850
5880
|
var collapseBtn = document.getElementById("sidebar-collapse-btn");
|
|
5851
5881
|
if (collapseBtn) collapseBtn.addEventListener("click", toggleSidebarCollapsed);
|
|
5852
5882
|
var sidebarMoreBtn = document.getElementById("sidebar-more-btn");
|
|
@@ -6114,11 +6144,6 @@
|
|
|
6114
6144
|
closeSessionsDrawer();
|
|
6115
6145
|
sendOrStart();
|
|
6116
6146
|
});
|
|
6117
|
-
var interruptSendBtn = document.getElementById("interrupt-send-button");
|
|
6118
|
-
if (interruptSendBtn) interruptSendBtn.addEventListener("click", function() {
|
|
6119
|
-
closeSessionsDrawer();
|
|
6120
|
-
sendOrStart({ interrupt: true });
|
|
6121
|
-
});
|
|
6122
6147
|
var stopBtn = document.getElementById("stop-button");
|
|
6123
6148
|
if (stopBtn) stopBtn.addEventListener("click", stopSession);
|
|
6124
6149
|
var modeSelect = document.getElementById("chat-mode-select");
|
|
@@ -6390,8 +6415,6 @@
|
|
|
6390
6415
|
if (fileRefresh) fileRefresh.addEventListener("click", function() { refreshFileExplorer(); });
|
|
6391
6416
|
var fileUp = document.getElementById("file-explorer-up");
|
|
6392
6417
|
if (fileUp) fileUp.addEventListener("click", navigateExplorerUp);
|
|
6393
|
-
var fileToggleHidden = document.getElementById("file-explorer-toggle-hidden");
|
|
6394
|
-
if (fileToggleHidden) fileToggleHidden.addEventListener("click", toggleExplorerHidden);
|
|
6395
6418
|
|
|
6396
6419
|
// 路径输入框:支持点击修改路径,回车跳转,Esc 撤销。
|
|
6397
6420
|
var fileCwdInput = document.getElementById("file-explorer-cwd");
|
|
@@ -6840,14 +6863,8 @@
|
|
|
6840
6863
|
setupVisualViewportHandlers();
|
|
6841
6864
|
|
|
6842
6865
|
// 排队条:每次 shell 重渲后,重新挂事件代理 + 刷新内容。
|
|
6843
|
-
// document-level 的 ESC / 外点击 handler 只挂一次(state.__queueBarGlobalAttached 守门)。
|
|
6844
6866
|
attachQueueBarDelegates();
|
|
6845
6867
|
updateQueueBar();
|
|
6846
|
-
if (!state.__queueBarGlobalAttached) {
|
|
6847
|
-
state.__queueBarGlobalAttached = true;
|
|
6848
|
-
document.addEventListener("pointerdown", handleQueueBarOutsideClick, true);
|
|
6849
|
-
document.addEventListener("keydown", handleQueueBarKeydown, true);
|
|
6850
|
-
}
|
|
6851
6868
|
}
|
|
6852
6869
|
|
|
6853
6870
|
function saveWorkingDir(path) {
|
|
@@ -8189,14 +8206,17 @@
|
|
|
8189
8206
|
// Keep placeholders short so they don't wrap on portrait mobile screens.
|
|
8190
8207
|
// Only show informative state hints; drop the redundant "send to X" labels.
|
|
8191
8208
|
if (terminalInteractive) return "终端交互中";
|
|
8192
|
-
|
|
8209
|
+
// 只有真正进入终止态(exited / failed / stopped)才提示"会话已结束"。
|
|
8210
|
+
// 结构化会话刚创建或一次回复结束后会回到 "idle"——那是等待下一条输入的
|
|
8211
|
+
// 正常状态,不应该被当成结束。
|
|
8212
|
+
if (session && (session.status === "exited" || session.status === "failed" || session.status === "stopped")) {
|
|
8193
8213
|
if (canAutoResumeSession(session)) return "";
|
|
8194
8214
|
return "会话已结束";
|
|
8195
8215
|
}
|
|
8196
8216
|
// 结构化会话在出 token 时,输入框仍然可用——告诉用户默认行为是排队,
|
|
8197
|
-
//
|
|
8217
|
+
// 想插队请直接点上面的气泡。短语尽量短,避免在窄屏手机上换行。
|
|
8198
8218
|
if (isStructuredSession(session) && session.structuredState && session.structuredState.inFlight) {
|
|
8199
|
-
return "回复中…Enter 排队 ·
|
|
8219
|
+
return "回复中…Enter 排队 · 点气泡插队";
|
|
8200
8220
|
}
|
|
8201
8221
|
return "";
|
|
8202
8222
|
}
|
|
@@ -9296,7 +9316,6 @@
|
|
|
9296
9316
|
function updatePinState() {
|
|
9297
9317
|
var drawer = document.getElementById("sessions-drawer");
|
|
9298
9318
|
var mainLayout = document.querySelector(".main-layout");
|
|
9299
|
-
var pinBtn = document.getElementById("sidebar-pin-btn");
|
|
9300
9319
|
// 与 renderAppShell 保持一致:手机端只允许窄条形态 anchored。
|
|
9301
9320
|
var isMobile = isMobileLayout();
|
|
9302
9321
|
var isCollapsed = !!state.sidebarPinned && !!state.sidebarCollapsed;
|
|
@@ -9309,10 +9328,6 @@
|
|
|
9309
9328
|
mainLayout.classList.toggle("sidebar-pinned", isAnchored);
|
|
9310
9329
|
mainLayout.classList.toggle("sidebar-collapsed", isCollapsed);
|
|
9311
9330
|
}
|
|
9312
|
-
if (pinBtn) {
|
|
9313
|
-
pinBtn.classList.toggle("pinned", state.sidebarPinned);
|
|
9314
|
-
pinBtn.title = state.sidebarPinned ? "取消固定侧栏" : "固定侧栏";
|
|
9315
|
-
}
|
|
9316
9331
|
}
|
|
9317
9332
|
|
|
9318
9333
|
function updateDrawerState() {
|
|
@@ -9336,9 +9351,26 @@
|
|
|
9336
9351
|
}
|
|
9337
9352
|
|
|
9338
9353
|
function toggleSessionsDrawer() {
|
|
9339
|
-
|
|
9354
|
+
var isMobile = isMobileLayout();
|
|
9355
|
+
if (!isMobile) {
|
|
9356
|
+
// 桌面:呼出 = 常驻全尺寸;再次点击 = 完全收起(floating-toggle 重新出现)。
|
|
9357
|
+
// 取消了独立的「图钉」概念,sidebarPinned 现在由呼出/关闭自动管理。
|
|
9358
|
+
var willOpen = !state.sidebarPinned;
|
|
9359
|
+
state.sidebarPinned = willOpen;
|
|
9360
|
+
state.sessionsDrawerOpen = willOpen;
|
|
9361
|
+
if (willOpen) {
|
|
9362
|
+
// 桌面重新呼出默认回到全尺寸;窄条形态需用户主动点 collapse 按钮切换。
|
|
9363
|
+
state.sidebarCollapsed = false;
|
|
9364
|
+
try { localStorage.setItem("wand-sidebar-collapsed", "false"); } catch (e) {}
|
|
9365
|
+
}
|
|
9366
|
+
try { localStorage.setItem("wand-sidebar-pinned", String(willOpen)); } catch (e) {}
|
|
9367
|
+
updateLayoutState();
|
|
9368
|
+
scheduleTerminalRefitAfterPaddingTransition();
|
|
9369
|
+
return;
|
|
9370
|
+
}
|
|
9371
|
+
// 手机端:保持原 drawer 行为。
|
|
9340
9372
|
state.sessionsDrawerOpen = !state.sessionsDrawerOpen;
|
|
9341
|
-
if (state.sessionsDrawerOpen
|
|
9373
|
+
if (state.sessionsDrawerOpen) {
|
|
9342
9374
|
state.filePanelOpen = false;
|
|
9343
9375
|
try {
|
|
9344
9376
|
localStorage.setItem("wand-file-panel-open", "false");
|
|
@@ -9348,13 +9380,42 @@
|
|
|
9348
9380
|
}
|
|
9349
9381
|
|
|
9350
9382
|
function closeSessionsDrawer() {
|
|
9351
|
-
|
|
9383
|
+
var isMobile = isMobileLayout();
|
|
9384
|
+
if (!isMobile) {
|
|
9385
|
+
// 桌面:X 按钮 / backdrop 点击 = 完全收起,撤掉常驻状态,floating-toggle 重新出现。
|
|
9386
|
+
// 窄条状态下没有 X 按钮(CSS 隐藏),不会走到这里,因此无需特判 collapsed。
|
|
9387
|
+
if (!state.sidebarPinned && !state.sessionsDrawerOpen) return;
|
|
9388
|
+
closeSwipedItem();
|
|
9389
|
+
state.sidebarPinned = false;
|
|
9390
|
+
state.sessionsDrawerOpen = false;
|
|
9391
|
+
try { localStorage.setItem("wand-sidebar-pinned", "false"); } catch (e) {}
|
|
9392
|
+
updateLayoutState();
|
|
9393
|
+
scheduleTerminalRefitAfterPaddingTransition();
|
|
9394
|
+
return;
|
|
9395
|
+
}
|
|
9396
|
+
// 手机端:保持原 drawer 关闭行为。
|
|
9352
9397
|
if (!state.sessionsDrawerOpen) return;
|
|
9353
9398
|
closeSwipedItem();
|
|
9354
9399
|
state.sessionsDrawerOpen = false;
|
|
9355
9400
|
updateLayoutState();
|
|
9356
9401
|
}
|
|
9357
9402
|
|
|
9403
|
+
// 桌面 padding-left transition 结束后重新拟合终端尺寸。
|
|
9404
|
+
// 抽出来给 toggleSessionsDrawer / closeSessionsDrawer / toggleSidebarCollapsed 复用。
|
|
9405
|
+
function scheduleTerminalRefitAfterPaddingTransition() {
|
|
9406
|
+
var mainLayout = document.querySelector(".main-layout");
|
|
9407
|
+
if (mainLayout) {
|
|
9408
|
+
var onEnd = function(e) {
|
|
9409
|
+
if (e.propertyName === "padding-left") {
|
|
9410
|
+
mainLayout.removeEventListener("transitionend", onEnd);
|
|
9411
|
+
scheduleTerminalResize(true);
|
|
9412
|
+
}
|
|
9413
|
+
};
|
|
9414
|
+
mainLayout.addEventListener("transitionend", onEnd);
|
|
9415
|
+
}
|
|
9416
|
+
setTimeout(function() { scheduleTerminalResize(true); }, 350);
|
|
9417
|
+
}
|
|
9418
|
+
|
|
9358
9419
|
var collapsedTileBubbleEl = null;
|
|
9359
9420
|
function ensureCollapsedTileBubble() {
|
|
9360
9421
|
if (collapsedTileBubbleEl && document.body.contains(collapsedTileBubbleEl)) {
|
|
@@ -9420,8 +9481,7 @@
|
|
|
9420
9481
|
|
|
9421
9482
|
function toggleSidebarCollapsed() {
|
|
9422
9483
|
var isMobile = isMobileLayout();
|
|
9423
|
-
//
|
|
9424
|
-
// 用户直觉是「点了就该看到窄条」,过去这里 early return 让按钮看上去没反应。
|
|
9484
|
+
// 任何形态下点窄条按钮都意味着「我要常驻」,确保 pinned 写上。
|
|
9425
9485
|
if (!state.sidebarPinned) {
|
|
9426
9486
|
state.sidebarPinned = true;
|
|
9427
9487
|
try {
|
|
@@ -9445,47 +9505,13 @@
|
|
|
9445
9505
|
localStorage.setItem("wand-sidebar-pinned", "false");
|
|
9446
9506
|
} catch (e) {}
|
|
9447
9507
|
} else {
|
|
9448
|
-
// 桌面端展开窄条 → 300px
|
|
9508
|
+
// 桌面端展开窄条 → 300px 全栏常驻。
|
|
9449
9509
|
state.sessionsDrawerOpen = true;
|
|
9450
9510
|
}
|
|
9451
9511
|
render();
|
|
9452
|
-
|
|
9453
|
-
if (mainLayout) {
|
|
9454
|
-
var onEnd = function(e) {
|
|
9455
|
-
if (e.propertyName === "padding-left") {
|
|
9456
|
-
mainLayout.removeEventListener("transitionend", onEnd);
|
|
9457
|
-
scheduleTerminalResize(true);
|
|
9458
|
-
}
|
|
9459
|
-
};
|
|
9460
|
-
mainLayout.addEventListener("transitionend", onEnd);
|
|
9461
|
-
}
|
|
9462
|
-
setTimeout(function() { scheduleTerminalResize(true); }, 350);
|
|
9512
|
+
scheduleTerminalRefitAfterPaddingTransition();
|
|
9463
9513
|
}
|
|
9464
9514
|
|
|
9465
|
-
function toggleSidebarPin() {
|
|
9466
|
-
if (isMobileLayout()) return;
|
|
9467
|
-
state.sidebarPinned = !state.sidebarPinned;
|
|
9468
|
-
try {
|
|
9469
|
-
localStorage.setItem("wand-sidebar-pinned", String(state.sidebarPinned));
|
|
9470
|
-
} catch (e) {}
|
|
9471
|
-
if (state.sidebarPinned) {
|
|
9472
|
-
state.sessionsDrawerOpen = true;
|
|
9473
|
-
}
|
|
9474
|
-
updateLayoutState();
|
|
9475
|
-
// Refit terminal after padding-left transition completes
|
|
9476
|
-
var mainLayout = document.querySelector(".main-layout");
|
|
9477
|
-
if (mainLayout) {
|
|
9478
|
-
var onEnd = function(e) {
|
|
9479
|
-
if (e.propertyName === "padding-left") {
|
|
9480
|
-
mainLayout.removeEventListener("transitionend", onEnd);
|
|
9481
|
-
scheduleTerminalResize(true);
|
|
9482
|
-
}
|
|
9483
|
-
};
|
|
9484
|
-
mainLayout.addEventListener("transitionend", onEnd);
|
|
9485
|
-
}
|
|
9486
|
-
// Fallback refit in case transition doesn't fire
|
|
9487
|
-
setTimeout(function() { scheduleTerminalResize(true); }, 350);
|
|
9488
|
-
}
|
|
9489
9515
|
|
|
9490
9516
|
// Store last focused element for focus trap
|
|
9491
9517
|
var lastFocusedElement = null;
|
|
@@ -12287,9 +12313,10 @@
|
|
|
12287
12313
|
|
|
12288
12314
|
function postStructuredInput(input, inputBox, session, opts) {
|
|
12289
12315
|
opts = opts || {};
|
|
12290
|
-
//
|
|
12291
|
-
//
|
|
12292
|
-
//
|
|
12316
|
+
// interrupt:true 现在只来自 Cmd/Ctrl+Enter 快捷键,或点队列气泡触发的
|
|
12317
|
+
// queueBarPromoteIndex()。普通 Enter / 点发送在上一条还在流式时默认走
|
|
12318
|
+
// queue —— 后端 sendMessage(...) 会把它追加到 queuedMessages,等当前 turn
|
|
12319
|
+
// 结束自动 flush;想插队就点输入框上方那条气泡。
|
|
12293
12320
|
var requestedInterrupt = !!opts.interrupt;
|
|
12294
12321
|
console.log("[WAND] postStructuredInput selectedId:", state.selectedId, "input:", input && input.substring(0, 50), "requestedInterrupt:", requestedInterrupt, "session:", session && { id: session.id, sessionKind: session.sessionKind, runner: session.runner, status: session.status, inFlight: session.structuredState && session.structuredState.inFlight });
|
|
12295
12322
|
if (!state.selectedId || !input) return Promise.resolve();
|
|
@@ -12327,7 +12354,7 @@
|
|
|
12327
12354
|
updateSessionSnapshot(optimisticPatch);
|
|
12328
12355
|
var queueRefreshed = state.sessions.find(function(s) { return s.id === session.id; }) || session;
|
|
12329
12356
|
state.currentMessages = buildMessagesForRender(queueRefreshed, getPreferredMessages(queueRefreshed, queueRefreshed.output, false));
|
|
12330
|
-
updateInputHint("
|
|
12357
|
+
updateInputHint("已加入排队…");
|
|
12331
12358
|
renderChat(true);
|
|
12332
12359
|
updateStructuredQueueCounter();
|
|
12333
12360
|
// 乐观 toast:原本只在 POST 完成后才提示,Claude 流式拖太久时用户根本
|
|
@@ -12483,104 +12510,80 @@
|
|
|
12483
12510
|
}
|
|
12484
12511
|
|
|
12485
12512
|
// ──────────────────────────────────────────────────────────────────────────
|
|
12486
|
-
//
|
|
12487
|
-
//
|
|
12488
|
-
//
|
|
12489
|
-
//
|
|
12490
|
-
//
|
|
12513
|
+
// 排队气泡条(.queue-bar)—— 垂直堆叠,浮在 "回复中" 状态条上方。
|
|
12514
|
+
// 交互参考 iOS 通讯录右侧的字母选择条:
|
|
12515
|
+
// · 默认只展开队首(即下一个要发的那条),显示编号 + 文本 + × 删除
|
|
12516
|
+
// · 其他消息收起成一根小横杠(指示存在但不占空间)
|
|
12517
|
+
// · 鼠标悬到任意小横杠 → 该条展开、原本展开的那条收回小横杠
|
|
12518
|
+
// · 点一下任意气泡 → 中断当前回复、把这条作为新输入插队发出去
|
|
12519
|
+
// · 按住任意气泡向上 / 向下拖拽 → 换序
|
|
12520
|
+
// 末尾跟一个 ⚡ 按钮:等价于点队首气泡(保留作为快速插队的视觉提示)。
|
|
12521
|
+
// 数据源:session.queuedMessages(后端 WS + postStructuredInput 乐观更新)。
|
|
12491
12522
|
// ──────────────────────────────────────────────────────────────────────────
|
|
12492
12523
|
|
|
12493
|
-
var QUEUE_BAR_MAX = 10;
|
|
12524
|
+
var QUEUE_BAR_MAX = 10; // 后端硬上限
|
|
12525
|
+
var QUEUE_CHIP_MAX_TEXT = 24; // 单个气泡展开时显示的字数上限
|
|
12494
12526
|
|
|
12495
|
-
function
|
|
12527
|
+
function queueChipTruncate(text) {
|
|
12496
12528
|
if (typeof text !== "string") return "";
|
|
12497
12529
|
var s = text.replace(/\s+/g, " ").trim();
|
|
12498
|
-
if (s.length <=
|
|
12499
|
-
return s.slice(0,
|
|
12530
|
+
if (s.length <= QUEUE_CHIP_MAX_TEXT) return s;
|
|
12531
|
+
return s.slice(0, QUEUE_CHIP_MAX_TEXT) + "…";
|
|
12500
12532
|
}
|
|
12501
12533
|
|
|
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;
|
|
12534
|
+
// 当前应该展开的下标:拖拽中 → 被拖的那条(data-index 不变);hover → 被 hover 的;否则 → 第 0 项
|
|
12535
|
+
function queueBarExpandedIndex(itemsLength) {
|
|
12536
|
+
if (state.queueBarDrag && typeof state.queueBarDrag.origIndex === "number") {
|
|
12537
|
+
return state.queueBarDrag.origIndex;
|
|
12538
|
+
}
|
|
12539
|
+
if (typeof state.queueBarHoverIndex === "number"
|
|
12540
|
+
&& state.queueBarHoverIndex >= 0
|
|
12541
|
+
&& state.queueBarHoverIndex < itemsLength) {
|
|
12542
|
+
return state.queueBarHoverIndex;
|
|
12543
|
+
}
|
|
12544
|
+
return 0;
|
|
12547
12545
|
}
|
|
12548
12546
|
|
|
12549
|
-
function
|
|
12550
|
-
// ol 内容单独 render —— 拖拽 / 删除 / 展开会频繁动它,外层骨架不重建避免抖动。
|
|
12547
|
+
function renderQueueBarHtml(items, inFlight, atCapacity, immediateLabel) {
|
|
12551
12548
|
var single = items.length <= 1;
|
|
12552
|
-
var
|
|
12549
|
+
var barClass = "queue-bar";
|
|
12550
|
+
if (atCapacity) barClass += " queue-bar-capacity";
|
|
12551
|
+
if (inFlight) barClass += " queue-bar-inflight";
|
|
12552
|
+
var expandedIdx = queueBarExpandedIndex(items.length);
|
|
12553
|
+
var chips = "";
|
|
12553
12554
|
for (var i = 0; i < items.length; i++) {
|
|
12554
12555
|
var raw = items[i] == null ? "" : String(items[i]);
|
|
12555
|
-
var
|
|
12556
|
+
var isExpanded = i === expandedIdx;
|
|
12556
12557
|
var itemClass = "queue-bar-item";
|
|
12557
|
-
if (
|
|
12558
|
+
if (isExpanded) itemClass += " expanded";
|
|
12558
12559
|
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>' +
|
|
12560
|
+
// 整个 chip 既是拖拽起手区,也是"点一下立即发送"的触发点;delete 按钮单独占点击。
|
|
12561
|
+
var titleAttr = isExpanded ? raw + "(点一下立即发送 · 按住可拖动调序)" : raw + "(点一下立即发送)";
|
|
12562
|
+
chips +=
|
|
12563
|
+
'<li class="' + itemClass + '" data-index="' + i + '" data-action="drag"' +
|
|
12564
|
+
' title="' + escapeHtml(titleAttr) + '">' +
|
|
12565
|
+
'<span class="queue-bar-item-index" aria-hidden="true">' + (i + 1) + '</span>' +
|
|
12566
|
+
'<span class="queue-bar-item-text">' + escapeHtml(queueChipTruncate(raw)) + '</span>' +
|
|
12575
12567
|
'<button type="button" class="queue-bar-item-delete" data-action="delete"' +
|
|
12576
|
-
|
|
12577
|
-
'<svg width="
|
|
12578
|
-
' stroke-width="
|
|
12568
|
+
' aria-label="删除这条排队消息" title="删除" tabindex="' + (isExpanded ? "0" : "-1") + '">' +
|
|
12569
|
+
'<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor"' +
|
|
12570
|
+
' stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
|
|
12579
12571
|
'<line x1="6" y1="6" x2="18" y2="18"/><line x1="6" y1="18" x2="18" y2="6"/></svg>' +
|
|
12580
12572
|
'</button>' +
|
|
12581
12573
|
'</li>';
|
|
12582
12574
|
}
|
|
12583
|
-
|
|
12575
|
+
return (
|
|
12576
|
+
'<div class="' + barClass + '" data-queue-bar="1">' +
|
|
12577
|
+
'<ol class="queue-bar-list" data-queue-list="1">' + chips + '</ol>' +
|
|
12578
|
+
'<button type="button" class="queue-bar-promote" data-action="promote"' +
|
|
12579
|
+
' title="中断当前回复,立刻发送队首这条"' +
|
|
12580
|
+
' aria-label="' + escapeHtml(immediateLabel) + '队首">' +
|
|
12581
|
+
'<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">' +
|
|
12582
|
+
'<path d="M13 2 L4 14 L11 14 L10 22 L20 9 L13 9 Z"/>' +
|
|
12583
|
+
'</svg>' +
|
|
12584
|
+
'</button>' +
|
|
12585
|
+
'</div>'
|
|
12586
|
+
);
|
|
12584
12587
|
}
|
|
12585
12588
|
|
|
12586
12589
|
function updateQueueBar() {
|
|
@@ -12592,64 +12595,50 @@
|
|
|
12592
12595
|
queue = Array.isArray(queue) ? queue : [];
|
|
12593
12596
|
|
|
12594
12597
|
if (!isStructured || queue.length === 0) {
|
|
12595
|
-
// 队列空 / 非结构化会话:整条隐藏,并清掉展开/逐条展开的本地态。
|
|
12596
12598
|
host.hidden = true;
|
|
12597
12599
|
host.innerHTML = "";
|
|
12598
|
-
state.
|
|
12599
|
-
state.queueBarItemExpanded = {};
|
|
12600
|
+
state.queueBarHoverIndex = null;
|
|
12600
12601
|
return;
|
|
12601
12602
|
}
|
|
12602
12603
|
|
|
12604
|
+
// 拖拽进行中绝不重建 DOM,否则 pointer capture 丢失、气泡闪屏。
|
|
12605
|
+
if (state.queueBarDrag) return;
|
|
12606
|
+
|
|
12603
12607
|
host.hidden = false;
|
|
12604
12608
|
var inFlight = !!(session.structuredState && session.structuredState.inFlight && session.status === "running");
|
|
12605
12609
|
var atCapacity = queue.length >= QUEUE_BAR_MAX;
|
|
12606
|
-
var latest = queueBarTruncatePreview(queue[queue.length - 1]);
|
|
12607
|
-
// inFlight=false 时按钮语义从"插队"退化为"立刻发";文案一并切换让用户不疑惑。
|
|
12608
12610
|
var immediateLabel = inFlight ? "立即" : "发送";
|
|
12609
12611
|
|
|
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);
|
|
12612
|
+
host.innerHTML = renderQueueBarHtml(queue, inFlight, atCapacity, immediateLabel);
|
|
12624
12613
|
}
|
|
12625
12614
|
|
|
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;
|
|
12615
|
+
// 只切换 .expanded class,不重建 DOM —— 避免鼠标移过去触发的重建
|
|
12616
|
+
// 让拖拽/输入框焦点等丢失。所有同步状态(hoverIndex / drag)的改变都通过这里反映到 DOM。
|
|
12617
|
+
function reflectQueueBarExpansion() {
|
|
12638
12618
|
var host = document.getElementById("queue-bar-host");
|
|
12639
|
-
if (!host) return;
|
|
12640
|
-
|
|
12641
|
-
|
|
12642
|
-
|
|
12643
|
-
|
|
12644
|
-
|
|
12645
|
-
|
|
12646
|
-
|
|
12647
|
-
|
|
12648
|
-
|
|
12649
|
-
|
|
12619
|
+
if (!host || host.hidden) return;
|
|
12620
|
+
var list = host.querySelector('[data-queue-list="1"]');
|
|
12621
|
+
if (!list) return;
|
|
12622
|
+
var children = list.children;
|
|
12623
|
+
var expandedIdx = queueBarExpandedIndex(children.length);
|
|
12624
|
+
for (var i = 0; i < children.length; i++) {
|
|
12625
|
+
var el = children[i];
|
|
12626
|
+
var should = i === expandedIdx;
|
|
12627
|
+
if (el.classList.contains("expanded") !== should) {
|
|
12628
|
+
el.classList.toggle("expanded", should);
|
|
12629
|
+
var del = el.querySelector('.queue-bar-item-delete');
|
|
12630
|
+
if (del) del.tabIndex = should ? 0 : -1;
|
|
12631
|
+
}
|
|
12650
12632
|
}
|
|
12651
12633
|
}
|
|
12652
12634
|
|
|
12635
|
+
function setQueueBarHoverIndex(idx) {
|
|
12636
|
+
var next = (idx == null ? null : Number(idx));
|
|
12637
|
+
if (state.queueBarHoverIndex === next) return;
|
|
12638
|
+
state.queueBarHoverIndex = next;
|
|
12639
|
+
reflectQueueBarExpansion();
|
|
12640
|
+
}
|
|
12641
|
+
|
|
12653
12642
|
// ── 单条删除 / 全部清空 / 队首插队 ──
|
|
12654
12643
|
function rollbackQueueOptimistic(session, prevQueue) {
|
|
12655
12644
|
updateSessionSnapshot({ id: session.id, queuedMessages: prevQueue });
|
|
@@ -12666,15 +12655,11 @@
|
|
|
12666
12655
|
if (index < 0 || index >= queue.length) return;
|
|
12667
12656
|
var prev = queue.slice();
|
|
12668
12657
|
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;
|
|
12658
|
+
// hover 下标也要随之收缩,否则删完后展开的是错位的那条
|
|
12659
|
+
if (typeof state.queueBarHoverIndex === "number") {
|
|
12660
|
+
if (state.queueBarHoverIndex === index) state.queueBarHoverIndex = null;
|
|
12661
|
+
else if (state.queueBarHoverIndex > index) state.queueBarHoverIndex -= 1;
|
|
12662
|
+
}
|
|
12678
12663
|
updateSessionSnapshot({ id: session.id, queuedMessages: next });
|
|
12679
12664
|
var refreshed = state.sessions.find(function(s) { return s.id === session.id; }) || session;
|
|
12680
12665
|
state.currentMessages = buildMessagesForRender(refreshed, getPreferredMessages(refreshed, refreshed.output, false));
|
|
@@ -12702,7 +12687,7 @@
|
|
|
12702
12687
|
if (!session) return;
|
|
12703
12688
|
var prev = Array.isArray(session.queuedMessages) ? session.queuedMessages.slice() : [];
|
|
12704
12689
|
if (prev.length === 0) return;
|
|
12705
|
-
state.
|
|
12690
|
+
state.queueBarHoverIndex = null;
|
|
12706
12691
|
updateSessionSnapshot({ id: session.id, queuedMessages: [] });
|
|
12707
12692
|
var refreshed = state.sessions.find(function(s) { return s.id === session.id; }) || session;
|
|
12708
12693
|
state.currentMessages = buildMessagesForRender(refreshed, getPreferredMessages(refreshed, refreshed.output, false));
|
|
@@ -12727,42 +12712,42 @@
|
|
|
12727
12712
|
}
|
|
12728
12713
|
|
|
12729
12714
|
function queueBarPromoteHead() {
|
|
12715
|
+
queueBarPromoteIndex(0);
|
|
12716
|
+
}
|
|
12717
|
+
|
|
12718
|
+
// 把队列里第 index 条剥下来,作为新的输入立刻发送出去。
|
|
12719
|
+
// - inFlight:interrupt + preserveQueue(中断当前回复,保留其它排队)
|
|
12720
|
+
// - 非 inFlight:当作普通新消息发出去
|
|
12721
|
+
// 用户路径:点输入框上方的气泡(chip)→ 这里。
|
|
12722
|
+
function queueBarPromoteIndex(index) {
|
|
12730
12723
|
var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
12731
12724
|
if (!session) return;
|
|
12732
12725
|
var queue = Array.isArray(session.queuedMessages) ? session.queuedMessages.slice() : [];
|
|
12733
|
-
if (queue.length
|
|
12734
|
-
var
|
|
12735
|
-
var rest = queue.slice(1);
|
|
12726
|
+
if (index < 0 || index >= queue.length) return;
|
|
12727
|
+
var picked = queue[index];
|
|
12728
|
+
var rest = queue.slice(0, index).concat(queue.slice(index + 1));
|
|
12736
12729
|
var prev = queue.slice();
|
|
12737
12730
|
var inFlight = !!(session.structuredState && session.structuredState.inFlight && session.status === "running");
|
|
12738
12731
|
|
|
12739
|
-
//
|
|
12740
|
-
state.
|
|
12741
|
-
|
|
12742
|
-
|
|
12743
|
-
|
|
12744
|
-
if (i === 0) return;
|
|
12745
|
-
out[i - 1] = state.queueBarItemExpanded[k];
|
|
12746
|
-
});
|
|
12747
|
-
return out;
|
|
12748
|
-
})();
|
|
12732
|
+
// 乐观:剥掉这一条;hover 下标随之收缩
|
|
12733
|
+
if (typeof state.queueBarHoverIndex === "number") {
|
|
12734
|
+
if (state.queueBarHoverIndex === index) state.queueBarHoverIndex = null;
|
|
12735
|
+
else if (state.queueBarHoverIndex > index) state.queueBarHoverIndex -= 1;
|
|
12736
|
+
}
|
|
12749
12737
|
updateSessionSnapshot({ id: session.id, queuedMessages: rest });
|
|
12750
12738
|
|
|
12751
|
-
// 收起面板,让用户视线回到 chat(新消息马上要进 user turn)
|
|
12752
|
-
setQueueBarExpanded(false);
|
|
12753
|
-
|
|
12754
12739
|
var idempotencyKey = (typeof crypto !== "undefined" && crypto.randomUUID)
|
|
12755
12740
|
? crypto.randomUUID()
|
|
12756
12741
|
: (Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 10));
|
|
12757
12742
|
|
|
12758
|
-
var body = { input:
|
|
12743
|
+
var body = { input: picked, idempotencyKey: idempotencyKey };
|
|
12759
12744
|
if (inFlight) {
|
|
12760
12745
|
// 中断 + 保留剩余队列
|
|
12761
12746
|
body.interrupt = true;
|
|
12762
12747
|
body.preserveQueue = true;
|
|
12763
12748
|
}
|
|
12764
12749
|
// 给一个乐观 toast,让用户瞬间知道点击生效了
|
|
12765
|
-
showToast(inFlight ? "
|
|
12750
|
+
showToast(inFlight ? "已请求中断当前回复,立即发送这条。" : "已立即发送这条消息。", "info");
|
|
12766
12751
|
|
|
12767
12752
|
fetch("/api/structured-sessions/" + session.id + "/messages", {
|
|
12768
12753
|
method: "POST",
|
|
@@ -12795,53 +12780,82 @@
|
|
|
12795
12780
|
});
|
|
12796
12781
|
}
|
|
12797
12782
|
|
|
12798
|
-
// ── 拖拽排序(Pointer Events +
|
|
12799
|
-
|
|
12783
|
+
// ── 拖拽排序(Pointer Events + 真实高度的 sort/animate)──
|
|
12784
|
+
// 单条气泡的 pointerdown 也会进这里,但 queue.length <= 1 时直接返回,让
|
|
12785
|
+
// 系统 click 事件穿透到 #queue-bar-host 的 click delegate(那里再判断"点击
|
|
12786
|
+
// 气泡 → 立即发送")。
|
|
12787
|
+
function queueBarDragStart(ev, chipEl) {
|
|
12800
12788
|
var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
12801
12789
|
if (!session) return;
|
|
12802
12790
|
var queue = Array.isArray(session.queuedMessages) ? session.queuedMessages.slice() : [];
|
|
12803
12791
|
if (queue.length <= 1) return;
|
|
12804
|
-
|
|
12805
|
-
|
|
12806
|
-
var listEl = itemEl.parentElement;
|
|
12792
|
+
if (!chipEl) return;
|
|
12793
|
+
var listEl = chipEl.parentElement;
|
|
12807
12794
|
if (!listEl) return;
|
|
12808
|
-
var origIndex = Number(
|
|
12795
|
+
var origIndex = Number(chipEl.getAttribute("data-index"));
|
|
12809
12796
|
var siblings = Array.prototype.slice.call(listEl.children);
|
|
12810
12797
|
var rects = siblings.map(function(el) { return el.getBoundingClientRect(); });
|
|
12811
|
-
|
|
12812
|
-
var
|
|
12813
|
-
|
|
12798
|
+
// 真实间距:相邻两个 chip 的 top 差减去前一个高度(容错 hover 状态变化后的高度切换)
|
|
12799
|
+
var gap = 3;
|
|
12800
|
+
if (rects.length >= 2) gap = Math.max(0, rects[1].top - rects[0].top - rects[0].height);
|
|
12814
12801
|
|
|
12815
12802
|
ev.preventDefault();
|
|
12816
|
-
try {
|
|
12803
|
+
try { chipEl.setPointerCapture(ev.pointerId); } catch (_e) {}
|
|
12817
12804
|
if (navigator && navigator.vibrate) { try { navigator.vibrate(8); } catch (_e2) {} }
|
|
12818
12805
|
|
|
12819
12806
|
state.queueBarDrag = {
|
|
12820
12807
|
pointerId: ev.pointerId,
|
|
12821
|
-
handleEl:
|
|
12822
|
-
itemEl:
|
|
12808
|
+
handleEl: chipEl,
|
|
12809
|
+
itemEl: chipEl,
|
|
12823
12810
|
listEl: listEl,
|
|
12824
12811
|
siblings: siblings,
|
|
12825
12812
|
rects: rects,
|
|
12826
12813
|
origIndex: origIndex,
|
|
12827
12814
|
targetIndex: origIndex,
|
|
12828
12815
|
startY: ev.clientY,
|
|
12829
|
-
itemHeight: itemHeight,
|
|
12830
12816
|
gap: gap,
|
|
12831
12817
|
queueSnapshot: queue,
|
|
12818
|
+
moved: false, // 没真正拖动过 → 抬手时按 tap 处理:promote 这条
|
|
12832
12819
|
};
|
|
12833
12820
|
|
|
12834
|
-
|
|
12821
|
+
chipEl.classList.add("dragging");
|
|
12822
|
+
// 让被拖元素保持 expanded(即便鼠标已经离开它)
|
|
12823
|
+
reflectQueueBarExpansion();
|
|
12835
12824
|
// 把所有兄弟先标记为"参与平滑动画"
|
|
12836
|
-
siblings.forEach(function(el) { if (el !==
|
|
12825
|
+
siblings.forEach(function(el) { if (el !== chipEl) el.classList.add("queue-bar-item-sliding"); });
|
|
12837
12826
|
|
|
12838
12827
|
var move = function(e) { queueBarDragMove(e); };
|
|
12839
12828
|
var up = function(e) { queueBarDragEnd(e); };
|
|
12840
12829
|
state.queueBarDrag.moveHandler = move;
|
|
12841
12830
|
state.queueBarDrag.upHandler = up;
|
|
12842
|
-
|
|
12843
|
-
|
|
12844
|
-
|
|
12831
|
+
chipEl.addEventListener("pointermove", move);
|
|
12832
|
+
chipEl.addEventListener("pointerup", up);
|
|
12833
|
+
chipEl.addEventListener("pointercancel", up);
|
|
12834
|
+
}
|
|
12835
|
+
|
|
12836
|
+
// 给定 origIndex / target / 真实 rects,算出新排列下每个 sibling 的目标 top。
|
|
12837
|
+
// 用真实高度而不是固定 shift,因为 expanded chip 比 collapsed 高很多。
|
|
12838
|
+
function queueBarComputeNewTops(origIndex, target, rects, gap) {
|
|
12839
|
+
var n = rects.length;
|
|
12840
|
+
var order = [];
|
|
12841
|
+
for (var i = 0; i < n; i++) order.push(i);
|
|
12842
|
+
order.splice(origIndex, 1);
|
|
12843
|
+
order.splice(target, 0, origIndex);
|
|
12844
|
+
var top = rects[0].top;
|
|
12845
|
+
// list 是右对齐 column flex,所有元素相对 list 左边对齐 — 我们只关心 top
|
|
12846
|
+
// 用第一个 rect 的 top 作为锚点累加。
|
|
12847
|
+
// 但 list 起始位置不一定是 rects[0].top(rects[0] 现在变到 order[0] 的位置)
|
|
12848
|
+
// 这里需要找原本的 list top —— 取 rects 里最小 top 即可。
|
|
12849
|
+
var listTop = rects[0].top;
|
|
12850
|
+
for (var k = 1; k < n; k++) if (rects[k].top < listTop) listTop = rects[k].top;
|
|
12851
|
+
var newTops = {};
|
|
12852
|
+
var cursor = listTop;
|
|
12853
|
+
for (var newPos = 0; newPos < n; newPos++) {
|
|
12854
|
+
var oldIdx = order[newPos];
|
|
12855
|
+
newTops[oldIdx] = cursor;
|
|
12856
|
+
cursor += rects[oldIdx].height + gap;
|
|
12857
|
+
}
|
|
12858
|
+
return newTops;
|
|
12845
12859
|
}
|
|
12846
12860
|
|
|
12847
12861
|
function queueBarDragMove(ev) {
|
|
@@ -12849,6 +12863,8 @@
|
|
|
12849
12863
|
if (!d || ev.pointerId !== d.pointerId) return;
|
|
12850
12864
|
ev.preventDefault();
|
|
12851
12865
|
var deltaY = ev.clientY - d.startY;
|
|
12866
|
+
// 4px 阈值过滤抖动 / 触屏轻微滑动;超过才算"真的在拖",否则抬手当 tap。
|
|
12867
|
+
if (Math.abs(deltaY) > 4) d.moved = true;
|
|
12852
12868
|
d.itemEl.style.transform = "translateY(" + deltaY + "px)";
|
|
12853
12869
|
|
|
12854
12870
|
// 拖动中心 Y 决定目标插入位置
|
|
@@ -12862,13 +12878,11 @@
|
|
|
12862
12878
|
}
|
|
12863
12879
|
if (target !== d.targetIndex) {
|
|
12864
12880
|
d.targetIndex = target;
|
|
12865
|
-
//
|
|
12866
|
-
var
|
|
12881
|
+
// 按真实高度精确算每个 sibling 的新 top
|
|
12882
|
+
var newTops = queueBarComputeNewTops(d.origIndex, target, d.rects, d.gap);
|
|
12867
12883
|
d.siblings.forEach(function(el, idx) {
|
|
12868
12884
|
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;
|
|
12885
|
+
var move = newTops[idx] - d.rects[idx].top;
|
|
12872
12886
|
el.style.transform = move ? "translateY(" + move + "px)" : "";
|
|
12873
12887
|
});
|
|
12874
12888
|
}
|
|
@@ -12885,6 +12899,7 @@
|
|
|
12885
12899
|
var origIndex = d.origIndex;
|
|
12886
12900
|
var targetIndex = d.targetIndex;
|
|
12887
12901
|
var queueSnapshot = d.queueSnapshot;
|
|
12902
|
+
var wasTap = !d.moved;
|
|
12888
12903
|
|
|
12889
12904
|
// 清掉 inline transform 让 CSS 自然回位
|
|
12890
12905
|
d.siblings.forEach(function(el) {
|
|
@@ -12896,8 +12911,9 @@
|
|
|
12896
12911
|
state.queueBarDrag = null;
|
|
12897
12912
|
|
|
12898
12913
|
if (origIndex === targetIndex) {
|
|
12899
|
-
//
|
|
12914
|
+
// 没真的移动过 → 按 tap 处理:把这条剥下来插队发送。
|
|
12900
12915
|
updateQueueBar();
|
|
12916
|
+
if (wasTap) queueBarPromoteIndex(origIndex);
|
|
12901
12917
|
return;
|
|
12902
12918
|
}
|
|
12903
12919
|
|
|
@@ -12908,14 +12924,8 @@
|
|
|
12908
12924
|
order.splice(targetIndex, 0, origIndex);
|
|
12909
12925
|
var nextQueue = order.map(function(i) { return queueSnapshot[i]; });
|
|
12910
12926
|
|
|
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;
|
|
12927
|
+
// hover 下标迁移到新位置(拖拽放手时鼠标停在 targetIndex 上)
|
|
12928
|
+
state.queueBarHoverIndex = targetIndex;
|
|
12919
12929
|
|
|
12920
12930
|
var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
12921
12931
|
if (!session) { updateQueueBar(); return; }
|
|
@@ -12950,33 +12960,45 @@
|
|
|
12950
12960
|
var actionEl = ev.target && ev.target.closest ? ev.target.closest("[data-action]") : null;
|
|
12951
12961
|
if (!actionEl || !host.contains(actionEl)) return;
|
|
12952
12962
|
var action = actionEl.getAttribute("data-action");
|
|
12953
|
-
if (action === "drag")
|
|
12963
|
+
if (action === "drag") {
|
|
12964
|
+
// queue.length > 1 时 pointerdown 已经 preventDefault → click 不会到这;
|
|
12965
|
+
// queue.length === 1 时 drag-start 早退、click 会落到这里:当成 tap,
|
|
12966
|
+
// 把这条直接 promote 出去。
|
|
12967
|
+
ev.preventDefault();
|
|
12968
|
+
ev.stopPropagation();
|
|
12969
|
+
var idx = Number(actionEl.getAttribute("data-index"));
|
|
12970
|
+
queueBarPromoteIndex(idx);
|
|
12971
|
+
return;
|
|
12972
|
+
}
|
|
12954
12973
|
ev.preventDefault();
|
|
12955
12974
|
ev.stopPropagation();
|
|
12956
|
-
if (action === "toggle") { toggleQueueBar(); return; }
|
|
12957
|
-
if (action === "collapse") { setQueueBarExpanded(false); return; }
|
|
12958
12975
|
if (action === "promote") { queueBarPromoteHead(); return; }
|
|
12959
|
-
if (action === "clear") { queueBarClearAll(); return; }
|
|
12960
12976
|
if (action === "delete") {
|
|
12961
12977
|
var itemEl = actionEl.closest(".queue-bar-item");
|
|
12962
12978
|
if (itemEl) queueBarDeleteItem(Number(itemEl.getAttribute("data-index")));
|
|
12963
12979
|
return;
|
|
12964
12980
|
}
|
|
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
12981
|
});
|
|
12982
|
+
// hover 跟随:鼠标移到哪一条,哪一条就展开(拖拽进行中不响应,免得抢拖拽)
|
|
12983
|
+
host.addEventListener("mouseover", function(ev) {
|
|
12984
|
+
if (state.queueBarDrag) return;
|
|
12985
|
+
var chip = ev.target && ev.target.closest ? ev.target.closest(".queue-bar-item") : null;
|
|
12986
|
+
if (!chip || !host.contains(chip)) return;
|
|
12987
|
+
setQueueBarHoverIndex(Number(chip.getAttribute("data-index")));
|
|
12988
|
+
});
|
|
12989
|
+
host.addEventListener("mouseleave", function() {
|
|
12990
|
+
if (state.queueBarDrag) return;
|
|
12991
|
+
setQueueBarHoverIndex(null);
|
|
12992
|
+
});
|
|
12993
|
+
// 整个气泡都是拖拽起手区。delete / promote 按钮通过 closest 检查跳过
|
|
12975
12994
|
host.addEventListener("pointerdown", function(ev) {
|
|
12976
12995
|
if (ev.button !== undefined && ev.button !== 0) return;
|
|
12977
|
-
|
|
12978
|
-
|
|
12979
|
-
|
|
12996
|
+
if (ev.target && ev.target.closest && ev.target.closest('[data-action="delete"], [data-action="promote"]')) return;
|
|
12997
|
+
var chip = ev.target && ev.target.closest ? ev.target.closest('.queue-bar-item') : null;
|
|
12998
|
+
if (!chip) return;
|
|
12999
|
+
// 拖拽前先把这条切到 expanded(鼠标按下时通常已经 hovered,但触屏没 hover)
|
|
13000
|
+
setQueueBarHoverIndex(Number(chip.getAttribute("data-index")));
|
|
13001
|
+
queueBarDragStart(ev, chip);
|
|
12980
13002
|
});
|
|
12981
13003
|
}
|
|
12982
13004
|
|
|
@@ -13563,12 +13585,6 @@
|
|
|
13563
13585
|
: (isCodex ? (isRunning ? "发送给 Codex" : "Codex 会话已结束") : (!selectedSession || isRunning || canResumeOnSend ? "发送" : "会话已结束")));
|
|
13564
13586
|
sendBtn.classList.toggle("queue-mode", structuredInFlight);
|
|
13565
13587
|
}
|
|
13566
|
-
var interruptBtn = document.getElementById("interrupt-send-button");
|
|
13567
|
-
if (interruptBtn) {
|
|
13568
|
-
// 仅结构化 + inFlight 时显示。pty 会话有自己的 Ctrl+C / stop 按钮,
|
|
13569
|
-
// 用不上这套语义。
|
|
13570
|
-
interruptBtn.classList.toggle("hidden", !structuredInFlight);
|
|
13571
|
-
}
|
|
13572
13588
|
var container = document.getElementById("output");
|
|
13573
13589
|
if (container) container.classList.toggle("interactive", !structured && state.terminalInteractive);
|
|
13574
13590
|
}
|
|
@@ -17054,24 +17070,13 @@
|
|
|
17054
17070
|
return;
|
|
17055
17071
|
}
|
|
17056
17072
|
|
|
17057
|
-
//
|
|
17058
|
-
//
|
|
17059
|
-
//
|
|
17060
|
-
|
|
17061
|
-
|
|
17062
|
-
|
|
17063
|
-
|
|
17064
|
-
turnDone = !(sel.structuredState && sel.structuredState.inFlight);
|
|
17065
|
-
} else {
|
|
17066
|
-
turnDone = sel.status !== "running";
|
|
17067
|
-
}
|
|
17068
|
-
}
|
|
17069
|
-
if (turnDone) {
|
|
17070
|
-
container.classList.add("hidden");
|
|
17071
|
-
if (bodyEl) bodyEl.classList.add("hidden");
|
|
17072
|
-
return;
|
|
17073
|
-
}
|
|
17074
|
-
|
|
17073
|
+
// 之前这里在 turn 结束(结构化 inFlight=false 或 PTY 非 running)时
|
|
17074
|
+
// 把进度条收起来,理由是「模型经常忘了发最后一条全 completed 的
|
|
17075
|
+
// TodoWrite,让用户对着 5/6 干瞪眼很别扭」。但反馈是:在结构化模式下
|
|
17076
|
+
// inFlight 在流间隙会短暂置假,进度条因此跟着闪没;而且 turn 刚结束
|
|
17077
|
+
// 时用户其实想再看一眼最终进度。改回「只要当前 turn 里有 todos 就显
|
|
17078
|
+
// 示,allDone 时再隐藏」,跨 turn 残留交给开头那段「最后一条 user
|
|
17079
|
+
// 消息后才扫 TodoWrite」的 scoping 兜住。
|
|
17075
17080
|
container.classList.remove("hidden");
|
|
17076
17081
|
if (bodyEl) bodyEl.classList.remove("hidden");
|
|
17077
17082
|
|