@co0ontty/wand 1.32.0 → 1.32.2
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/web-ui/content/scripts.js +191 -153
- package/dist/web-ui/content/styles.css +251 -163
- package/package.json +1 -1
|
@@ -80,6 +80,33 @@
|
|
|
80
80
|
var configPath = "${escapeHtml(configPath)}";
|
|
81
81
|
var CHAT_EXPAND_STATE_STORAGE_KEY = "wand-chat-expand-state-v1";
|
|
82
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
|
+
|
|
83
110
|
var state = {
|
|
84
111
|
selectedId: (function() {
|
|
85
112
|
try { return localStorage.getItem("wand-selected-session") || null; } catch (e) { return null; }
|
|
@@ -154,7 +181,12 @@
|
|
|
154
181
|
bootstrapping: true,
|
|
155
182
|
sessionsDrawerOpen: false,
|
|
156
183
|
sidebarPinned: (function() {
|
|
157
|
-
|
|
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; }
|
|
158
190
|
})(),
|
|
159
191
|
sidebarCollapsed: (function() {
|
|
160
192
|
try { return localStorage.getItem("wand-sidebar-collapsed") === "true"; } catch (e) { return false; }
|
|
@@ -1380,6 +1412,36 @@
|
|
|
1380
1412
|
});
|
|
1381
1413
|
}
|
|
1382
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
|
+
|
|
1383
1445
|
renderBootLoading();
|
|
1384
1446
|
restoreLoginSession();
|
|
1385
1447
|
|
|
@@ -1604,9 +1666,6 @@
|
|
|
1604
1666
|
'</button>' +
|
|
1605
1667
|
'</div>' +
|
|
1606
1668
|
'</div>' +
|
|
1607
|
-
'<button id="sidebar-pin-btn" class="btn btn-ghost btn-sm sidebar-pin-toggle' + (state.sidebarPinned ? ' pinned' : '') + '" type="button" title="' + (state.sidebarPinned ? '取消固定侧栏' : '固定侧栏') + '">' +
|
|
1608
|
-
'<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>' +
|
|
1609
|
-
'</button>' +
|
|
1610
1669
|
'<button id="sidebar-collapse-btn" class="btn btn-ghost btn-sm sidebar-collapse-toggle' + (isCollapsed ? ' collapsed' : '') + '" type="button" title="' + (isCollapsed ? '展开侧栏' : '收起为窄条') + '" aria-label="' + (isCollapsed ? '展开侧栏' : '收起为窄条') + '">' +
|
|
1611
1670
|
(isCollapsed
|
|
1612
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>'
|
|
@@ -1727,6 +1786,9 @@
|
|
|
1727
1786
|
'<span class="chat-unread-bubble-icon"><svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 3.5v9M3.5 8l4.5 4.5L12.5 8"/></svg></span>' +
|
|
1728
1787
|
'<span class="chat-unread-bubble-count" aria-hidden="true"></span>' +
|
|
1729
1788
|
'</button>' +
|
|
1789
|
+
// 排队气泡宿主:贴在对话显示区域的右下角(在"回复中"状态线上方),
|
|
1790
|
+
// 不进输入框 panel。updateQueueBar() 仅在 queuedMessages 非空时显形。
|
|
1791
|
+
'<div id="queue-bar-host" class="queue-bar-host" hidden></div>' +
|
|
1730
1792
|
'</div>' +
|
|
1731
1793
|
'<div id="blank-chat" class="blank-chat' + (state.selectedId ? " hidden" : "") + '">' +
|
|
1732
1794
|
'<div class="blank-chat-inner">' +
|
|
@@ -1755,10 +1817,7 @@
|
|
|
1755
1817
|
'</div>' +
|
|
1756
1818
|
'</div>' +
|
|
1757
1819
|
'<div class="input-panel' + (state.selectedId ? "" : " hidden") + '">' +
|
|
1758
|
-
//
|
|
1759
|
-
// 显形。位置在 composer-top-row(含 "回复中" 状态条)之上,对话框右下角,
|
|
1760
|
-
// 不进入输入框内部。所有内容由 updater 注入;这里只保留稳定的外层骨架。
|
|
1761
|
-
'<div id="queue-bar-host" class="queue-bar-host" hidden></div>' +
|
|
1820
|
+
// #queue-bar-host 已搬到 #chat-output 内部(对话区右下角),不在这里了。
|
|
1762
1821
|
'<div class="composer-top-row">' +
|
|
1763
1822
|
'<div id="todo-progress" class="todo-progress hidden">' +
|
|
1764
1823
|
'<div class="todo-progress-header" id="todo-progress-toggle">' +
|
|
@@ -1841,13 +1900,8 @@
|
|
|
1841
1900
|
'<button id="stop-button" class="btn-circle btn-circle-stop' + (state.selectedId ? "" : " hidden") + '" title="停止">' +
|
|
1842
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>' +
|
|
1843
1902
|
'</button>' +
|
|
1844
|
-
//
|
|
1845
|
-
//
|
|
1846
|
-
// 用 pill 形态 + 文字 + 脉动,让用户一眼就看到「立即发送」这条快捷路径。
|
|
1847
|
-
'<button id="interrupt-send-button" class="btn-pill btn-pill-interrupt hidden" type="button" title="中断当前回复并立即发送新输入(Cmd/Ctrl+Enter)" aria-label="立即发送">' +
|
|
1848
|
-
'<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>' +
|
|
1849
|
-
'<span class="btn-pill-label">立即</span>' +
|
|
1850
|
-
'</button>' +
|
|
1903
|
+
// 「立即发送」按钮已下线 —— 默认行为永远是排队(气泡),想插队
|
|
1904
|
+
// 请点输入框上方那条气泡(chip)。Cmd/Ctrl+Enter 快捷键仍保留。
|
|
1851
1905
|
'<button id="send-input-button" class="btn-circle btn-circle-send" title="发送">' +
|
|
1852
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>' +
|
|
1853
1907
|
'</button>' +
|
|
@@ -5823,8 +5877,6 @@
|
|
|
5823
5877
|
if (drawerBackdrop) drawerBackdrop.addEventListener("click", closeSessionsDrawer);
|
|
5824
5878
|
var closeDrawerBtn = document.getElementById("close-drawer-button");
|
|
5825
5879
|
if (closeDrawerBtn) closeDrawerBtn.addEventListener("click", closeSessionsDrawer);
|
|
5826
|
-
var pinBtn = document.getElementById("sidebar-pin-btn");
|
|
5827
|
-
if (pinBtn) pinBtn.addEventListener("click", toggleSidebarPin);
|
|
5828
5880
|
var collapseBtn = document.getElementById("sidebar-collapse-btn");
|
|
5829
5881
|
if (collapseBtn) collapseBtn.addEventListener("click", toggleSidebarCollapsed);
|
|
5830
5882
|
var sidebarMoreBtn = document.getElementById("sidebar-more-btn");
|
|
@@ -6092,11 +6144,6 @@
|
|
|
6092
6144
|
closeSessionsDrawer();
|
|
6093
6145
|
sendOrStart();
|
|
6094
6146
|
});
|
|
6095
|
-
var interruptSendBtn = document.getElementById("interrupt-send-button");
|
|
6096
|
-
if (interruptSendBtn) interruptSendBtn.addEventListener("click", function() {
|
|
6097
|
-
closeSessionsDrawer();
|
|
6098
|
-
sendOrStart({ interrupt: true });
|
|
6099
|
-
});
|
|
6100
6147
|
var stopBtn = document.getElementById("stop-button");
|
|
6101
6148
|
if (stopBtn) stopBtn.addEventListener("click", stopSession);
|
|
6102
6149
|
var modeSelect = document.getElementById("chat-mode-select");
|
|
@@ -8159,14 +8206,17 @@
|
|
|
8159
8206
|
// Keep placeholders short so they don't wrap on portrait mobile screens.
|
|
8160
8207
|
// Only show informative state hints; drop the redundant "send to X" labels.
|
|
8161
8208
|
if (terminalInteractive) return "终端交互中";
|
|
8162
|
-
|
|
8209
|
+
// 只有真正进入终止态(exited / failed / stopped)才提示"会话已结束"。
|
|
8210
|
+
// 结构化会话刚创建或一次回复结束后会回到 "idle"——那是等待下一条输入的
|
|
8211
|
+
// 正常状态,不应该被当成结束。
|
|
8212
|
+
if (session && (session.status === "exited" || session.status === "failed" || session.status === "stopped")) {
|
|
8163
8213
|
if (canAutoResumeSession(session)) return "";
|
|
8164
8214
|
return "会话已结束";
|
|
8165
8215
|
}
|
|
8166
8216
|
// 结构化会话在出 token 时,输入框仍然可用——告诉用户默认行为是排队,
|
|
8167
|
-
//
|
|
8217
|
+
// 想插队请按气泡上的 ⚡ 按钮。短语尽量短,避免在窄屏手机上换行。
|
|
8168
8218
|
if (isStructuredSession(session) && session.structuredState && session.structuredState.inFlight) {
|
|
8169
|
-
return "回复中…Enter 排队 ·
|
|
8219
|
+
return "回复中…Enter 排队 · ⚡ 立即发送";
|
|
8170
8220
|
}
|
|
8171
8221
|
return "";
|
|
8172
8222
|
}
|
|
@@ -9266,7 +9316,6 @@
|
|
|
9266
9316
|
function updatePinState() {
|
|
9267
9317
|
var drawer = document.getElementById("sessions-drawer");
|
|
9268
9318
|
var mainLayout = document.querySelector(".main-layout");
|
|
9269
|
-
var pinBtn = document.getElementById("sidebar-pin-btn");
|
|
9270
9319
|
// 与 renderAppShell 保持一致:手机端只允许窄条形态 anchored。
|
|
9271
9320
|
var isMobile = isMobileLayout();
|
|
9272
9321
|
var isCollapsed = !!state.sidebarPinned && !!state.sidebarCollapsed;
|
|
@@ -9279,10 +9328,6 @@
|
|
|
9279
9328
|
mainLayout.classList.toggle("sidebar-pinned", isAnchored);
|
|
9280
9329
|
mainLayout.classList.toggle("sidebar-collapsed", isCollapsed);
|
|
9281
9330
|
}
|
|
9282
|
-
if (pinBtn) {
|
|
9283
|
-
pinBtn.classList.toggle("pinned", state.sidebarPinned);
|
|
9284
|
-
pinBtn.title = state.sidebarPinned ? "取消固定侧栏" : "固定侧栏";
|
|
9285
|
-
}
|
|
9286
9331
|
}
|
|
9287
9332
|
|
|
9288
9333
|
function updateDrawerState() {
|
|
@@ -9306,9 +9351,26 @@
|
|
|
9306
9351
|
}
|
|
9307
9352
|
|
|
9308
9353
|
function toggleSessionsDrawer() {
|
|
9309
|
-
|
|
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 行为。
|
|
9310
9372
|
state.sessionsDrawerOpen = !state.sessionsDrawerOpen;
|
|
9311
|
-
if (state.sessionsDrawerOpen
|
|
9373
|
+
if (state.sessionsDrawerOpen) {
|
|
9312
9374
|
state.filePanelOpen = false;
|
|
9313
9375
|
try {
|
|
9314
9376
|
localStorage.setItem("wand-file-panel-open", "false");
|
|
@@ -9318,13 +9380,42 @@
|
|
|
9318
9380
|
}
|
|
9319
9381
|
|
|
9320
9382
|
function closeSessionsDrawer() {
|
|
9321
|
-
|
|
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 关闭行为。
|
|
9322
9397
|
if (!state.sessionsDrawerOpen) return;
|
|
9323
9398
|
closeSwipedItem();
|
|
9324
9399
|
state.sessionsDrawerOpen = false;
|
|
9325
9400
|
updateLayoutState();
|
|
9326
9401
|
}
|
|
9327
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
|
+
|
|
9328
9419
|
var collapsedTileBubbleEl = null;
|
|
9329
9420
|
function ensureCollapsedTileBubble() {
|
|
9330
9421
|
if (collapsedTileBubbleEl && document.body.contains(collapsedTileBubbleEl)) {
|
|
@@ -9390,8 +9481,7 @@
|
|
|
9390
9481
|
|
|
9391
9482
|
function toggleSidebarCollapsed() {
|
|
9392
9483
|
var isMobile = isMobileLayout();
|
|
9393
|
-
//
|
|
9394
|
-
// 用户直觉是「点了就该看到窄条」,过去这里 early return 让按钮看上去没反应。
|
|
9484
|
+
// 任何形态下点窄条按钮都意味着「我要常驻」,确保 pinned 写上。
|
|
9395
9485
|
if (!state.sidebarPinned) {
|
|
9396
9486
|
state.sidebarPinned = true;
|
|
9397
9487
|
try {
|
|
@@ -9415,47 +9505,13 @@
|
|
|
9415
9505
|
localStorage.setItem("wand-sidebar-pinned", "false");
|
|
9416
9506
|
} catch (e) {}
|
|
9417
9507
|
} else {
|
|
9418
|
-
// 桌面端展开窄条 → 300px
|
|
9508
|
+
// 桌面端展开窄条 → 300px 全栏常驻。
|
|
9419
9509
|
state.sessionsDrawerOpen = true;
|
|
9420
9510
|
}
|
|
9421
9511
|
render();
|
|
9422
|
-
|
|
9423
|
-
if (mainLayout) {
|
|
9424
|
-
var onEnd = function(e) {
|
|
9425
|
-
if (e.propertyName === "padding-left") {
|
|
9426
|
-
mainLayout.removeEventListener("transitionend", onEnd);
|
|
9427
|
-
scheduleTerminalResize(true);
|
|
9428
|
-
}
|
|
9429
|
-
};
|
|
9430
|
-
mainLayout.addEventListener("transitionend", onEnd);
|
|
9431
|
-
}
|
|
9432
|
-
setTimeout(function() { scheduleTerminalResize(true); }, 350);
|
|
9512
|
+
scheduleTerminalRefitAfterPaddingTransition();
|
|
9433
9513
|
}
|
|
9434
9514
|
|
|
9435
|
-
function toggleSidebarPin() {
|
|
9436
|
-
if (isMobileLayout()) return;
|
|
9437
|
-
state.sidebarPinned = !state.sidebarPinned;
|
|
9438
|
-
try {
|
|
9439
|
-
localStorage.setItem("wand-sidebar-pinned", String(state.sidebarPinned));
|
|
9440
|
-
} catch (e) {}
|
|
9441
|
-
if (state.sidebarPinned) {
|
|
9442
|
-
state.sessionsDrawerOpen = true;
|
|
9443
|
-
}
|
|
9444
|
-
updateLayoutState();
|
|
9445
|
-
// Refit terminal after padding-left transition completes
|
|
9446
|
-
var mainLayout = document.querySelector(".main-layout");
|
|
9447
|
-
if (mainLayout) {
|
|
9448
|
-
var onEnd = function(e) {
|
|
9449
|
-
if (e.propertyName === "padding-left") {
|
|
9450
|
-
mainLayout.removeEventListener("transitionend", onEnd);
|
|
9451
|
-
scheduleTerminalResize(true);
|
|
9452
|
-
}
|
|
9453
|
-
};
|
|
9454
|
-
mainLayout.addEventListener("transitionend", onEnd);
|
|
9455
|
-
}
|
|
9456
|
-
// Fallback refit in case transition doesn't fire
|
|
9457
|
-
setTimeout(function() { scheduleTerminalResize(true); }, 350);
|
|
9458
|
-
}
|
|
9459
9515
|
|
|
9460
9516
|
// Store last focused element for focus trap
|
|
9461
9517
|
var lastFocusedElement = null;
|
|
@@ -12257,9 +12313,10 @@
|
|
|
12257
12313
|
|
|
12258
12314
|
function postStructuredInput(input, inputBox, session, opts) {
|
|
12259
12315
|
opts = opts || {};
|
|
12260
|
-
//
|
|
12261
|
-
//
|
|
12262
|
-
//
|
|
12316
|
+
// interrupt:true 现在只来自 Cmd/Ctrl+Enter 快捷键,或点队列气泡触发的
|
|
12317
|
+
// queueBarPromoteIndex()。普通 Enter / 点发送在上一条还在流式时默认走
|
|
12318
|
+
// queue —— 后端 sendMessage(...) 会把它追加到 queuedMessages,等当前 turn
|
|
12319
|
+
// 结束自动 flush;想插队就点输入框上方那条气泡。
|
|
12263
12320
|
var requestedInterrupt = !!opts.interrupt;
|
|
12264
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 });
|
|
12265
12322
|
if (!state.selectedId || !input) return Promise.resolve();
|
|
@@ -12297,7 +12354,7 @@
|
|
|
12297
12354
|
updateSessionSnapshot(optimisticPatch);
|
|
12298
12355
|
var queueRefreshed = state.sessions.find(function(s) { return s.id === session.id; }) || session;
|
|
12299
12356
|
state.currentMessages = buildMessagesForRender(queueRefreshed, getPreferredMessages(queueRefreshed, queueRefreshed.output, false));
|
|
12300
|
-
updateInputHint("
|
|
12357
|
+
updateInputHint("已加入排队…");
|
|
12301
12358
|
renderChat(true);
|
|
12302
12359
|
updateStructuredQueueCounter();
|
|
12303
12360
|
// 乐观 toast:原本只在 POST 完成后才提示,Claude 流式拖太久时用户根本
|
|
@@ -12458,8 +12515,9 @@
|
|
|
12458
12515
|
// · 默认只展开队首(即下一个要发的那条),显示编号 + 文本 + × 删除
|
|
12459
12516
|
// · 其他消息收起成一根小横杠(指示存在但不占空间)
|
|
12460
12517
|
// · 鼠标悬到任意小横杠 → 该条展开、原本展开的那条收回小横杠
|
|
12461
|
-
// ·
|
|
12462
|
-
//
|
|
12518
|
+
// · 点一下任意气泡 → 中断当前回复、把这条作为新输入插队发出去
|
|
12519
|
+
// · 按住任意气泡向上 / 向下拖拽 → 换序
|
|
12520
|
+
// 末尾跟一个 ⚡ 按钮:等价于点队首气泡(保留作为快速插队的视觉提示)。
|
|
12463
12521
|
// 数据源:session.queuedMessages(后端 WS + postStructuredInput 乐观更新)。
|
|
12464
12522
|
// ──────────────────────────────────────────────────────────────────────────
|
|
12465
12523
|
|
|
@@ -12486,12 +12544,15 @@
|
|
|
12486
12544
|
return 0;
|
|
12487
12545
|
}
|
|
12488
12546
|
|
|
12489
|
-
function renderQueueBarHtml(items, inFlight, atCapacity
|
|
12547
|
+
function renderQueueBarHtml(items, inFlight, atCapacity) {
|
|
12548
|
+
// 底部独立 ⚡ 按钮已下线,每条 chip 内部自带 ⚡ "立即"按钮 ——
|
|
12549
|
+
// 这样用户一眼就能看出"是把哪一条插队"。
|
|
12490
12550
|
var single = items.length <= 1;
|
|
12491
12551
|
var barClass = "queue-bar";
|
|
12492
12552
|
if (atCapacity) barClass += " queue-bar-capacity";
|
|
12493
12553
|
if (inFlight) barClass += " queue-bar-inflight";
|
|
12494
12554
|
var expandedIdx = queueBarExpandedIndex(items.length);
|
|
12555
|
+
var promoteTip = inFlight ? "中断当前回复,立即发送这条" : "立即发送这条";
|
|
12495
12556
|
var chips = "";
|
|
12496
12557
|
for (var i = 0; i < items.length; i++) {
|
|
12497
12558
|
var raw = items[i] == null ? "" : String(items[i]);
|
|
@@ -12499,13 +12560,21 @@
|
|
|
12499
12560
|
var itemClass = "queue-bar-item";
|
|
12500
12561
|
if (isExpanded) itemClass += " expanded";
|
|
12501
12562
|
if (single) itemClass += " queue-bar-item-single";
|
|
12502
|
-
//
|
|
12503
|
-
var titleAttr = isExpanded ? raw + "
|
|
12563
|
+
// chip 本体是"拖拽起手区";内部 ⚡ 按钮独占 click 用于立即发送、× 用于删除。
|
|
12564
|
+
var titleAttr = isExpanded ? raw + "(按住可拖动调序)" : raw;
|
|
12504
12565
|
chips +=
|
|
12505
12566
|
'<li class="' + itemClass + '" data-index="' + i + '" data-action="drag"' +
|
|
12506
12567
|
' title="' + escapeHtml(titleAttr) + '">' +
|
|
12507
12568
|
'<span class="queue-bar-item-index" aria-hidden="true">' + (i + 1) + '</span>' +
|
|
12508
12569
|
'<span class="queue-bar-item-text">' + escapeHtml(queueChipTruncate(raw)) + '</span>' +
|
|
12570
|
+
'<button type="button" class="queue-bar-item-promote" data-action="promote-item"' +
|
|
12571
|
+
' title="' + escapeHtml(promoteTip) + '" aria-label="立即发送这条"' +
|
|
12572
|
+
' tabindex="' + (isExpanded ? "0" : "-1") + '">' +
|
|
12573
|
+
'<svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">' +
|
|
12574
|
+
'<path d="M13 2 L4 14 L11 14 L10 22 L20 9 L13 9 Z"/>' +
|
|
12575
|
+
'</svg>' +
|
|
12576
|
+
'<span class="queue-bar-item-promote-label">立即</span>' +
|
|
12577
|
+
'</button>' +
|
|
12509
12578
|
'<button type="button" class="queue-bar-item-delete" data-action="delete"' +
|
|
12510
12579
|
' aria-label="删除这条排队消息" title="删除" tabindex="' + (isExpanded ? "0" : "-1") + '">' +
|
|
12511
12580
|
'<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor"' +
|
|
@@ -12517,13 +12586,6 @@
|
|
|
12517
12586
|
return (
|
|
12518
12587
|
'<div class="' + barClass + '" data-queue-bar="1">' +
|
|
12519
12588
|
'<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
12589
|
'</div>'
|
|
12528
12590
|
);
|
|
12529
12591
|
}
|
|
@@ -12549,9 +12611,8 @@
|
|
|
12549
12611
|
host.hidden = false;
|
|
12550
12612
|
var inFlight = !!(session.structuredState && session.structuredState.inFlight && session.status === "running");
|
|
12551
12613
|
var atCapacity = queue.length >= QUEUE_BAR_MAX;
|
|
12552
|
-
var immediateLabel = inFlight ? "立即" : "发送";
|
|
12553
12614
|
|
|
12554
|
-
host.innerHTML = renderQueueBarHtml(queue, inFlight, atCapacity
|
|
12615
|
+
host.innerHTML = renderQueueBarHtml(queue, inFlight, atCapacity);
|
|
12555
12616
|
}
|
|
12556
12617
|
|
|
12557
12618
|
// 只切换 .expanded class,不重建 DOM —— 避免鼠标移过去触发的重建
|
|
@@ -12653,20 +12714,24 @@
|
|
|
12653
12714
|
});
|
|
12654
12715
|
}
|
|
12655
12716
|
|
|
12656
|
-
|
|
12717
|
+
// 把队列里第 index 条剥下来,作为新的输入立刻发送出去。
|
|
12718
|
+
// - inFlight:interrupt + preserveQueue(中断当前回复,保留其它排队)
|
|
12719
|
+
// - 非 inFlight:当作普通新消息发出去
|
|
12720
|
+
// 用户路径:点输入框上方的气泡(chip)→ 这里。
|
|
12721
|
+
function queueBarPromoteIndex(index) {
|
|
12657
12722
|
var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
12658
12723
|
if (!session) return;
|
|
12659
12724
|
var queue = Array.isArray(session.queuedMessages) ? session.queuedMessages.slice() : [];
|
|
12660
|
-
if (queue.length
|
|
12661
|
-
var
|
|
12662
|
-
var rest = queue.slice(1);
|
|
12725
|
+
if (index < 0 || index >= queue.length) return;
|
|
12726
|
+
var picked = queue[index];
|
|
12727
|
+
var rest = queue.slice(0, index).concat(queue.slice(index + 1));
|
|
12663
12728
|
var prev = queue.slice();
|
|
12664
12729
|
var inFlight = !!(session.structuredState && session.structuredState.inFlight && session.status === "running");
|
|
12665
12730
|
|
|
12666
|
-
//
|
|
12731
|
+
// 乐观:剥掉这一条;hover 下标随之收缩
|
|
12667
12732
|
if (typeof state.queueBarHoverIndex === "number") {
|
|
12668
|
-
if (state.queueBarHoverIndex ===
|
|
12669
|
-
else state.queueBarHoverIndex -= 1;
|
|
12733
|
+
if (state.queueBarHoverIndex === index) state.queueBarHoverIndex = null;
|
|
12734
|
+
else if (state.queueBarHoverIndex > index) state.queueBarHoverIndex -= 1;
|
|
12670
12735
|
}
|
|
12671
12736
|
updateSessionSnapshot({ id: session.id, queuedMessages: rest });
|
|
12672
12737
|
|
|
@@ -12674,14 +12739,14 @@
|
|
|
12674
12739
|
? crypto.randomUUID()
|
|
12675
12740
|
: (Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 10));
|
|
12676
12741
|
|
|
12677
|
-
var body = { input:
|
|
12742
|
+
var body = { input: picked, idempotencyKey: idempotencyKey };
|
|
12678
12743
|
if (inFlight) {
|
|
12679
12744
|
// 中断 + 保留剩余队列
|
|
12680
12745
|
body.interrupt = true;
|
|
12681
12746
|
body.preserveQueue = true;
|
|
12682
12747
|
}
|
|
12683
12748
|
// 给一个乐观 toast,让用户瞬间知道点击生效了
|
|
12684
|
-
showToast(inFlight ? "
|
|
12749
|
+
showToast(inFlight ? "已请求中断当前回复,立即发送这条。" : "已立即发送这条消息。", "info");
|
|
12685
12750
|
|
|
12686
12751
|
fetch("/api/structured-sessions/" + session.id + "/messages", {
|
|
12687
12752
|
method: "POST",
|
|
@@ -12715,6 +12780,9 @@
|
|
|
12715
12780
|
}
|
|
12716
12781
|
|
|
12717
12782
|
// ── 拖拽排序(Pointer Events + 真实高度的 sort/animate)──
|
|
12783
|
+
// 单条气泡的 pointerdown 也会进这里,但 queue.length <= 1 时直接返回,让
|
|
12784
|
+
// 系统 click 事件穿透到 #queue-bar-host 的 click delegate(那里再判断"点击
|
|
12785
|
+
// 气泡 → 立即发送")。
|
|
12718
12786
|
function queueBarDragStart(ev, chipEl) {
|
|
12719
12787
|
var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
12720
12788
|
if (!session) return;
|
|
@@ -12838,7 +12906,8 @@
|
|
|
12838
12906
|
state.queueBarDrag = null;
|
|
12839
12907
|
|
|
12840
12908
|
if (origIndex === targetIndex) {
|
|
12841
|
-
//
|
|
12909
|
+
// 没动 → 单纯刷新一下。立即发送由 chip 内部的 ⚡ 按钮触发,
|
|
12910
|
+
// 不在 chip 本体上做隐式 tap-to-promote(容易误触)。
|
|
12842
12911
|
updateQueueBar();
|
|
12843
12912
|
return;
|
|
12844
12913
|
}
|
|
@@ -12886,10 +12955,18 @@
|
|
|
12886
12955
|
var actionEl = ev.target && ev.target.closest ? ev.target.closest("[data-action]") : null;
|
|
12887
12956
|
if (!actionEl || !host.contains(actionEl)) return;
|
|
12888
12957
|
var action = actionEl.getAttribute("data-action");
|
|
12889
|
-
|
|
12958
|
+
// chip 本体(data-action="drag")由 pointerdown 走 drag-or-tap 流程;
|
|
12959
|
+
// click 阶段不处理,否则会和拖拽收尾冲突。
|
|
12960
|
+
if (action === "drag") return;
|
|
12890
12961
|
ev.preventDefault();
|
|
12891
12962
|
ev.stopPropagation();
|
|
12892
|
-
if (action === "promote") {
|
|
12963
|
+
if (action === "promote-item") {
|
|
12964
|
+
// chip 内部的 ⚡ "立即"按钮:把这一条剥下来插队发送,让用户一眼看到
|
|
12965
|
+
// 自己点的就是哪一条。
|
|
12966
|
+
var pItem = actionEl.closest(".queue-bar-item");
|
|
12967
|
+
if (pItem) queueBarPromoteIndex(Number(pItem.getAttribute("data-index")));
|
|
12968
|
+
return;
|
|
12969
|
+
}
|
|
12893
12970
|
if (action === "delete") {
|
|
12894
12971
|
var itemEl = actionEl.closest(".queue-bar-item");
|
|
12895
12972
|
if (itemEl) queueBarDeleteItem(Number(itemEl.getAttribute("data-index")));
|
|
@@ -12907,10 +12984,11 @@
|
|
|
12907
12984
|
if (state.queueBarDrag) return;
|
|
12908
12985
|
setQueueBarHoverIndex(null);
|
|
12909
12986
|
});
|
|
12910
|
-
// 整个气泡都是拖拽起手区。
|
|
12987
|
+
// 整个气泡都是拖拽起手区。chip 内部的 ⚡ / × 按钮通过 closest 跳过,
|
|
12988
|
+
// 让 click 阶段去处理它们。
|
|
12911
12989
|
host.addEventListener("pointerdown", function(ev) {
|
|
12912
12990
|
if (ev.button !== undefined && ev.button !== 0) return;
|
|
12913
|
-
if (ev.target && ev.target.closest && ev.target.closest('[data-action="delete"], [data-action="promote"]')) return;
|
|
12991
|
+
if (ev.target && ev.target.closest && ev.target.closest('[data-action="delete"], [data-action="promote-item"]')) return;
|
|
12914
12992
|
var chip = ev.target && ev.target.closest ? ev.target.closest('.queue-bar-item') : null;
|
|
12915
12993
|
if (!chip) return;
|
|
12916
12994
|
// 拖拽前先把这条切到 expanded(鼠标按下时通常已经 hovered,但触屏没 hover)
|
|
@@ -12961,38 +13039,15 @@
|
|
|
12961
13039
|
});
|
|
12962
13040
|
}
|
|
12963
13041
|
|
|
12964
|
-
//
|
|
12965
|
-
//
|
|
13042
|
+
// 结构化会话的"对话视图"现在只渲染真实的 user/assistant turn。排队消息(还没
|
|
13043
|
+
// flush 出去那批)由 .queue-bar 在对话区右下角统一展示,不再在 chat 流里贴一份
|
|
13044
|
+
// 半透明 "排队中" 用户气泡——避免同一条消息在 UI 上出现两次。
|
|
12966
13045
|
function buildMessagesForRender(session, messages) {
|
|
12967
13046
|
var sanitized = Array.isArray(messages) ? stripRenderOnlyStructuredMessages(messages) : [];
|
|
12968
13047
|
var base = Array.isArray(sanitized) ? sanitized.slice() : [];
|
|
12969
13048
|
if (!session || session.sessionKind !== "structured") {
|
|
12970
13049
|
return base;
|
|
12971
13050
|
}
|
|
12972
|
-
var queued = getStructuredQueuedInputs(session);
|
|
12973
|
-
if (queued && queued.length > 0) {
|
|
12974
|
-
// Collect recent user message texts to deduplicate against queued items.
|
|
12975
|
-
// A queued message that already appears as a real user turn should not
|
|
12976
|
-
// be rendered a second time with the "排队中" badge.
|
|
12977
|
-
var existingUserTexts = {};
|
|
12978
|
-
for (var ei = base.length - 1; ei >= 0 && Object.keys(existingUserTexts).length < queued.length + 5; ei--) {
|
|
12979
|
-
var em = base[ei];
|
|
12980
|
-
if (em && em.role === "user" && Array.isArray(em.content)) {
|
|
12981
|
-
for (var ej = 0; ej < em.content.length; ej++) {
|
|
12982
|
-
if (em.content[ej] && em.content[ej].type === "text" && em.content[ej].text) {
|
|
12983
|
-
existingUserTexts[em.content[ej].text] = (existingUserTexts[em.content[ej].text] || 0) + 1;
|
|
12984
|
-
}
|
|
12985
|
-
}
|
|
12986
|
-
}
|
|
12987
|
-
}
|
|
12988
|
-
for (var qi = 0; qi < queued.length; qi++) {
|
|
12989
|
-
if (existingUserTexts[queued[qi]]) {
|
|
12990
|
-
existingUserTexts[queued[qi]]--;
|
|
12991
|
-
continue; // Skip — this queued text is already shown as a real message
|
|
12992
|
-
}
|
|
12993
|
-
base.push({ role: "user", content: [{ type: "text", text: queued[qi], __queued: true }] });
|
|
12994
|
-
}
|
|
12995
|
-
}
|
|
12996
13051
|
if (session.structuredState && session.structuredState.inFlight) {
|
|
12997
13052
|
var last = base[base.length - 1];
|
|
12998
13053
|
if (!last || last.role !== "assistant") {
|
|
@@ -13502,12 +13557,6 @@
|
|
|
13502
13557
|
: (isCodex ? (isRunning ? "发送给 Codex" : "Codex 会话已结束") : (!selectedSession || isRunning || canResumeOnSend ? "发送" : "会话已结束")));
|
|
13503
13558
|
sendBtn.classList.toggle("queue-mode", structuredInFlight);
|
|
13504
13559
|
}
|
|
13505
|
-
var interruptBtn = document.getElementById("interrupt-send-button");
|
|
13506
|
-
if (interruptBtn) {
|
|
13507
|
-
// 仅结构化 + inFlight 时显示。pty 会话有自己的 Ctrl+C / stop 按钮,
|
|
13508
|
-
// 用不上这套语义。
|
|
13509
|
-
interruptBtn.classList.toggle("hidden", !structuredInFlight);
|
|
13510
|
-
}
|
|
13511
13560
|
var container = document.getElementById("output");
|
|
13512
13561
|
if (container) container.classList.toggle("interactive", !structured && state.terminalInteractive);
|
|
13513
13562
|
}
|
|
@@ -16993,24 +17042,13 @@
|
|
|
16993
17042
|
return;
|
|
16994
17043
|
}
|
|
16995
17044
|
|
|
16996
|
-
//
|
|
16997
|
-
//
|
|
16998
|
-
//
|
|
16999
|
-
|
|
17000
|
-
|
|
17001
|
-
|
|
17002
|
-
|
|
17003
|
-
turnDone = !(sel.structuredState && sel.structuredState.inFlight);
|
|
17004
|
-
} else {
|
|
17005
|
-
turnDone = sel.status !== "running";
|
|
17006
|
-
}
|
|
17007
|
-
}
|
|
17008
|
-
if (turnDone) {
|
|
17009
|
-
container.classList.add("hidden");
|
|
17010
|
-
if (bodyEl) bodyEl.classList.add("hidden");
|
|
17011
|
-
return;
|
|
17012
|
-
}
|
|
17013
|
-
|
|
17045
|
+
// 之前这里在 turn 结束(结构化 inFlight=false 或 PTY 非 running)时
|
|
17046
|
+
// 把进度条收起来,理由是「模型经常忘了发最后一条全 completed 的
|
|
17047
|
+
// TodoWrite,让用户对着 5/6 干瞪眼很别扭」。但反馈是:在结构化模式下
|
|
17048
|
+
// inFlight 在流间隙会短暂置假,进度条因此跟着闪没;而且 turn 刚结束
|
|
17049
|
+
// 时用户其实想再看一眼最终进度。改回「只要当前 turn 里有 todos 就显
|
|
17050
|
+
// 示,allDone 时再隐藏」,跨 turn 残留交给开头那段「最后一条 user
|
|
17051
|
+
// 消息后才扫 TodoWrite」的 scoping 兜住。
|
|
17014
17052
|
container.classList.remove("hidden");
|
|
17015
17053
|
if (bodyEl) bodyEl.classList.remove("hidden");
|
|
17016
17054
|
|