@co0ontty/wand 1.30.3 → 1.31.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/README.md +39 -2
- package/dist/claude-sdk-runner.d.ts +31 -0
- package/dist/claude-sdk-runner.js +142 -0
- package/dist/cli.js +104 -0
- package/dist/git-quick-commit.js +18 -26
- package/dist/process-manager.d.ts +7 -0
- package/dist/process-manager.js +26 -2
- package/dist/prompt-optimizer.js +17 -26
- package/dist/server-session-routes.js +72 -3
- package/dist/server.js +1 -0
- package/dist/structured-session-manager.d.ts +24 -0
- package/dist/structured-session-manager.js +106 -7
- package/dist/tui/attach.js +7 -8
- package/dist/tui/commands.d.ts +24 -7
- package/dist/tui/commands.js +200 -86
- package/dist/tui/index.js +8 -8
- package/dist/tui/service-panel.js +3 -4
- package/dist/types.d.ts +2 -0
- package/dist/web-ui/content/scripts.js +927 -81
- package/dist/web-ui/content/styles.css +986 -141
- package/package.json +1 -1
|
@@ -129,6 +129,13 @@
|
|
|
129
129
|
}
|
|
130
130
|
})(), // 跨会话排队消息 [{ id, text, cwd, mode, tool }]
|
|
131
131
|
structuredInputQueue: [], // 结构化会话同会话排队消息
|
|
132
|
+
// 排队条 UI 局部状态 ——
|
|
133
|
+
// queueBarExpanded: 折叠条点击展开成下拉面板
|
|
134
|
+
// queueBarItemExpanded: 展开面板里被点开看完整内容的 item 下标集合
|
|
135
|
+
// queueBarDrag: 拖拽排序进行中时的临时状态(pointer 捕获、起始坐标、参考 rect)
|
|
136
|
+
queueBarExpanded: false,
|
|
137
|
+
queueBarItemExpanded: {},
|
|
138
|
+
queueBarDrag: null,
|
|
132
139
|
drafts: {},
|
|
133
140
|
isSyncingInputBox: false,
|
|
134
141
|
loginPending: false,
|
|
@@ -149,6 +156,12 @@
|
|
|
149
156
|
chatModel: (function() {
|
|
150
157
|
try { return localStorage.getItem("wand-chat-model") || ""; } catch (e) { return ""; }
|
|
151
158
|
})(),
|
|
159
|
+
chatThinking: (function() {
|
|
160
|
+
try {
|
|
161
|
+
var v = localStorage.getItem("wand-thinking-effort") || "off";
|
|
162
|
+
return (v === "off" || v === "standard" || v === "deep" || v === "max") ? v : "off";
|
|
163
|
+
} catch (e) { return "off"; }
|
|
164
|
+
})(),
|
|
152
165
|
availableModels: [],
|
|
153
166
|
availableCodexModels: [],
|
|
154
167
|
modelsRefreshing: false,
|
|
@@ -1497,14 +1510,17 @@
|
|
|
1497
1510
|
var preferredTool = getComposerTool();
|
|
1498
1511
|
var composerMode = getSafeModeForTool(preferredTool, state.chatMode);
|
|
1499
1512
|
|
|
1500
|
-
|
|
1501
|
-
|
|
1513
|
+
// 手机端不允许「pin 但不窄条」(300px 固定边栏太占地),只允许窄条形态。
|
|
1514
|
+
// isAnchored = 边栏占据布局空间(推开主内容)。桌面 pin 或 任意端窄条都算 anchored。
|
|
1515
|
+
var isMobile = isMobileLayout();
|
|
1516
|
+
var isCollapsed = !!state.sidebarPinned && !!state.sidebarCollapsed;
|
|
1517
|
+
var isAnchored = isCollapsed || (!!state.sidebarPinned && !isMobile);
|
|
1502
1518
|
var collapsedCls = isCollapsed ? ' sidebar-collapsed' : '';
|
|
1503
1519
|
var sidebarCollapsedCls = isCollapsed ? ' collapsed' : '';
|
|
1504
1520
|
return '<div class="app-container">' +
|
|
1505
1521
|
'<div id="sessions-drawer-backdrop" class="drawer-backdrop' + drawerClass + '"></div>' +
|
|
1506
|
-
'<div class="main-layout' + (state.sessionsDrawerOpen ? ' sidebar-open' : '') + (
|
|
1507
|
-
'<aside id="sessions-drawer" class="sidebar' + drawerClass + (
|
|
1522
|
+
'<div class="main-layout' + (state.sessionsDrawerOpen ? ' sidebar-open' : '') + (isAnchored ? ' sidebar-pinned' : '') + collapsedCls + '">' +
|
|
1523
|
+
'<aside id="sessions-drawer" class="sidebar' + drawerClass + (isAnchored ? ' pinned' : '') + sidebarCollapsedCls + '">' +
|
|
1508
1524
|
'<div class="sidebar-header">' +
|
|
1509
1525
|
'<div class="sidebar-header-main">' +
|
|
1510
1526
|
'<div class="topbar-logo-icon">W</div>' +
|
|
@@ -1704,6 +1720,11 @@
|
|
|
1704
1720
|
'<ul class="todo-progress-list" id="todo-progress-list"></ul>' +
|
|
1705
1721
|
'</div>' +
|
|
1706
1722
|
'</div>' +
|
|
1723
|
+
// 排队条宿主:默认 display:none,updateQueueBar() 在 queuedMessages 非空时
|
|
1724
|
+
// 显形。结构上夹在 composer-top-row(todo 进度)和 input-composer(输入框 +
|
|
1725
|
+
// 工具栏)之间,位置正好"在输入框上方、对话框右下角"。所有内容由 updater
|
|
1726
|
+
// 注入;这里只保留稳定的外层骨架,便于 renderAppShell 全量重建后无缝复位。
|
|
1727
|
+
'<div id="queue-bar-host" class="queue-bar-host" hidden></div>' +
|
|
1707
1728
|
'<div class="input-composer">' +
|
|
1708
1729
|
'<button id="prompt-optimize-btn" class="prompt-optimize-btn" type="button" title="提示词优化(AI)" aria-label="提示词优化">' +
|
|
1709
1730
|
'<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">' +
|
|
@@ -1724,13 +1745,33 @@
|
|
|
1724
1745
|
// tabindex="-1": 把这些控件移出 iOS Safari 的表单导航链,
|
|
1725
1746
|
// 这样 textarea 聚焦时键盘上方就不会出现 ⌃ ⌄ ✓ 表单辅助栏。
|
|
1726
1747
|
'<input type="file" id="file-upload-input" multiple tabindex="-1" style="position:absolute;width:1px;height:1px;opacity:0;overflow:hidden;clip:rect(0,0,0,0);pointer-events:none">' +
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
'<
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1748
|
+
// 三件套 (Mode / Model / Thinking) 同属"会话设置"层:扁平文字 + · 分隔。
|
|
1749
|
+
// 文字下叠一个透明 <select> 承载交互,桌面端弹原生下拉、移动端弹滚轮选择。
|
|
1750
|
+
// 显示文本用 raw id(如 default / claude-sonnet-4-5 / standard),不做翻译。
|
|
1751
|
+
'<span class="composer-text-group" role="group" aria-label="会话设置">' +
|
|
1752
|
+
'<span class="composer-text-pill" title="模式">' +
|
|
1753
|
+
'<span class="composer-text-label" id="chat-mode-label">' + escapeHtml(composerMode) + '</span>' +
|
|
1754
|
+
'<select id="chat-mode-select" class="composer-text-hidden-select" tabindex="-1" aria-label="模式">' +
|
|
1755
|
+
renderChatModeOptionsRaw(preferredTool, composerMode) +
|
|
1756
|
+
'</select>' +
|
|
1757
|
+
'</span>' +
|
|
1758
|
+
'<span class="composer-text-sep" aria-hidden="true">·</span>' +
|
|
1759
|
+
'<span class="composer-text-pill chat-model-text-pill" title="模型">' +
|
|
1760
|
+
'<span class="composer-text-label" id="chat-model-label">' + escapeHtml(getEffectiveModel(selectedSession) || "default") + '</span>' +
|
|
1761
|
+
'<select id="chat-model-select" class="composer-text-hidden-select" tabindex="-1" aria-label="模型">' +
|
|
1762
|
+
renderChatModelOptionsRaw(getEffectiveModel(selectedSession), selectedSession) +
|
|
1763
|
+
'</select>' +
|
|
1764
|
+
'</span>' +
|
|
1765
|
+
'<span class="composer-text-sep" aria-hidden="true">·</span>' +
|
|
1766
|
+
'<span class="composer-text-pill chat-thinking-text-pill" title="思考深度">' +
|
|
1767
|
+
'<span class="composer-text-label" id="chat-thinking-label">' + escapeHtml(getEffectiveThinking(selectedSession)) + '</span>' +
|
|
1768
|
+
'<select id="chat-thinking-select" class="composer-text-hidden-select" tabindex="-1" aria-label="思考深度">' +
|
|
1769
|
+
renderChatThinkingOptionsRaw(getEffectiveThinking(selectedSession)) +
|
|
1770
|
+
'</select>' +
|
|
1771
|
+
'</span>' +
|
|
1772
|
+
'</span>' +
|
|
1773
|
+
renderAutoApproveChip(selectedSession) +
|
|
1774
|
+
'<button id="terminal-interactive-toggle-top" class="composer-pill composer-pill-chip composer-interactive-toggle' + (state.terminalInteractive ? " active" : "") + '" type="button" title="切换终端交互模式">⌨</button>' +
|
|
1734
1775
|
'<span class="permission-actions hidden" id="permission-actions">' +
|
|
1735
1776
|
'<span class="permission-actions-divider"></span>' +
|
|
1736
1777
|
'<span class="permission-actions-label" id="permission-actions-label">等待授权</span>' +
|
|
@@ -1740,7 +1781,7 @@
|
|
|
1740
1781
|
renderApprovalStatsBadge() +
|
|
1741
1782
|
'</div>' +
|
|
1742
1783
|
'<div class="input-composer-right">' +
|
|
1743
|
-
|
|
1784
|
+
// 排队提示从这里搬到 .queue-bar(输入框上方独立浮条),原 #queue-counter 已移除。
|
|
1744
1785
|
'<span class="input-hint' + (state.terminalInteractive ? ' terminal-interactive-hint' : state.currentView === "terminal" ? " hidden" : "") + '">' + (state.terminalInteractive ? '终端交互中 · Ctrl+C 中断 · Ctrl+L 清屏' : 'Enter 发送 · Shift+Enter 换行') + '</span>' +
|
|
1745
1786
|
renderInlineKeyboard() +
|
|
1746
1787
|
'<button id="stop-button" class="btn-circle btn-circle-stop' + (state.selectedId ? "" : " hidden") + '" title="停止">' +
|
|
@@ -1748,8 +1789,10 @@
|
|
|
1748
1789
|
'</button>' +
|
|
1749
1790
|
// 结构化模式且正在出 token 时显示:中断当前回复、立刻发送新输入。
|
|
1750
1791
|
// 默认走 #send-input-button → 排队;想插队的人显式按这颗。
|
|
1751
|
-
|
|
1752
|
-
|
|
1792
|
+
// 用 pill 形态 + 文字 + 脉动,让用户一眼就看到「立即发送」这条快捷路径。
|
|
1793
|
+
'<button id="interrupt-send-button" class="btn-pill btn-pill-interrupt hidden" type="button" title="中断当前回复并立即发送新输入(Cmd/Ctrl+Enter)" aria-label="立即发送">' +
|
|
1794
|
+
'<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>' +
|
|
1795
|
+
'<span class="btn-pill-label">立即</span>' +
|
|
1753
1796
|
'</button>' +
|
|
1754
1797
|
'<button id="send-input-button" class="btn-circle btn-circle-send" title="发送">' +
|
|
1755
1798
|
'<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>' +
|
|
@@ -1757,14 +1800,20 @@
|
|
|
1757
1800
|
'</div>' +
|
|
1758
1801
|
'</div>' +
|
|
1759
1802
|
renderExpandedShortcutsRow() +
|
|
1760
|
-
// Session info bar at bottom —
|
|
1761
|
-
//
|
|
1803
|
+
// Session info bar at bottom — 仅保留信息类徽章(Claude session id / exit code)。
|
|
1804
|
+
// 自动批准已从这里移到主 pill 行(renderAutoApproveChip)。
|
|
1762
1805
|
(selectedSession
|
|
1763
|
-
?
|
|
1764
|
-
|
|
1765
|
-
(selectedSession.provider === "claude" && selectedSession.claudeSessionId
|
|
1766
|
-
|
|
1767
|
-
|
|
1806
|
+
? (function() {
|
|
1807
|
+
var bits = "";
|
|
1808
|
+
if (selectedSession.provider === "claude" && selectedSession.claudeSessionId) {
|
|
1809
|
+
bits += '<span id="claude-session-id-badge" class="claude-session-id-badge" data-claude-id="' + escapeHtml(selectedSession.claudeSessionId) + '" title="点击复制 Claude 会话 ID">☁ ' + escapeHtml(selectedSession.claudeSessionId.slice(0, 8)) + '</span>';
|
|
1810
|
+
}
|
|
1811
|
+
if (!isStructuredSession(selectedSession) && selectedSession.exitCode !== undefined && selectedSession.exitCode !== null) {
|
|
1812
|
+
if (bits) bits += '<span class="session-info-separator">|</span>';
|
|
1813
|
+
bits += '<span id="session-exit-display" class="session-exit-display">退出码=' + selectedSession.exitCode + '</span>';
|
|
1814
|
+
}
|
|
1815
|
+
return bits ? '<div class="input-session-info-bar">' + bits + '</div>' : '';
|
|
1816
|
+
})()
|
|
1768
1817
|
: '') +
|
|
1769
1818
|
'</div>' +
|
|
1770
1819
|
'<p id="action-error" class="error-message hidden"></p>' +
|
|
@@ -3213,7 +3262,9 @@
|
|
|
3213
3262
|
}
|
|
3214
3263
|
|
|
3215
3264
|
function isSidebarNarrow() {
|
|
3216
|
-
|
|
3265
|
+
// 桌面: pinned + collapsed = 56px 窄条。
|
|
3266
|
+
// 手机: pinned + collapsed 同样允许窄条(pin 单独不在手机生效,但 collapsed 是窄条形态的标志)。
|
|
3267
|
+
return !!state.sidebarPinned && !!state.sidebarCollapsed;
|
|
3217
3268
|
}
|
|
3218
3269
|
|
|
3219
3270
|
function renderCollapsedSessionTiles() {
|
|
@@ -5291,7 +5342,8 @@
|
|
|
5291
5342
|
'</div>' +
|
|
5292
5343
|
'<div class="field-hint session-kind-hint-row">' +
|
|
5293
5344
|
'<span id="session-kind-description">' + escapeHtml(getSessionKindHint(sessionKind)) + '</span>' +
|
|
5294
|
-
renderWorktreeToggle
|
|
5345
|
+
// Worktree 模式入口暂时隐藏,保留 renderWorktreeToggle/state.sessionCreateWorktree 以便后续恢复
|
|
5346
|
+
// renderWorktreeToggle(worktreeEnabled) +
|
|
5295
5347
|
'</div>' +
|
|
5296
5348
|
'</div>' +
|
|
5297
5349
|
'<div class="field">' +
|
|
@@ -5406,6 +5458,36 @@
|
|
|
5406
5458
|
}
|
|
5407
5459
|
persistElementExpandState(el, "thinking");
|
|
5408
5460
|
};
|
|
5461
|
+
// Toggle function for subagent reply bubbles — cycles preview → expanded → collapsed.
|
|
5462
|
+
// 三态循环(preview 默认 ~5 行可滚 / expanded 大区可滚 / collapsed 完全收起)。
|
|
5463
|
+
window.__subagentReplyCycle = function(e, btn) {
|
|
5464
|
+
if (e) { e.preventDefault(); e.stopPropagation(); }
|
|
5465
|
+
var bubble = btn.closest(".subagent-reply");
|
|
5466
|
+
if (!bubble) return;
|
|
5467
|
+
var modes = ["preview", "expanded", "collapsed"];
|
|
5468
|
+
var current = bubble.getAttribute("data-collapse-mode") || "preview";
|
|
5469
|
+
var idx = modes.indexOf(current);
|
|
5470
|
+
if (idx < 0) idx = 0;
|
|
5471
|
+
var next = modes[(idx + 1) % modes.length];
|
|
5472
|
+
bubble.setAttribute("data-collapse-mode", next);
|
|
5473
|
+
var label = btn.querySelector(".subagent-reply-cycle-label");
|
|
5474
|
+
var icon = btn.querySelector(".subagent-reply-cycle-icon");
|
|
5475
|
+
if (label) {
|
|
5476
|
+
label.textContent = next === "preview" ? "展开"
|
|
5477
|
+
: next === "expanded" ? "收起"
|
|
5478
|
+
: "预览";
|
|
5479
|
+
}
|
|
5480
|
+
if (icon) {
|
|
5481
|
+
icon.textContent = next === "collapsed" ? "▸"
|
|
5482
|
+
: next === "expanded" ? "▴"
|
|
5483
|
+
: "▾";
|
|
5484
|
+
}
|
|
5485
|
+
btn.setAttribute("aria-label",
|
|
5486
|
+
next === "preview" ? "点击展开全部" :
|
|
5487
|
+
next === "expanded" ? "点击完全收起" :
|
|
5488
|
+
"点击切回预览"
|
|
5489
|
+
);
|
|
5490
|
+
};
|
|
5409
5491
|
// Toggle function for inline tool rows (Read, Glob, Grep, etc.)
|
|
5410
5492
|
window.__inlineToolToggle = function(el) {
|
|
5411
5493
|
var expanded = el.classList.toggle("inline-tool-open");
|
|
@@ -5982,12 +6064,18 @@
|
|
|
5982
6064
|
var modeSelect = document.getElementById("chat-mode-select");
|
|
5983
6065
|
if (modeSelect) modeSelect.addEventListener("change", function() {
|
|
5984
6066
|
state.chatMode = this.value;
|
|
5985
|
-
|
|
6067
|
+
var label = document.getElementById("chat-mode-label");
|
|
6068
|
+
if (label) label.textContent = this.value;
|
|
6069
|
+
showToast("新会话模式:" + this.value, "info");
|
|
5986
6070
|
});
|
|
5987
6071
|
var modelSelect = document.getElementById("chat-model-select");
|
|
5988
6072
|
if (modelSelect) modelSelect.addEventListener("change", function() {
|
|
5989
6073
|
onChatModelChange(this.value);
|
|
5990
6074
|
});
|
|
6075
|
+
var thinkingSelect = document.getElementById("chat-thinking-select");
|
|
6076
|
+
if (thinkingSelect) thinkingSelect.addEventListener("change", function() {
|
|
6077
|
+
onChatThinkingChange(this.value);
|
|
6078
|
+
});
|
|
5991
6079
|
|
|
5992
6080
|
var sessionModal = document.getElementById("session-modal");
|
|
5993
6081
|
if (sessionModal) sessionModal.addEventListener("click", function(e) {
|
|
@@ -6690,6 +6778,16 @@
|
|
|
6690
6778
|
initTerminal();
|
|
6691
6779
|
setupMobileKeyboardHandlers();
|
|
6692
6780
|
setupVisualViewportHandlers();
|
|
6781
|
+
|
|
6782
|
+
// 排队条:每次 shell 重渲后,重新挂事件代理 + 刷新内容。
|
|
6783
|
+
// document-level 的 ESC / 外点击 handler 只挂一次(state.__queueBarGlobalAttached 守门)。
|
|
6784
|
+
attachQueueBarDelegates();
|
|
6785
|
+
updateQueueBar();
|
|
6786
|
+
if (!state.__queueBarGlobalAttached) {
|
|
6787
|
+
state.__queueBarGlobalAttached = true;
|
|
6788
|
+
document.addEventListener("pointerdown", handleQueueBarOutsideClick, true);
|
|
6789
|
+
document.addEventListener("keydown", handleQueueBarKeydown, true);
|
|
6790
|
+
}
|
|
6693
6791
|
}
|
|
6694
6792
|
|
|
6695
6793
|
function saveWorkingDir(path) {
|
|
@@ -7996,7 +8094,7 @@
|
|
|
7996
8094
|
// 结构化会话在出 token 时,输入框仍然可用——告诉用户默认行为是排队,
|
|
7997
8095
|
// 想插队请按右侧的 » 按钮。短语保持单行不换行。
|
|
7998
8096
|
if (isStructuredSession(session) && session.structuredState && session.structuredState.inFlight) {
|
|
7999
|
-
return "
|
|
8097
|
+
return "回复中…Enter 排队 · 旁边的「» 立即」按钮中断并立即发送";
|
|
8000
8098
|
}
|
|
8001
8099
|
return "";
|
|
8002
8100
|
}
|
|
@@ -8092,14 +8190,26 @@
|
|
|
8092
8190
|
|
|
8093
8191
|
function syncComposerModeSelect() {
|
|
8094
8192
|
var select = document.getElementById("chat-mode-select");
|
|
8095
|
-
if (!select) return;
|
|
8096
8193
|
state.chatMode = getSafeModeForTool("claude", state.chatMode);
|
|
8097
|
-
|
|
8098
|
-
|
|
8194
|
+
if (select) {
|
|
8195
|
+
select.innerHTML = renderChatModeOptionsRaw("claude", state.chatMode);
|
|
8196
|
+
select.value = state.chatMode;
|
|
8197
|
+
}
|
|
8198
|
+
var labelEl = document.getElementById("chat-mode-label");
|
|
8199
|
+
if (labelEl) labelEl.textContent = state.chatMode;
|
|
8099
8200
|
var modeHint = document.getElementById("mode-hint");
|
|
8100
8201
|
if (modeHint) modeHint.textContent = getModeHint(state.chatMode);
|
|
8101
8202
|
}
|
|
8102
8203
|
|
|
8204
|
+
// 三件套 raw 选项渲染:option 文本直接是 id(不带括号注释 / 不本地化)。
|
|
8205
|
+
function renderChatModeOptionsRaw(tool, selectedMode) {
|
|
8206
|
+
return getSupportedModes(tool).map(function(mode) {
|
|
8207
|
+
return '<option value="' + escapeHtml(mode) + '"' + (mode === selectedMode ? " selected" : "") + '>' +
|
|
8208
|
+
escapeHtml(mode) +
|
|
8209
|
+
'</option>';
|
|
8210
|
+
}).join("");
|
|
8211
|
+
}
|
|
8212
|
+
|
|
8103
8213
|
function getEffectiveModel(session) {
|
|
8104
8214
|
if (session && session.selectedModel) return session.selectedModel;
|
|
8105
8215
|
if (state.chatModel) return state.chatModel;
|
|
@@ -8127,12 +8237,139 @@
|
|
|
8127
8237
|
return html;
|
|
8128
8238
|
}
|
|
8129
8239
|
|
|
8240
|
+
// model 选项 raw 版:空值显示 "default",其它直接用 raw id(不带"(自定义)"等后缀)。
|
|
8241
|
+
function renderChatModelOptionsRaw(selected, session) {
|
|
8242
|
+
var models = getModelsForCurrentProvider(session);
|
|
8243
|
+
var html = '<option value="">default</option>';
|
|
8244
|
+
for (var i = 0; i < models.length; i++) {
|
|
8245
|
+
var m = models[i];
|
|
8246
|
+
html += '<option value="' + escapeHtml(m.id) + '"' + (m.id === selected ? " selected" : "") + '>' + escapeHtml(m.id) + '</option>';
|
|
8247
|
+
}
|
|
8248
|
+
if (selected && !models.some(function(m) { return m.id === selected; })) {
|
|
8249
|
+
html += '<option value="' + escapeHtml(selected) + '" selected>' + escapeHtml(selected) + '</option>';
|
|
8250
|
+
}
|
|
8251
|
+
return html;
|
|
8252
|
+
}
|
|
8253
|
+
|
|
8130
8254
|
function syncComposerModelSelect(session) {
|
|
8131
8255
|
var select = document.getElementById("chat-model-select");
|
|
8132
|
-
if (!select) return;
|
|
8133
8256
|
var effective = getEffectiveModel(session);
|
|
8134
|
-
|
|
8135
|
-
|
|
8257
|
+
if (select) {
|
|
8258
|
+
select.innerHTML = renderChatModelOptionsRaw(effective, session);
|
|
8259
|
+
select.value = effective;
|
|
8260
|
+
}
|
|
8261
|
+
var labelEl = document.getElementById("chat-model-label");
|
|
8262
|
+
if (labelEl) labelEl.textContent = effective || "default";
|
|
8263
|
+
// thinking 选择器与 model 选择器属于同一组"会话级设置",
|
|
8264
|
+
// 任何刷新 model 的时机也应该同步刷新 thinking,避免漂移。
|
|
8265
|
+
syncComposerThinkingSelect(session);
|
|
8266
|
+
}
|
|
8267
|
+
|
|
8268
|
+
// ── 思考深度 (thinkingEffort) —— 与 model 选择三件套对称 ──
|
|
8269
|
+
|
|
8270
|
+
// 标签直接用 Claude CLI 原生 magic word:think / think hard / ultrathink。
|
|
8271
|
+
// 这样用户一眼能对上官方文档里的思考强度档位,PTY 模式下也是这几个词被注入到 prompt 前缀。
|
|
8272
|
+
var THINKING_LEVELS = [
|
|
8273
|
+
{ id: "off", label: "off", hint: "不启用思考(CLI 无前缀;SDK 关闭 thinking;Codex --reasoning-effort minimal)" },
|
|
8274
|
+
{ id: "standard", label: "think", hint: "Claude CLI: think · SDK budget 4096 · Codex low" },
|
|
8275
|
+
{ id: "deep", label: "think hard", hint: "Claude CLI: think hard · SDK budget 16000 · Codex medium" },
|
|
8276
|
+
{ id: "max", label: "ultrathink", hint: "Claude CLI: ultrathink · SDK budget 31999 · Codex high" }
|
|
8277
|
+
];
|
|
8278
|
+
|
|
8279
|
+
function getThinkingLabel(id) {
|
|
8280
|
+
for (var i = 0; i < THINKING_LEVELS.length; i++) {
|
|
8281
|
+
if (THINKING_LEVELS[i].id === id) return THINKING_LEVELS[i].label;
|
|
8282
|
+
}
|
|
8283
|
+
return THINKING_LEVELS[0].label;
|
|
8284
|
+
}
|
|
8285
|
+
|
|
8286
|
+
function getEffectiveThinking(session) {
|
|
8287
|
+
if (session && session.thinkingEffort) return session.thinkingEffort;
|
|
8288
|
+
if (state.chatThinking) return state.chatThinking;
|
|
8289
|
+
return "off";
|
|
8290
|
+
}
|
|
8291
|
+
|
|
8292
|
+
function renderChatThinkingOptions(selected) {
|
|
8293
|
+
var v = selected || "off";
|
|
8294
|
+
var html = "";
|
|
8295
|
+
for (var i = 0; i < THINKING_LEVELS.length; i++) {
|
|
8296
|
+
var lvl = THINKING_LEVELS[i];
|
|
8297
|
+
html += '<option value="' + escapeHtml(lvl.id) + '"' + (lvl.id === v ? ' selected' : '') + ' title="' + escapeHtml(lvl.hint) + '">' + escapeHtml(lvl.label) + '</option>';
|
|
8298
|
+
}
|
|
8299
|
+
return html;
|
|
8300
|
+
}
|
|
8301
|
+
|
|
8302
|
+
// thinking 选项 raw 版:option 文本直接是 id(off / standard / deep / max)。
|
|
8303
|
+
function renderChatThinkingOptionsRaw(selected) {
|
|
8304
|
+
var v = selected || "off";
|
|
8305
|
+
var html = "";
|
|
8306
|
+
for (var i = 0; i < THINKING_LEVELS.length; i++) {
|
|
8307
|
+
var lvl = THINKING_LEVELS[i];
|
|
8308
|
+
html += '<option value="' + escapeHtml(lvl.id) + '"' + (lvl.id === v ? ' selected' : '') + '>' + escapeHtml(lvl.id) + '</option>';
|
|
8309
|
+
}
|
|
8310
|
+
return html;
|
|
8311
|
+
}
|
|
8312
|
+
|
|
8313
|
+
function syncComposerThinkingSelect(session) {
|
|
8314
|
+
var select = document.getElementById("chat-thinking-select");
|
|
8315
|
+
var effective = getEffectiveThinking(session);
|
|
8316
|
+
if (select) {
|
|
8317
|
+
select.innerHTML = renderChatThinkingOptionsRaw(effective);
|
|
8318
|
+
select.value = effective;
|
|
8319
|
+
}
|
|
8320
|
+
var labelEl = document.getElementById("chat-thinking-label");
|
|
8321
|
+
if (labelEl) labelEl.textContent = effective;
|
|
8322
|
+
}
|
|
8323
|
+
|
|
8324
|
+
function onChatThinkingChange(value) {
|
|
8325
|
+
var normalized = (value || "off").trim();
|
|
8326
|
+
if (normalized !== "off" && normalized !== "standard" && normalized !== "deep" && normalized !== "max") {
|
|
8327
|
+
normalized = "off";
|
|
8328
|
+
}
|
|
8329
|
+
state.chatThinking = normalized;
|
|
8330
|
+
try { localStorage.setItem("wand-thinking-effort", normalized); } catch (e) {}
|
|
8331
|
+
var labelEl = document.getElementById("chat-thinking-label");
|
|
8332
|
+
if (labelEl) labelEl.textContent = normalized;
|
|
8333
|
+
var session = getSelectedSession();
|
|
8334
|
+
if (!session) return;
|
|
8335
|
+
fetch("/api/sessions/" + encodeURIComponent(session.id) + "/thinking-effort", {
|
|
8336
|
+
method: "POST",
|
|
8337
|
+
headers: { "Content-Type": "application/json" },
|
|
8338
|
+
credentials: "same-origin",
|
|
8339
|
+
body: JSON.stringify({ thinkingEffort: normalized })
|
|
8340
|
+
})
|
|
8341
|
+
.then(function(res) { return res.json(); })
|
|
8342
|
+
.then(function(data) {
|
|
8343
|
+
if (data && data.error) {
|
|
8344
|
+
showToast(data.error, "error");
|
|
8345
|
+
return;
|
|
8346
|
+
}
|
|
8347
|
+
if (data && data.id) {
|
|
8348
|
+
updateSessionSnapshot(data);
|
|
8349
|
+
if (typeof showToast === "function") {
|
|
8350
|
+
showToast("已切换思考深度 → " + getThinkingLabel(normalized), "success");
|
|
8351
|
+
}
|
|
8352
|
+
}
|
|
8353
|
+
})
|
|
8354
|
+
.catch(function() { showToast("切换思考深度失败", "error"); });
|
|
8355
|
+
}
|
|
8356
|
+
|
|
8357
|
+
// 自动批准 chip:与原 .auto-approve-indicator 等价,但用统一的 .composer-pill 风格放主行。
|
|
8358
|
+
// Codex 会话固定全权限不可切;结构化 Claude 会话后端 toggle-auto-approve 路由会拒绝。
|
|
8359
|
+
// 当会话已经处于 managed / full-access 模式时,"自动批准"语义已经由模式表达,
|
|
8360
|
+
// 重复显示一个独立 chip 只会占用空间又制造歧义 —— 此时直接折叠掉。
|
|
8361
|
+
function isAutoApproveImpliedByMode(session) {
|
|
8362
|
+
if (!session) return false;
|
|
8363
|
+
var m = session.mode;
|
|
8364
|
+
return m === "managed" || m === "full-access";
|
|
8365
|
+
}
|
|
8366
|
+
function renderAutoApproveChip(session) {
|
|
8367
|
+
if (!session) return "";
|
|
8368
|
+
if (isAutoApproveImpliedByMode(session)) return "";
|
|
8369
|
+
var enabled = !!session.autoApprovePermissions;
|
|
8370
|
+
return enabled
|
|
8371
|
+
? '<span id="auto-approve-toggle" class="composer-pill composer-pill-chip auto-approve-indicator active" title="自动批准已启用 — 点击关闭">🛡 自动</span>'
|
|
8372
|
+
: '<span id="auto-approve-toggle" class="composer-pill composer-pill-chip auto-approve-indicator" title="自动批准已关闭 — 点击开启">🛡 手动</span>';
|
|
8136
8373
|
}
|
|
8137
8374
|
|
|
8138
8375
|
function fetchAvailableModels() {
|
|
@@ -8341,6 +8578,8 @@
|
|
|
8341
8578
|
var normalized = (value || "").trim();
|
|
8342
8579
|
state.chatModel = normalized;
|
|
8343
8580
|
try { localStorage.setItem("wand-chat-model", normalized); } catch (e) {}
|
|
8581
|
+
var labelEl = document.getElementById("chat-model-label");
|
|
8582
|
+
if (labelEl) labelEl.textContent = normalized || "default";
|
|
8344
8583
|
var session = getSelectedSession();
|
|
8345
8584
|
if (!session) return;
|
|
8346
8585
|
fetch("/api/sessions/" + encodeURIComponent(session.id) + "/model", {
|
|
@@ -8370,6 +8609,7 @@
|
|
|
8370
8609
|
function createStructuredSession(prompt, cwdOverride, modeOverride, worktreeEnabled) {
|
|
8371
8610
|
var provider = state.sessionTool === "codex" ? "codex" : "claude";
|
|
8372
8611
|
var modelPref = state.chatModel || (state.config && state.config.defaultModel) || "";
|
|
8612
|
+
var thinkingPref = state.chatThinking || "off";
|
|
8373
8613
|
var payload = {
|
|
8374
8614
|
cwd: cwdOverride || getEffectiveCwd(),
|
|
8375
8615
|
mode: modeOverride || state.chatMode || (state.config && state.config.defaultMode) || "default",
|
|
@@ -8377,7 +8617,8 @@
|
|
|
8377
8617
|
runner: provider === "codex" ? "codex-cli-exec" : ((state.config && state.config.structuredRunner === "sdk") ? "claude-sdk" : (state.structuredRunner || "claude-cli-print")),
|
|
8378
8618
|
prompt: prompt || undefined,
|
|
8379
8619
|
worktreeEnabled: worktreeEnabled === true,
|
|
8380
|
-
model: modelPref || undefined
|
|
8620
|
+
model: modelPref || undefined,
|
|
8621
|
+
thinkingEffort: thinkingPref
|
|
8381
8622
|
};
|
|
8382
8623
|
console.log("[WAND] createStructuredSession payload:", JSON.stringify(payload));
|
|
8383
8624
|
return fetch("/api/structured-sessions", {
|
|
@@ -8954,14 +9195,16 @@
|
|
|
8954
9195
|
var drawer = document.getElementById("sessions-drawer");
|
|
8955
9196
|
var mainLayout = document.querySelector(".main-layout");
|
|
8956
9197
|
var pinBtn = document.getElementById("sidebar-pin-btn");
|
|
8957
|
-
|
|
8958
|
-
var
|
|
9198
|
+
// 与 renderAppShell 保持一致:手机端只允许窄条形态 anchored。
|
|
9199
|
+
var isMobile = isMobileLayout();
|
|
9200
|
+
var isCollapsed = !!state.sidebarPinned && !!state.sidebarCollapsed;
|
|
9201
|
+
var isAnchored = isCollapsed || (!!state.sidebarPinned && !isMobile);
|
|
8959
9202
|
if (drawer) {
|
|
8960
|
-
drawer.classList.toggle("pinned",
|
|
9203
|
+
drawer.classList.toggle("pinned", isAnchored);
|
|
8961
9204
|
drawer.classList.toggle("collapsed", isCollapsed);
|
|
8962
9205
|
}
|
|
8963
9206
|
if (mainLayout) {
|
|
8964
|
-
mainLayout.classList.toggle("sidebar-pinned",
|
|
9207
|
+
mainLayout.classList.toggle("sidebar-pinned", isAnchored);
|
|
8965
9208
|
mainLayout.classList.toggle("sidebar-collapsed", isCollapsed);
|
|
8966
9209
|
}
|
|
8967
9210
|
if (pinBtn) {
|
|
@@ -9074,12 +9317,11 @@
|
|
|
9074
9317
|
}
|
|
9075
9318
|
|
|
9076
9319
|
function toggleSidebarCollapsed() {
|
|
9077
|
-
|
|
9320
|
+
var isMobile = isMobileLayout();
|
|
9078
9321
|
// 在 drawer 模式(未 pin)下点 collapse 视为「先固定、再收起为窄条」——
|
|
9079
9322
|
// 用户直觉是「点了就该看到窄条」,过去这里 early return 让按钮看上去没反应。
|
|
9080
9323
|
if (!state.sidebarPinned) {
|
|
9081
9324
|
state.sidebarPinned = true;
|
|
9082
|
-
state.sessionsDrawerOpen = true;
|
|
9083
9325
|
try {
|
|
9084
9326
|
localStorage.setItem("wand-sidebar-pinned", "true");
|
|
9085
9327
|
} catch (e) {}
|
|
@@ -9088,6 +9330,22 @@
|
|
|
9088
9330
|
try {
|
|
9089
9331
|
localStorage.setItem("wand-sidebar-collapsed", String(state.sidebarCollapsed));
|
|
9090
9332
|
} catch (e) {}
|
|
9333
|
+
if (state.sidebarCollapsed) {
|
|
9334
|
+
// 进入窄条形态:sessionsDrawerOpen 设 false,避免手机上 .drawer-backdrop
|
|
9335
|
+
// 仍带 .open 类导致背景遮罩误显示(窄条已经常驻显示,不需要遮罩)。
|
|
9336
|
+
state.sessionsDrawerOpen = false;
|
|
9337
|
+
} else if (isMobile) {
|
|
9338
|
+
// 手机端展开窄条:不允许「pin 但不窄条」的 300px 全栏(太占地),
|
|
9339
|
+
// 改为回到 drawer 模式并自动打开抽屉,让用户看到完整会话列表。
|
|
9340
|
+
state.sidebarPinned = false;
|
|
9341
|
+
state.sessionsDrawerOpen = true;
|
|
9342
|
+
try {
|
|
9343
|
+
localStorage.setItem("wand-sidebar-pinned", "false");
|
|
9344
|
+
} catch (e) {}
|
|
9345
|
+
} else {
|
|
9346
|
+
// 桌面端展开窄条 → 300px 全栏固定,自动打开。
|
|
9347
|
+
state.sessionsDrawerOpen = true;
|
|
9348
|
+
}
|
|
9091
9349
|
render();
|
|
9092
9350
|
var mainLayout = document.querySelector(".main-layout");
|
|
9093
9351
|
if (mainLayout) {
|
|
@@ -11916,8 +12174,14 @@
|
|
|
11916
12174
|
return Promise.resolve();
|
|
11917
12175
|
}
|
|
11918
12176
|
|
|
11919
|
-
//
|
|
11920
|
-
|
|
12177
|
+
// 防止同一会话「快速双击 / 重复触发」。原来这是个布尔 flag,绑在 fetch 的
|
|
12178
|
+
// promise 上 —— 但 structured-sessions/:id/messages 的 POST 对首条消息会 await
|
|
12179
|
+
// 整段流式 streaming,flag 会被卡到回复完才释放。结果:用户点发送 → 服务端
|
|
12180
|
+
// 流式 30s 不响应 → 这 30s 里再点发送全被这里静默 drop,看起来"排队 / 立即发送
|
|
12181
|
+
// 都没效果"。改成时间戳 + 短窗口(350ms)只挡真正的连击。idempotencyKey 已经
|
|
12182
|
+
// 在后端兜底防 webview 网络层重发,这里的 hot-path 守门只需要应付 UI 双触发。
|
|
12183
|
+
var _structuredLastSubmitAt = {};
|
|
12184
|
+
var DUPLICATE_SUBMIT_WINDOW_MS = 350;
|
|
11921
12185
|
|
|
11922
12186
|
function postStructuredInput(input, inputBox, session, opts) {
|
|
11923
12187
|
opts = opts || {};
|
|
@@ -11931,11 +12195,15 @@
|
|
|
11931
12195
|
showToast("会话不存在,请重新选择或新建会话。", "error");
|
|
11932
12196
|
return Promise.resolve();
|
|
11933
12197
|
}
|
|
11934
|
-
//
|
|
11935
|
-
|
|
11936
|
-
|
|
12198
|
+
// 短窗口内的连击当作重复点击丢掉;正常间隔的两次提交(哪怕第一次还在流式)
|
|
12199
|
+
// 都放行,让 queue / interrupt 真正生效。
|
|
12200
|
+
var nowTs = Date.now();
|
|
12201
|
+
var lastTs = _structuredLastSubmitAt[session.id] || 0;
|
|
12202
|
+
if (nowTs - lastTs < DUPLICATE_SUBMIT_WINDOW_MS) {
|
|
12203
|
+
console.log("[wand] postStructuredInput: duplicate submit (within " + DUPLICATE_SUBMIT_WINDOW_MS + "ms) ignored for session", session.id);
|
|
11937
12204
|
return Promise.resolve();
|
|
11938
12205
|
}
|
|
12206
|
+
_structuredLastSubmitAt[session.id] = nowTs;
|
|
11939
12207
|
|
|
11940
12208
|
var sessionInFlight = !!(session.structuredState && session.structuredState.inFlight && session.status === "running");
|
|
11941
12209
|
var isInterrupting = sessionInFlight && requestedInterrupt;
|
|
@@ -11960,6 +12228,9 @@
|
|
|
11960
12228
|
updateInputHint("已加入排队,等待当前回复完成…");
|
|
11961
12229
|
renderChat(true);
|
|
11962
12230
|
updateStructuredQueueCounter();
|
|
12231
|
+
// 乐观 toast:原本只在 POST 完成后才提示,Claude 流式拖太久时用户根本
|
|
12232
|
+
// 看不到反馈,会误判"点了没反应"。点击瞬间就给一条短提示。
|
|
12233
|
+
showToast(nextQueue.length > 1 ? ("已加入排队(共 " + nextQueue.length + " 条等待)") : "已加入排队,等当前回复完成会自动发送。", "info");
|
|
11963
12234
|
} else {
|
|
11964
12235
|
// 普通发送 / interrupt 发送:照旧乐观推 user turn + inFlight=true
|
|
11965
12236
|
var userTurn = { role: "user", content: [{ type: "text", text: input }] };
|
|
@@ -11978,6 +12249,11 @@
|
|
|
11978
12249
|
}), userMsgs);
|
|
11979
12250
|
updateInputHint(isInterrupting ? "已中断,正在处理新消息…" : "思考中…");
|
|
11980
12251
|
renderChat(true);
|
|
12252
|
+
// 中断模式:乐观给一条提示,让用户立刻知道"中断成功了",否则跟 queue 一样会
|
|
12253
|
+
// 觉得"点了没反应"。原 toast 在 then() 里,等 SIGTERM/HTTP roundtrip 完才出。
|
|
12254
|
+
if (isInterrupting) {
|
|
12255
|
+
showToast("已中断上一条回复,正在处理新消息…", "info");
|
|
12256
|
+
}
|
|
11981
12257
|
}
|
|
11982
12258
|
|
|
11983
12259
|
if (inputBox) {
|
|
@@ -12001,7 +12277,6 @@
|
|
|
12001
12277
|
|
|
12002
12278
|
// 用 session.id(参数绑定,in-flight 期间不变)而不是 state.selectedId
|
|
12003
12279
|
// 拼 URL,避免用户切到别的会话后 fetch 落到错误 sessionId。
|
|
12004
|
-
_structuredSubmittingSessions[session.id] = true;
|
|
12005
12280
|
return fetch("/api/structured-sessions/" + session.id + "/messages", {
|
|
12006
12281
|
method: "POST",
|
|
12007
12282
|
headers: { "Content-Type": "application/json" },
|
|
@@ -12020,7 +12295,6 @@
|
|
|
12020
12295
|
return res.json();
|
|
12021
12296
|
})
|
|
12022
12297
|
.then(function(snapshot) {
|
|
12023
|
-
_structuredSubmittingSessions[session.id] = false;
|
|
12024
12298
|
if (snapshot && snapshot.error) {
|
|
12025
12299
|
throw new Error(snapshot.error);
|
|
12026
12300
|
}
|
|
@@ -12035,18 +12309,12 @@
|
|
|
12035
12309
|
state.currentMessages = buildMessagesForRender(refreshedSession, getPreferredMessages(refreshedSession, snapshot.output, false));
|
|
12036
12310
|
renderChat(true);
|
|
12037
12311
|
updateStructuredQueueCounter();
|
|
12038
|
-
|
|
12039
|
-
|
|
12040
|
-
} else if (isQueueing) {
|
|
12041
|
-
var qLen = Array.isArray(refreshedSession.queuedMessages) ? refreshedSession.queuedMessages.length : 0;
|
|
12042
|
-
showToast(qLen > 1 ? ("已加入排队(共 " + qLen + " 条等待)") : "已加入排队,等待当前回复完成。", "info");
|
|
12043
|
-
}
|
|
12312
|
+
// toast 已在 click 时乐观 fire(见 isQueueing / isInterrupting 分支),
|
|
12313
|
+
// 这里不再重复推送,避免同一动作出两条一样的 toast。
|
|
12044
12314
|
}
|
|
12045
12315
|
}
|
|
12046
12316
|
})
|
|
12047
12317
|
.catch(function(error) {
|
|
12048
|
-
_structuredSubmittingSessions[session.id] = false;
|
|
12049
|
-
|
|
12050
12318
|
// duplicate_idempotency_key:服务端识别出 WebView 底层重发的副本,
|
|
12051
12319
|
// 直接拦截不处理。这里**不**回滚乐观更新——第一次的请求实际上已经
|
|
12052
12320
|
// 被服务端接收并处理(或正在处理),ws 推送会带回真实状态;如果在
|
|
@@ -12107,16 +12375,507 @@
|
|
|
12107
12375
|
}
|
|
12108
12376
|
|
|
12109
12377
|
function updateStructuredQueueCounter() {
|
|
12110
|
-
|
|
12111
|
-
|
|
12112
|
-
|
|
12113
|
-
|
|
12114
|
-
|
|
12115
|
-
|
|
12116
|
-
|
|
12117
|
-
|
|
12378
|
+
// 旧 #queue-counter 已下线,所有"排队"提示由 .queue-bar(输入框上方独立浮条)承担。
|
|
12379
|
+
// 函数名先保留 —— 老的调用点(postStructuredInput / WS 事件等)都还在指向它。
|
|
12380
|
+
updateQueueBar();
|
|
12381
|
+
}
|
|
12382
|
+
|
|
12383
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
12384
|
+
// 排队条(.queue-bar)—— 输入框上方独立浮条,承担三个事情:
|
|
12385
|
+
// 1) 折叠态:● 排队 N + 队尾预览 + ⌃ chevron + ⚡ 立即 按钮
|
|
12386
|
+
// 2) 展开面板:列出所有排队消息,支持拖拽换序 / 单条删除 / 一键清空
|
|
12387
|
+
// 3) 立即按钮:中断当前回复,把队首作为新消息插队发出去(剩余队列保留)
|
|
12388
|
+
// 数据源:session.queuedMessages(由后端 WS 推送 + postStructuredInput 乐观更新)。
|
|
12389
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
12390
|
+
|
|
12391
|
+
var QUEUE_BAR_MAX = 10; // 后端硬上限
|
|
12392
|
+
|
|
12393
|
+
function queueBarTruncatePreview(text) {
|
|
12394
|
+
if (typeof text !== "string") return "";
|
|
12395
|
+
var s = text.replace(/\s+/g, " ").trim();
|
|
12396
|
+
if (s.length <= 48) return s;
|
|
12397
|
+
return s.slice(0, 46) + "…";
|
|
12398
|
+
}
|
|
12399
|
+
|
|
12400
|
+
function renderQueueBarSkeleton(count, latestPreview, inFlight, atCapacity, immediateLabel) {
|
|
12401
|
+
// 折叠条 + 展开面板的 HTML 一次性渲染好,靠 .queue-bar.expanded class 切换可见性。
|
|
12402
|
+
// 这样展开/收起不需要拼字符串,纯 class toggle,动画也好做。
|
|
12403
|
+
var dotClass = inFlight ? "queue-bar-dot queue-bar-dot-pulse" : "queue-bar-dot";
|
|
12404
|
+
var barClass = "queue-bar";
|
|
12405
|
+
if (state.queueBarExpanded) barClass += " expanded";
|
|
12406
|
+
if (atCapacity) barClass += " queue-bar-capacity";
|
|
12407
|
+
if (inFlight) barClass += " queue-bar-inflight";
|
|
12408
|
+
var html =
|
|
12409
|
+
'<div class="' + barClass + '" data-queue-bar="1">' +
|
|
12410
|
+
'<button type="button" class="queue-bar-toggle" data-action="toggle"' +
|
|
12411
|
+
' aria-expanded="' + (state.queueBarExpanded ? "true" : "false") + '"' +
|
|
12412
|
+
' title="点击查看 / 收起排队消息">' +
|
|
12413
|
+
'<span class="' + dotClass + '" aria-hidden="true"></span>' +
|
|
12414
|
+
'<span class="queue-bar-count">' + (atCapacity ? "队列已满 " : "排队 ") + count + '</span>' +
|
|
12415
|
+
'<span class="queue-bar-sep" aria-hidden="true">·</span>' +
|
|
12416
|
+
'<span class="queue-bar-preview">' + escapeHtml(latestPreview) + '</span>' +
|
|
12417
|
+
'<svg class="queue-bar-chevron" width="11" height="11" viewBox="0 0 24 24"' +
|
|
12418
|
+
' fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round"' +
|
|
12419
|
+
' stroke-linejoin="round" aria-hidden="true"><polyline points="6 15 12 9 18 15"/></svg>' +
|
|
12420
|
+
'</button>' +
|
|
12421
|
+
'<span class="queue-bar-divider" aria-hidden="true"></span>' +
|
|
12422
|
+
'<button type="button" class="queue-bar-promote" data-action="promote"' +
|
|
12423
|
+
' title="中断当前回复,立刻发送队首这条" aria-label="立即发送队首">' +
|
|
12424
|
+
'<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">' +
|
|
12425
|
+
'<path d="M13 2 L4 14 L11 14 L10 22 L20 9 L13 9 Z"/>' +
|
|
12426
|
+
'</svg>' +
|
|
12427
|
+
'<span class="queue-bar-promote-label">' + escapeHtml(immediateLabel) + '</span>' +
|
|
12428
|
+
'</button>' +
|
|
12429
|
+
'<div class="queue-bar-panel" data-queue-panel="1" role="region" aria-label="排队消息列表">' +
|
|
12430
|
+
'<div class="queue-bar-panel-header">' +
|
|
12431
|
+
'<span class="queue-bar-panel-title">📥 排队中 (' + count + ')</span>' +
|
|
12432
|
+
'<button type="button" class="queue-bar-clear" data-action="clear"' +
|
|
12433
|
+
(count === 0 ? " disabled" : "") + '>清空</button>' +
|
|
12434
|
+
'<button type="button" class="queue-bar-collapse" data-action="collapse" aria-label="收起">' +
|
|
12435
|
+
'收起' +
|
|
12436
|
+
'<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor"' +
|
|
12437
|
+
' stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
|
|
12438
|
+
'<polyline points="6 9 12 15 18 9"/></svg>' +
|
|
12439
|
+
'</button>' +
|
|
12440
|
+
'</div>' +
|
|
12441
|
+
'<ol class="queue-bar-list" data-queue-list="1"></ol>' +
|
|
12442
|
+
'</div>' +
|
|
12443
|
+
'</div>';
|
|
12444
|
+
return html;
|
|
12445
|
+
}
|
|
12446
|
+
|
|
12447
|
+
function renderQueueBarItems(listEl, items) {
|
|
12448
|
+
// ol 内容单独 render —— 拖拽 / 删除 / 展开会频繁动它,外层骨架不重建避免抖动。
|
|
12449
|
+
var single = items.length <= 1;
|
|
12450
|
+
var html = "";
|
|
12451
|
+
for (var i = 0; i < items.length; i++) {
|
|
12452
|
+
var raw = items[i] == null ? "" : String(items[i]);
|
|
12453
|
+
var expanded = !!state.queueBarItemExpanded[i];
|
|
12454
|
+
var itemClass = "queue-bar-item";
|
|
12455
|
+
if (expanded) itemClass += " expanded";
|
|
12456
|
+
if (single) itemClass += " queue-bar-item-single";
|
|
12457
|
+
html +=
|
|
12458
|
+
'<li class="' + itemClass + '" data-index="' + i + '">' +
|
|
12459
|
+
'<button type="button" class="queue-bar-item-drag" data-action="drag" aria-label="拖动调整顺序"' +
|
|
12460
|
+
' title="按住拖动调整顺序"' + (single ? " disabled" : "") + '>' +
|
|
12461
|
+
'<svg width="10" height="14" viewBox="0 0 10 14" fill="currentColor" aria-hidden="true">' +
|
|
12462
|
+
'<circle cx="2.2" cy="2.2" r="1.2"/><circle cx="7.8" cy="2.2" r="1.2"/>' +
|
|
12463
|
+
'<circle cx="2.2" cy="7" r="1.2"/><circle cx="7.8" cy="7" r="1.2"/>' +
|
|
12464
|
+
'<circle cx="2.2" cy="11.8" r="1.2"/><circle cx="7.8" cy="11.8" r="1.2"/>' +
|
|
12465
|
+
'</svg>' +
|
|
12466
|
+
'</button>' +
|
|
12467
|
+
'<span class="queue-bar-item-index">#' + (i + 1) + '</span>' +
|
|
12468
|
+
'<button type="button" class="queue-bar-item-text" data-action="expand-text"' +
|
|
12469
|
+
' aria-expanded="' + (expanded ? "true" : "false") + '"' +
|
|
12470
|
+
' title="点击展开 / 收起完整内容">' +
|
|
12471
|
+
escapeHtml(raw) +
|
|
12472
|
+
'</button>' +
|
|
12473
|
+
'<button type="button" class="queue-bar-item-delete" data-action="delete"' +
|
|
12474
|
+
' aria-label="删除这条排队消息" title="删除">' +
|
|
12475
|
+
'<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor"' +
|
|
12476
|
+
' stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
|
|
12477
|
+
'<line x1="6" y1="6" x2="18" y2="18"/><line x1="6" y1="18" x2="18" y2="6"/></svg>' +
|
|
12478
|
+
'</button>' +
|
|
12479
|
+
'</li>';
|
|
12480
|
+
}
|
|
12481
|
+
listEl.innerHTML = html;
|
|
12482
|
+
}
|
|
12483
|
+
|
|
12484
|
+
function updateQueueBar() {
|
|
12485
|
+
var host = document.getElementById("queue-bar-host");
|
|
12486
|
+
if (!host) return;
|
|
12487
|
+
var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
12488
|
+
var isStructured = session && session.sessionKind === "structured";
|
|
12489
|
+
var queue = isStructured ? getStructuredQueuedInputs(session) : [];
|
|
12490
|
+
queue = Array.isArray(queue) ? queue : [];
|
|
12491
|
+
|
|
12492
|
+
if (!isStructured || queue.length === 0) {
|
|
12493
|
+
// 队列空 / 非结构化会话:整条隐藏,并清掉展开/逐条展开的本地态。
|
|
12494
|
+
host.hidden = true;
|
|
12495
|
+
host.innerHTML = "";
|
|
12496
|
+
state.queueBarExpanded = false;
|
|
12497
|
+
state.queueBarItemExpanded = {};
|
|
12498
|
+
return;
|
|
12499
|
+
}
|
|
12500
|
+
|
|
12501
|
+
host.hidden = false;
|
|
12502
|
+
var inFlight = !!(session.structuredState && session.structuredState.inFlight && session.status === "running");
|
|
12503
|
+
var atCapacity = queue.length >= QUEUE_BAR_MAX;
|
|
12504
|
+
var latest = queueBarTruncatePreview(queue[queue.length - 1]);
|
|
12505
|
+
// inFlight=false 时按钮语义从"插队"退化为"立刻发";文案一并切换让用户不疑惑。
|
|
12506
|
+
var immediateLabel = inFlight ? "立即" : "发送";
|
|
12507
|
+
|
|
12508
|
+
// 拖拽进行中绝不重建骨架,否则 pointer capture 丢失、items 闪屏。
|
|
12509
|
+
// 只更新列表内容(且如果数量不变也跳过整段重排)。
|
|
12510
|
+
var existing = host.querySelector(".queue-bar");
|
|
12511
|
+
if (state.queueBarDrag && existing) {
|
|
12512
|
+
var listInDrag = existing.querySelector('[data-queue-list="1"]');
|
|
12513
|
+
if (listInDrag && listInDrag.children.length !== queue.length) {
|
|
12514
|
+
renderQueueBarItems(listInDrag, queue);
|
|
12118
12515
|
}
|
|
12516
|
+
return;
|
|
12119
12517
|
}
|
|
12518
|
+
|
|
12519
|
+
host.innerHTML = renderQueueBarSkeleton(queue.length, latest, inFlight, atCapacity, immediateLabel);
|
|
12520
|
+
var listEl = host.querySelector('[data-queue-list="1"]');
|
|
12521
|
+
if (listEl) renderQueueBarItems(listEl, queue);
|
|
12522
|
+
}
|
|
12523
|
+
|
|
12524
|
+
// ── 折叠 / 展开 ──
|
|
12525
|
+
function setQueueBarExpanded(expanded) {
|
|
12526
|
+
var next = !!expanded;
|
|
12527
|
+
if (state.queueBarExpanded === next) return;
|
|
12528
|
+
state.queueBarExpanded = next;
|
|
12529
|
+
if (!next) state.queueBarItemExpanded = {};
|
|
12530
|
+
updateQueueBar();
|
|
12531
|
+
}
|
|
12532
|
+
function toggleQueueBar() { setQueueBarExpanded(!state.queueBarExpanded); }
|
|
12533
|
+
|
|
12534
|
+
function handleQueueBarOutsideClick(ev) {
|
|
12535
|
+
if (!state.queueBarExpanded) return;
|
|
12536
|
+
var host = document.getElementById("queue-bar-host");
|
|
12537
|
+
if (!host) return;
|
|
12538
|
+
if (host.contains(ev.target)) return;
|
|
12539
|
+
setQueueBarExpanded(false);
|
|
12540
|
+
}
|
|
12541
|
+
function handleQueueBarKeydown(ev) {
|
|
12542
|
+
if (!state.queueBarExpanded) return;
|
|
12543
|
+
if (ev.key === "Escape" || ev.key === "Esc") {
|
|
12544
|
+
setQueueBarExpanded(false);
|
|
12545
|
+
// 焦点回到 toggle 按钮,方便键盘党
|
|
12546
|
+
var toggle = document.querySelector(".queue-bar-toggle");
|
|
12547
|
+
if (toggle) toggle.focus();
|
|
12548
|
+
}
|
|
12549
|
+
}
|
|
12550
|
+
|
|
12551
|
+
// ── 单条删除 / 全部清空 / 队首插队 ──
|
|
12552
|
+
function rollbackQueueOptimistic(session, prevQueue) {
|
|
12553
|
+
updateSessionSnapshot({ id: session.id, queuedMessages: prevQueue });
|
|
12554
|
+
var refreshed = state.sessions.find(function(s) { return s.id === session.id; }) || session;
|
|
12555
|
+
state.currentMessages = buildMessagesForRender(refreshed, getPreferredMessages(refreshed, refreshed.output, false));
|
|
12556
|
+
renderChat(true);
|
|
12557
|
+
updateQueueBar();
|
|
12558
|
+
}
|
|
12559
|
+
|
|
12560
|
+
function queueBarDeleteItem(index) {
|
|
12561
|
+
var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
12562
|
+
if (!session) return;
|
|
12563
|
+
var queue = Array.isArray(session.queuedMessages) ? session.queuedMessages.slice() : [];
|
|
12564
|
+
if (index < 0 || index >= queue.length) return;
|
|
12565
|
+
var prev = queue.slice();
|
|
12566
|
+
var next = queue.slice(0, index).concat(queue.slice(index + 1));
|
|
12567
|
+
// 调整 queueBarItemExpanded 的下标偏移
|
|
12568
|
+
var nextExpanded = {};
|
|
12569
|
+
Object.keys(state.queueBarItemExpanded).forEach(function(k) {
|
|
12570
|
+
var i = Number(k);
|
|
12571
|
+
if (i === index) return;
|
|
12572
|
+
if (i > index) nextExpanded[i - 1] = state.queueBarItemExpanded[k];
|
|
12573
|
+
else nextExpanded[i] = state.queueBarItemExpanded[k];
|
|
12574
|
+
});
|
|
12575
|
+
state.queueBarItemExpanded = nextExpanded;
|
|
12576
|
+
updateSessionSnapshot({ id: session.id, queuedMessages: next });
|
|
12577
|
+
var refreshed = state.sessions.find(function(s) { return s.id === session.id; }) || session;
|
|
12578
|
+
state.currentMessages = buildMessagesForRender(refreshed, getPreferredMessages(refreshed, refreshed.output, false));
|
|
12579
|
+
renderChat(true);
|
|
12580
|
+
updateQueueBar();
|
|
12581
|
+
fetch("/api/structured-sessions/" + session.id + "/queued/" + index, {
|
|
12582
|
+
method: "DELETE",
|
|
12583
|
+
credentials: "same-origin",
|
|
12584
|
+
})
|
|
12585
|
+
.then(function(res) {
|
|
12586
|
+
if (!res.ok) {
|
|
12587
|
+
return res.json().catch(function() { return {}; }).then(function(p) {
|
|
12588
|
+
throw new Error((p && p.error) || "删除失败");
|
|
12589
|
+
});
|
|
12590
|
+
}
|
|
12591
|
+
})
|
|
12592
|
+
.catch(function(err) {
|
|
12593
|
+
rollbackQueueOptimistic(session, prev);
|
|
12594
|
+
showToast((err && err.message) || "删除排队消息失败。", "error");
|
|
12595
|
+
});
|
|
12596
|
+
}
|
|
12597
|
+
|
|
12598
|
+
function queueBarClearAll() {
|
|
12599
|
+
var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
12600
|
+
if (!session) return;
|
|
12601
|
+
var prev = Array.isArray(session.queuedMessages) ? session.queuedMessages.slice() : [];
|
|
12602
|
+
if (prev.length === 0) return;
|
|
12603
|
+
state.queueBarItemExpanded = {};
|
|
12604
|
+
updateSessionSnapshot({ id: session.id, queuedMessages: [] });
|
|
12605
|
+
var refreshed = state.sessions.find(function(s) { return s.id === session.id; }) || session;
|
|
12606
|
+
state.currentMessages = buildMessagesForRender(refreshed, getPreferredMessages(refreshed, refreshed.output, false));
|
|
12607
|
+
renderChat(true);
|
|
12608
|
+
updateQueueBar();
|
|
12609
|
+
fetch("/api/structured-sessions/" + session.id + "/queued", {
|
|
12610
|
+
method: "DELETE",
|
|
12611
|
+
credentials: "same-origin",
|
|
12612
|
+
})
|
|
12613
|
+
.then(function(res) {
|
|
12614
|
+
if (!res.ok) {
|
|
12615
|
+
return res.json().catch(function() { return {}; }).then(function(p) {
|
|
12616
|
+
throw new Error((p && p.error) || "清空失败");
|
|
12617
|
+
});
|
|
12618
|
+
}
|
|
12619
|
+
showToast("已清空 " + prev.length + " 条排队消息。", "info");
|
|
12620
|
+
})
|
|
12621
|
+
.catch(function(err) {
|
|
12622
|
+
rollbackQueueOptimistic(session, prev);
|
|
12623
|
+
showToast((err && err.message) || "清空排队消息失败。", "error");
|
|
12624
|
+
});
|
|
12625
|
+
}
|
|
12626
|
+
|
|
12627
|
+
function queueBarPromoteHead() {
|
|
12628
|
+
var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
12629
|
+
if (!session) return;
|
|
12630
|
+
var queue = Array.isArray(session.queuedMessages) ? session.queuedMessages.slice() : [];
|
|
12631
|
+
if (queue.length === 0) return;
|
|
12632
|
+
var head = queue[0];
|
|
12633
|
+
var rest = queue.slice(1);
|
|
12634
|
+
var prev = queue.slice();
|
|
12635
|
+
var inFlight = !!(session.structuredState && session.structuredState.inFlight && session.status === "running");
|
|
12636
|
+
|
|
12637
|
+
// 乐观:剥掉队首
|
|
12638
|
+
state.queueBarItemExpanded = (function() {
|
|
12639
|
+
var out = {};
|
|
12640
|
+
Object.keys(state.queueBarItemExpanded).forEach(function(k) {
|
|
12641
|
+
var i = Number(k);
|
|
12642
|
+
if (i === 0) return;
|
|
12643
|
+
out[i - 1] = state.queueBarItemExpanded[k];
|
|
12644
|
+
});
|
|
12645
|
+
return out;
|
|
12646
|
+
})();
|
|
12647
|
+
updateSessionSnapshot({ id: session.id, queuedMessages: rest });
|
|
12648
|
+
|
|
12649
|
+
// 收起面板,让用户视线回到 chat(新消息马上要进 user turn)
|
|
12650
|
+
setQueueBarExpanded(false);
|
|
12651
|
+
|
|
12652
|
+
var idempotencyKey = (typeof crypto !== "undefined" && crypto.randomUUID)
|
|
12653
|
+
? crypto.randomUUID()
|
|
12654
|
+
: (Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 10));
|
|
12655
|
+
|
|
12656
|
+
var body = { input: head, idempotencyKey: idempotencyKey };
|
|
12657
|
+
if (inFlight) {
|
|
12658
|
+
// 中断 + 保留剩余队列
|
|
12659
|
+
body.interrupt = true;
|
|
12660
|
+
body.preserveQueue = true;
|
|
12661
|
+
}
|
|
12662
|
+
// 给一个乐观 toast,让用户瞬间知道点击生效了
|
|
12663
|
+
showToast(inFlight ? "已请求中断当前回复,立即发送队首。" : "已立即发送队首消息。", "info");
|
|
12664
|
+
|
|
12665
|
+
fetch("/api/structured-sessions/" + session.id + "/messages", {
|
|
12666
|
+
method: "POST",
|
|
12667
|
+
headers: { "Content-Type": "application/json" },
|
|
12668
|
+
credentials: "same-origin",
|
|
12669
|
+
body: JSON.stringify(body),
|
|
12670
|
+
})
|
|
12671
|
+
.then(function(res) {
|
|
12672
|
+
if (!res.ok) {
|
|
12673
|
+
return res.json().catch(function() { return {}; }).then(function(p) {
|
|
12674
|
+
throw new Error((p && p.error) || "立即发送失败");
|
|
12675
|
+
});
|
|
12676
|
+
}
|
|
12677
|
+
return res.json();
|
|
12678
|
+
})
|
|
12679
|
+
.then(function(snapshot) {
|
|
12680
|
+
if (snapshot && snapshot.id) {
|
|
12681
|
+
updateSessionSnapshot(snapshot);
|
|
12682
|
+
if (snapshot.id === state.selectedId) {
|
|
12683
|
+
var refreshed = state.sessions.find(function(s) { return s.id === snapshot.id; }) || snapshot;
|
|
12684
|
+
state.currentMessages = buildMessagesForRender(refreshed, getPreferredMessages(refreshed, snapshot.output, false));
|
|
12685
|
+
renderChat(true);
|
|
12686
|
+
updateQueueBar();
|
|
12687
|
+
}
|
|
12688
|
+
}
|
|
12689
|
+
})
|
|
12690
|
+
.catch(function(err) {
|
|
12691
|
+
rollbackQueueOptimistic(session, prev);
|
|
12692
|
+
showToast((err && err.message) || "立即发送失败。", "error");
|
|
12693
|
+
});
|
|
12694
|
+
}
|
|
12695
|
+
|
|
12696
|
+
// ── 拖拽排序(Pointer Events + 简化版 sort/animate)──
|
|
12697
|
+
function queueBarDragStart(ev, handleEl) {
|
|
12698
|
+
var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
12699
|
+
if (!session) return;
|
|
12700
|
+
var queue = Array.isArray(session.queuedMessages) ? session.queuedMessages.slice() : [];
|
|
12701
|
+
if (queue.length <= 1) return;
|
|
12702
|
+
var itemEl = handleEl.closest(".queue-bar-item");
|
|
12703
|
+
if (!itemEl) return;
|
|
12704
|
+
var listEl = itemEl.parentElement;
|
|
12705
|
+
if (!listEl) return;
|
|
12706
|
+
var origIndex = Number(itemEl.getAttribute("data-index"));
|
|
12707
|
+
var siblings = Array.prototype.slice.call(listEl.children);
|
|
12708
|
+
var rects = siblings.map(function(el) { return el.getBoundingClientRect(); });
|
|
12709
|
+
var rect0 = rects[origIndex];
|
|
12710
|
+
var itemHeight = rect0.height;
|
|
12711
|
+
var gap = 6; // 与 CSS .queue-bar-list 的 gap 保持一致
|
|
12712
|
+
|
|
12713
|
+
ev.preventDefault();
|
|
12714
|
+
try { handleEl.setPointerCapture(ev.pointerId); } catch (_e) {}
|
|
12715
|
+
if (navigator && navigator.vibrate) { try { navigator.vibrate(8); } catch (_e2) {} }
|
|
12716
|
+
|
|
12717
|
+
state.queueBarDrag = {
|
|
12718
|
+
pointerId: ev.pointerId,
|
|
12719
|
+
handleEl: handleEl,
|
|
12720
|
+
itemEl: itemEl,
|
|
12721
|
+
listEl: listEl,
|
|
12722
|
+
siblings: siblings,
|
|
12723
|
+
rects: rects,
|
|
12724
|
+
origIndex: origIndex,
|
|
12725
|
+
targetIndex: origIndex,
|
|
12726
|
+
startY: ev.clientY,
|
|
12727
|
+
itemHeight: itemHeight,
|
|
12728
|
+
gap: gap,
|
|
12729
|
+
queueSnapshot: queue,
|
|
12730
|
+
};
|
|
12731
|
+
|
|
12732
|
+
itemEl.classList.add("dragging");
|
|
12733
|
+
// 把所有兄弟先标记为"参与平滑动画"
|
|
12734
|
+
siblings.forEach(function(el) { if (el !== itemEl) el.classList.add("queue-bar-item-sliding"); });
|
|
12735
|
+
|
|
12736
|
+
var move = function(e) { queueBarDragMove(e); };
|
|
12737
|
+
var up = function(e) { queueBarDragEnd(e); };
|
|
12738
|
+
state.queueBarDrag.moveHandler = move;
|
|
12739
|
+
state.queueBarDrag.upHandler = up;
|
|
12740
|
+
handleEl.addEventListener("pointermove", move);
|
|
12741
|
+
handleEl.addEventListener("pointerup", up);
|
|
12742
|
+
handleEl.addEventListener("pointercancel", up);
|
|
12743
|
+
}
|
|
12744
|
+
|
|
12745
|
+
function queueBarDragMove(ev) {
|
|
12746
|
+
var d = state.queueBarDrag;
|
|
12747
|
+
if (!d || ev.pointerId !== d.pointerId) return;
|
|
12748
|
+
ev.preventDefault();
|
|
12749
|
+
var deltaY = ev.clientY - d.startY;
|
|
12750
|
+
d.itemEl.style.transform = "translateY(" + deltaY + "px)";
|
|
12751
|
+
|
|
12752
|
+
// 拖动中心 Y 决定目标插入位置
|
|
12753
|
+
var centerY = d.rects[d.origIndex].top + d.rects[d.origIndex].height / 2 + deltaY;
|
|
12754
|
+
var target = d.origIndex;
|
|
12755
|
+
for (var i = 0; i < d.rects.length; i++) {
|
|
12756
|
+
if (i === d.origIndex) continue;
|
|
12757
|
+
var midY = d.rects[i].top + d.rects[i].height / 2;
|
|
12758
|
+
if (i < d.origIndex && centerY < midY) { target = Math.min(target, i); }
|
|
12759
|
+
else if (i > d.origIndex && centerY > midY) { target = Math.max(target, i); }
|
|
12760
|
+
}
|
|
12761
|
+
if (target !== d.targetIndex) {
|
|
12762
|
+
d.targetIndex = target;
|
|
12763
|
+
// 重排兄弟元素的 translateY
|
|
12764
|
+
var shift = d.itemHeight + d.gap;
|
|
12765
|
+
d.siblings.forEach(function(el, idx) {
|
|
12766
|
+
if (idx === d.origIndex) return;
|
|
12767
|
+
var move = 0;
|
|
12768
|
+
if (d.origIndex < target && idx > d.origIndex && idx <= target) move = -shift;
|
|
12769
|
+
else if (d.origIndex > target && idx < d.origIndex && idx >= target) move = shift;
|
|
12770
|
+
el.style.transform = move ? "translateY(" + move + "px)" : "";
|
|
12771
|
+
});
|
|
12772
|
+
}
|
|
12773
|
+
}
|
|
12774
|
+
|
|
12775
|
+
function queueBarDragEnd(ev) {
|
|
12776
|
+
var d = state.queueBarDrag;
|
|
12777
|
+
if (!d || (ev && ev.pointerId !== d.pointerId)) return;
|
|
12778
|
+
try { d.handleEl.releasePointerCapture(d.pointerId); } catch (_e) {}
|
|
12779
|
+
d.handleEl.removeEventListener("pointermove", d.moveHandler);
|
|
12780
|
+
d.handleEl.removeEventListener("pointerup", d.upHandler);
|
|
12781
|
+
d.handleEl.removeEventListener("pointercancel", d.upHandler);
|
|
12782
|
+
|
|
12783
|
+
var origIndex = d.origIndex;
|
|
12784
|
+
var targetIndex = d.targetIndex;
|
|
12785
|
+
var queueSnapshot = d.queueSnapshot;
|
|
12786
|
+
|
|
12787
|
+
// 清掉 inline transform 让 CSS 自然回位
|
|
12788
|
+
d.siblings.forEach(function(el) {
|
|
12789
|
+
el.style.transform = "";
|
|
12790
|
+
el.classList.remove("queue-bar-item-sliding");
|
|
12791
|
+
});
|
|
12792
|
+
d.itemEl.classList.remove("dragging");
|
|
12793
|
+
|
|
12794
|
+
state.queueBarDrag = null;
|
|
12795
|
+
|
|
12796
|
+
if (origIndex === targetIndex) {
|
|
12797
|
+
// 没动,光擦一下重渲就行
|
|
12798
|
+
updateQueueBar();
|
|
12799
|
+
return;
|
|
12800
|
+
}
|
|
12801
|
+
|
|
12802
|
+
// 计算 order: 原下标的新排列
|
|
12803
|
+
var order = [];
|
|
12804
|
+
for (var i = 0; i < queueSnapshot.length; i++) order.push(i);
|
|
12805
|
+
order.splice(origIndex, 1);
|
|
12806
|
+
order.splice(targetIndex, 0, origIndex);
|
|
12807
|
+
var nextQueue = order.map(function(i) { return queueSnapshot[i]; });
|
|
12808
|
+
|
|
12809
|
+
// 同步迁移 queueBarItemExpanded 下标
|
|
12810
|
+
var nextExpanded = {};
|
|
12811
|
+
Object.keys(state.queueBarItemExpanded).forEach(function(k) {
|
|
12812
|
+
var oldI = Number(k);
|
|
12813
|
+
var newI = order.indexOf(oldI);
|
|
12814
|
+
if (newI >= 0) nextExpanded[newI] = state.queueBarItemExpanded[k];
|
|
12815
|
+
});
|
|
12816
|
+
state.queueBarItemExpanded = nextExpanded;
|
|
12817
|
+
|
|
12818
|
+
var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
12819
|
+
if (!session) { updateQueueBar(); return; }
|
|
12820
|
+
updateSessionSnapshot({ id: session.id, queuedMessages: nextQueue });
|
|
12821
|
+
updateQueueBar();
|
|
12822
|
+
|
|
12823
|
+
fetch("/api/structured-sessions/" + session.id + "/queued", {
|
|
12824
|
+
method: "PATCH",
|
|
12825
|
+
headers: { "Content-Type": "application/json" },
|
|
12826
|
+
credentials: "same-origin",
|
|
12827
|
+
body: JSON.stringify({ order: order }),
|
|
12828
|
+
})
|
|
12829
|
+
.then(function(res) {
|
|
12830
|
+
if (!res.ok) {
|
|
12831
|
+
return res.json().catch(function() { return {}; }).then(function(p) {
|
|
12832
|
+
throw new Error((p && p.error) || "排序失败");
|
|
12833
|
+
});
|
|
12834
|
+
}
|
|
12835
|
+
})
|
|
12836
|
+
.catch(function(err) {
|
|
12837
|
+
rollbackQueueOptimistic(session, queueSnapshot);
|
|
12838
|
+
showToast((err && err.message) || "调整排队顺序失败。", "error");
|
|
12839
|
+
});
|
|
12840
|
+
}
|
|
12841
|
+
|
|
12842
|
+
// ── 事件代理:所有交互入口都从 #queue-bar-host 起手 ──
|
|
12843
|
+
function attachQueueBarDelegates() {
|
|
12844
|
+
var host = document.getElementById("queue-bar-host");
|
|
12845
|
+
if (!host || host.__queueDelegated) return;
|
|
12846
|
+
host.__queueDelegated = true;
|
|
12847
|
+
host.addEventListener("click", function(ev) {
|
|
12848
|
+
var actionEl = ev.target && ev.target.closest ? ev.target.closest("[data-action]") : null;
|
|
12849
|
+
if (!actionEl || !host.contains(actionEl)) return;
|
|
12850
|
+
var action = actionEl.getAttribute("data-action");
|
|
12851
|
+
if (action === "drag") return; // 拖拽由 pointerdown 处理,吞掉点击避免误触发
|
|
12852
|
+
ev.preventDefault();
|
|
12853
|
+
ev.stopPropagation();
|
|
12854
|
+
if (action === "toggle") { toggleQueueBar(); return; }
|
|
12855
|
+
if (action === "collapse") { setQueueBarExpanded(false); return; }
|
|
12856
|
+
if (action === "promote") { queueBarPromoteHead(); return; }
|
|
12857
|
+
if (action === "clear") { queueBarClearAll(); return; }
|
|
12858
|
+
if (action === "delete") {
|
|
12859
|
+
var itemEl = actionEl.closest(".queue-bar-item");
|
|
12860
|
+
if (itemEl) queueBarDeleteItem(Number(itemEl.getAttribute("data-index")));
|
|
12861
|
+
return;
|
|
12862
|
+
}
|
|
12863
|
+
if (action === "expand-text") {
|
|
12864
|
+
var item = actionEl.closest(".queue-bar-item");
|
|
12865
|
+
if (!item) return;
|
|
12866
|
+
var idx = Number(item.getAttribute("data-index"));
|
|
12867
|
+
state.queueBarItemExpanded[idx] = !state.queueBarItemExpanded[idx];
|
|
12868
|
+
item.classList.toggle("expanded", !!state.queueBarItemExpanded[idx]);
|
|
12869
|
+
actionEl.setAttribute("aria-expanded", state.queueBarItemExpanded[idx] ? "true" : "false");
|
|
12870
|
+
return;
|
|
12871
|
+
}
|
|
12872
|
+
});
|
|
12873
|
+
host.addEventListener("pointerdown", function(ev) {
|
|
12874
|
+
if (ev.button !== undefined && ev.button !== 0) return;
|
|
12875
|
+
var handle = ev.target && ev.target.closest ? ev.target.closest('[data-action="drag"]') : null;
|
|
12876
|
+
if (!handle || handle.disabled) return;
|
|
12877
|
+
queueBarDragStart(ev, handle);
|
|
12878
|
+
});
|
|
12120
12879
|
}
|
|
12121
12880
|
|
|
12122
12881
|
// 计算一条 ConversationTurn 里所有 content block 的"信息体积"——文字 / 思考 /
|
|
@@ -13087,7 +13846,8 @@
|
|
|
13087
13846
|
command: command,
|
|
13088
13847
|
cwd: cwd || "",
|
|
13089
13848
|
mode: state.chatMode || state.config.defaultMode || "default",
|
|
13090
|
-
model: modelPref || undefined
|
|
13849
|
+
model: modelPref || undefined,
|
|
13850
|
+
thinkingEffort: state.chatThinking || undefined
|
|
13091
13851
|
}))
|
|
13092
13852
|
})
|
|
13093
13853
|
.then(function(res) { return res.json(); })
|
|
@@ -13229,7 +13989,8 @@
|
|
|
13229
13989
|
cwd: defaultCwd,
|
|
13230
13990
|
mode: mode,
|
|
13231
13991
|
initialInput: value,
|
|
13232
|
-
model: modelPref || undefined
|
|
13992
|
+
model: modelPref || undefined,
|
|
13993
|
+
thinkingEffort: state.chatThinking || undefined
|
|
13233
13994
|
}))
|
|
13234
13995
|
})
|
|
13235
13996
|
.then(function(res) { return res.json(); })
|
|
@@ -13267,7 +14028,8 @@
|
|
|
13267
14028
|
cwd: defaultCwd,
|
|
13268
14029
|
mode: mode,
|
|
13269
14030
|
initialInput: value || undefined,
|
|
13270
|
-
model: modelPref || undefined
|
|
14031
|
+
model: modelPref || undefined,
|
|
14032
|
+
thinkingEffort: state.chatThinking || undefined
|
|
13271
14033
|
}))
|
|
13272
14034
|
})
|
|
13273
14035
|
.then(function(res) { return res.json(); })
|
|
@@ -14183,7 +14945,27 @@
|
|
|
14183
14945
|
// Without an immediate refit, any chunk arriving while the keyboard
|
|
14184
14946
|
// animates in renders against the old grid and tears the screen.
|
|
14185
14947
|
if (!keyboardOpen && isKeyboardOpen) {
|
|
14948
|
+
// Snapshot bottom-pinned intent BEFORE the layout starts shifting.
|
|
14949
|
+
// visualViewport.resize fires while the keyboard is still mid-
|
|
14950
|
+
// animation on iOS; if we wait until ensureTerminalFit's rAF
|
|
14951
|
+
// body executes, clientHeight has already shrunk and the user
|
|
14952
|
+
// who was visibly at the bottom registers as "scrolled up".
|
|
14953
|
+
// We pass the snapshot through to a delayed catch-up so the
|
|
14954
|
+
// final scroll lands AFTER the animation settles.
|
|
14955
|
+
var wasStickToBottom = state.terminalAutoFollow || isTerminalNearBottom();
|
|
14186
14956
|
ensureTerminalFit("keyboard-open", { forceReplay: true });
|
|
14957
|
+
// Mirror the keyboard-close 200ms delay: by then the iOS / Android
|
|
14958
|
+
// keyboard slide-in animation is done, vv.height is final, and
|
|
14959
|
+
// scrollHeight reflects the post-replay grid. One more force
|
|
14960
|
+
// scroll closes the gap between "we scrolled during animation
|
|
14961
|
+
// when scrollHeight was still in flux" and "user expects to see
|
|
14962
|
+
// the bottom now that the keyboard has fully settled".
|
|
14963
|
+
if (wasStickToBottom) {
|
|
14964
|
+
setTimeout(function() {
|
|
14965
|
+
if (!state.terminal) return;
|
|
14966
|
+
maybeScrollTerminalToBottom("force");
|
|
14967
|
+
}, 220);
|
|
14968
|
+
}
|
|
14187
14969
|
}
|
|
14188
14970
|
|
|
14189
14971
|
// Keyboard just closed — force terminal refit and scroll to bottom
|
|
@@ -14552,6 +15334,21 @@
|
|
|
14552
15334
|
ensureTerminalFitWithRetry(reason || "fit-retry", { forceReplay: forceReplay });
|
|
14553
15335
|
return false;
|
|
14554
15336
|
}
|
|
15337
|
+
// Snapshot stick-to-bottom intent NOW, before any layout work.
|
|
15338
|
+
// Two concrete bugs this guards against:
|
|
15339
|
+
// 1. Mobile keyboard opens → visualViewport shrinks → terminal
|
|
15340
|
+
// clientHeight drops while scrollTop stays put → by the time
|
|
15341
|
+
// the rAF body runs, isTerminalNearBottom() reads false even
|
|
15342
|
+
// though the user was visibly pinned to the bottom a frame ago.
|
|
15343
|
+
// 2. Any softResyncTerminal triggered below does resetTerminal()
|
|
15344
|
+
// (scrollTop snaps to 0) then re-writes; the wterm element
|
|
15345
|
+
// can fire intermediate scroll events that flip
|
|
15346
|
+
// terminalAutoFollow to false before we get a chance to
|
|
15347
|
+
// scroll back.
|
|
15348
|
+
// Both failure modes left users mid-buffer after a resize. We
|
|
15349
|
+
// capture the intent up front and use "force" below to bypass
|
|
15350
|
+
// the (now-poisoned) flag check inside maybeScrollTerminalToBottom.
|
|
15351
|
+
var shouldStickToBottom = state.terminalAutoFollow || isTerminalNearBottom();
|
|
14555
15352
|
var prevCols = state.terminal.cols;
|
|
14556
15353
|
var prevRows = state.terminal.rows;
|
|
14557
15354
|
requestAnimationFrame(function() {
|
|
@@ -14571,8 +15368,8 @@
|
|
|
14571
15368
|
if (!didResize && forceReplay && state.terminalOutput) {
|
|
14572
15369
|
softResyncTerminal({ skipFit: true });
|
|
14573
15370
|
}
|
|
14574
|
-
if (
|
|
14575
|
-
maybeScrollTerminalToBottom("
|
|
15371
|
+
if (shouldStickToBottom) {
|
|
15372
|
+
maybeScrollTerminalToBottom("force");
|
|
14576
15373
|
}
|
|
14577
15374
|
});
|
|
14578
15375
|
});
|
|
@@ -14629,9 +15426,15 @@
|
|
|
14629
15426
|
|
|
14630
15427
|
function syncTerminalSize() {
|
|
14631
15428
|
if (!state.terminal) return;
|
|
14632
|
-
|
|
14633
|
-
|
|
14634
|
-
|
|
15429
|
+
// Force-scroll (vs the weaker maybeScrollTerminalToBottom("resize"))
|
|
15430
|
+
// for the same reason ensureTerminalFit does: between this entry
|
|
15431
|
+
// and the actual scroll, the wterm DOM may fire scroll events that
|
|
15432
|
+
// poison terminalAutoFollow. Capturing intent now + force-scrolling
|
|
15433
|
+
// keeps a user who was visibly at the bottom pinned there across
|
|
15434
|
+
// window/orientation/viewport resizes.
|
|
15435
|
+
var shouldStickToBottom = state.terminalAutoFollow || isTerminalNearBottom();
|
|
15436
|
+
if (shouldStickToBottom) {
|
|
15437
|
+
maybeScrollTerminalToBottom("force");
|
|
14635
15438
|
}
|
|
14636
15439
|
sendTerminalResize(state.terminal.cols, state.terminal.rows);
|
|
14637
15440
|
}
|
|
@@ -15427,21 +16230,22 @@
|
|
|
15427
16230
|
|
|
15428
16231
|
function updateAutoApproveIndicator() {
|
|
15429
16232
|
var toggle = document.getElementById("auto-approve-toggle");
|
|
15430
|
-
if (!toggle) return;
|
|
15431
16233
|
var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
15432
|
-
|
|
15433
|
-
|
|
15434
|
-
|
|
15435
|
-
toggle
|
|
16234
|
+
// 当模式(managed / full-access)已隐含自动批准,chip 应该不存在;如果上一次渲染留下来了,
|
|
16235
|
+
// 在这里清理掉,避免视觉上还有冗余 chip。
|
|
16236
|
+
if (isAutoApproveImpliedByMode(selectedSession)) {
|
|
16237
|
+
if (toggle && toggle.parentNode) toggle.parentNode.removeChild(toggle);
|
|
15436
16238
|
return;
|
|
15437
16239
|
}
|
|
16240
|
+
if (!toggle) return;
|
|
16241
|
+
var base = "composer-pill composer-pill-chip auto-approve-indicator";
|
|
15438
16242
|
var enabled = selectedSession && selectedSession.autoApprovePermissions;
|
|
15439
16243
|
if (enabled) {
|
|
15440
|
-
toggle.className = "
|
|
16244
|
+
toggle.className = base + " active";
|
|
15441
16245
|
toggle.title = "自动批准已启用 — 点击关闭";
|
|
15442
|
-
toggle.textContent = "🛡
|
|
16246
|
+
toggle.textContent = "🛡 自动";
|
|
15443
16247
|
} else {
|
|
15444
|
-
toggle.className =
|
|
16248
|
+
toggle.className = base;
|
|
15445
16249
|
toggle.title = "自动批准已关闭 — 点击开启";
|
|
15446
16250
|
toggle.textContent = "🛡 手动";
|
|
15447
16251
|
}
|
|
@@ -15707,6 +16511,9 @@
|
|
|
15707
16511
|
// (inFlight state may have changed without new message content)
|
|
15708
16512
|
var chatMessages = chatOutput.querySelector(".chat-messages");
|
|
15709
16513
|
if (chatMessages) renderStructuredStatusBar(chatMessages, selectedSession);
|
|
16514
|
+
// 同步刷一次进度条:inFlight 从 true→false 时(turn 结束)没有新消息,
|
|
16515
|
+
// updateTodoProgress 不被调到就会让"5/6"卡在底部一直不消失。
|
|
16516
|
+
updateTodoProgress(allMessages);
|
|
15710
16517
|
return;
|
|
15711
16518
|
}
|
|
15712
16519
|
var prevHash = state.lastRenderedHash;
|
|
@@ -16110,9 +16917,19 @@
|
|
|
16110
16917
|
});
|
|
16111
16918
|
|
|
16112
16919
|
function updateTodoProgress(messages) {
|
|
16920
|
+
// 只看"当前 turn"里的 TodoWrite——即最后一条 user 消息之后的那段。
|
|
16921
|
+
// 不限制范围的话,上一轮留下的进度条会在新一轮(哪怕新一轮根本没用
|
|
16922
|
+
// TodoWrite)里阴魂不散地重现。
|
|
16923
|
+
var startIdx = 0;
|
|
16924
|
+
for (var ui = messages.length - 1; ui >= 0; ui--) {
|
|
16925
|
+
if (messages[ui] && messages[ui].role === "user") {
|
|
16926
|
+
startIdx = ui + 1;
|
|
16927
|
+
break;
|
|
16928
|
+
}
|
|
16929
|
+
}
|
|
16930
|
+
|
|
16113
16931
|
var todos = null;
|
|
16114
|
-
|
|
16115
|
-
for (var i = messages.length - 1; i >= 0; i--) {
|
|
16932
|
+
for (var i = messages.length - 1; i >= startIdx; i--) {
|
|
16116
16933
|
var msg = messages[i];
|
|
16117
16934
|
if (!msg.content || !Array.isArray(msg.content)) continue;
|
|
16118
16935
|
for (var j = msg.content.length - 1; j >= 0; j--) {
|
|
@@ -16135,6 +16952,24 @@
|
|
|
16135
16952
|
return;
|
|
16136
16953
|
}
|
|
16137
16954
|
|
|
16955
|
+
// 当前 turn 已结束(结构化 inFlight=false 或 PTY 非 running)就把进度条
|
|
16956
|
+
// 收起来——模型经常忘了发最后一条"全 completed"的 TodoWrite,让用户
|
|
16957
|
+
// 对着 "5/6" 干瞪眼很别扭。allDone 那条分支保留,提前命中更快返回。
|
|
16958
|
+
var sel = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
16959
|
+
var turnDone = false;
|
|
16960
|
+
if (sel) {
|
|
16961
|
+
if (isStructuredSession(sel)) {
|
|
16962
|
+
turnDone = !(sel.structuredState && sel.structuredState.inFlight);
|
|
16963
|
+
} else {
|
|
16964
|
+
turnDone = sel.status !== "running";
|
|
16965
|
+
}
|
|
16966
|
+
}
|
|
16967
|
+
if (turnDone) {
|
|
16968
|
+
container.classList.add("hidden");
|
|
16969
|
+
if (bodyEl) bodyEl.classList.add("hidden");
|
|
16970
|
+
return;
|
|
16971
|
+
}
|
|
16972
|
+
|
|
16138
16973
|
container.classList.remove("hidden");
|
|
16139
16974
|
if (bodyEl) bodyEl.classList.remove("hidden");
|
|
16140
16975
|
|
|
@@ -17019,7 +17854,16 @@
|
|
|
17019
17854
|
return '<div class="subagent-reply pending"><span class="typing-indicator"><span></span><span></span><span></span></span></div>';
|
|
17020
17855
|
}
|
|
17021
17856
|
|
|
17022
|
-
|
|
17857
|
+
// 三态折叠:preview(默认 ~5 行预览,内部可滚)→ expanded(高一些上限,可滚)→
|
|
17858
|
+
// collapsed(完全收起,只剩工具条)→ preview。按钮一直可见在右下,状态写在
|
|
17859
|
+
// data-collapse-mode 上,配套 CSS 控制 max-height。
|
|
17860
|
+
return '<div class="subagent-reply collapsible" data-collapse-mode="preview">' +
|
|
17861
|
+
'<div class="subagent-reply-scroll">' + renderMarkdown(text) + '</div>' +
|
|
17862
|
+
'<button type="button" class="subagent-reply-cycle" onclick="__subagentReplyCycle(event, this)" title="展开 / 收起">' +
|
|
17863
|
+
'<span class="subagent-reply-cycle-label">展开</span>' +
|
|
17864
|
+
'<span class="subagent-reply-cycle-icon" aria-hidden="true">▾</span>' +
|
|
17865
|
+
'</button>' +
|
|
17866
|
+
'</div>';
|
|
17023
17867
|
}
|
|
17024
17868
|
var PIXEL_AVATAR = {
|
|
17025
17869
|
assistant: buildPixelSvg(buildCatGrid(GARFIELD_PALETTE)),
|
|
@@ -17377,7 +18221,9 @@
|
|
|
17377
18221
|
: '';
|
|
17378
18222
|
html += '<div class="chat-handoff" style="--agent-color:' + subPalette.primary + '">' +
|
|
17379
18223
|
'<span class="chat-handoff-arrow">↳</span> ' +
|
|
17380
|
-
escapeHtml(parentPersonaName) + ' 让 <strong>' + escapeHtml(subName) + '</strong>
|
|
18224
|
+
escapeHtml(parentPersonaName) + ' 让 <strong>' + escapeHtml(subName) + '</strong>' +
|
|
18225
|
+
'<span class="chat-handoff-tag" title="子代理 / subagent">subagent</span>' +
|
|
18226
|
+
'帮忙' + desc +
|
|
17381
18227
|
'</div>';
|
|
17382
18228
|
}
|
|
17383
18229
|
html += '<div class="chat-message-segment subagent" data-agent-id="' + escapeHtml(seg.subagent.taskId) + '" style="--agent-color:' + subPalette.primary + '">' +
|