@co0ontty/wand 1.31.0 → 1.31.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.
|
@@ -106,6 +106,13 @@
|
|
|
106
106
|
terminalHealthTimer: null,
|
|
107
107
|
lastTerminalResyncAt: 0,
|
|
108
108
|
terminalAutoFollow: true,
|
|
109
|
+
// 程序触发的滚动(wand 主动 scrollTo / wterm 内部因 _shouldScrollToBottom=true
|
|
110
|
+
// 拽 scrollTop=scrollHeight)落到 scroll handler 时会被误判为"用户滚回严格
|
|
111
|
+
// 底部",把 autoFollow 反转回 true,把用户刚 wheel 上滚的意图吞掉。
|
|
112
|
+
// 存"窗口截止时间戳"而非"开始时间戳":不同调用方按各自动画长度延长窗口
|
|
113
|
+
// (瞬时 120ms 覆盖一次 rAF + 事件分发;smooth 500ms 覆盖 Chromium smooth
|
|
114
|
+
// scroll 动画),多次调用用 Math.max 合并、不会被短窗口缩短。
|
|
115
|
+
terminalProgrammaticScrollUntil: 0,
|
|
109
116
|
terminalScrollIdleTimer: null,
|
|
110
117
|
terminalScrollIdleMs: 1800,
|
|
111
118
|
terminalScrollThreshold: 12,
|
|
@@ -129,6 +136,13 @@
|
|
|
129
136
|
}
|
|
130
137
|
})(), // 跨会话排队消息 [{ id, text, cwd, mode, tool }]
|
|
131
138
|
structuredInputQueue: [], // 结构化会话同会话排队消息
|
|
139
|
+
// 排队条 UI 局部状态 ——
|
|
140
|
+
// queueBarExpanded: 折叠条点击展开成下拉面板
|
|
141
|
+
// queueBarItemExpanded: 展开面板里被点开看完整内容的 item 下标集合
|
|
142
|
+
// queueBarDrag: 拖拽排序进行中时的临时状态(pointer 捕获、起始坐标、参考 rect)
|
|
143
|
+
queueBarExpanded: false,
|
|
144
|
+
queueBarItemExpanded: {},
|
|
145
|
+
queueBarDrag: null,
|
|
132
146
|
drafts: {},
|
|
133
147
|
isSyncingInputBox: false,
|
|
134
148
|
loginPending: false,
|
|
@@ -1503,14 +1517,17 @@
|
|
|
1503
1517
|
var preferredTool = getComposerTool();
|
|
1504
1518
|
var composerMode = getSafeModeForTool(preferredTool, state.chatMode);
|
|
1505
1519
|
|
|
1506
|
-
|
|
1507
|
-
|
|
1520
|
+
// 手机端不允许「pin 但不窄条」(300px 固定边栏太占地),只允许窄条形态。
|
|
1521
|
+
// isAnchored = 边栏占据布局空间(推开主内容)。桌面 pin 或 任意端窄条都算 anchored。
|
|
1522
|
+
var isMobile = isMobileLayout();
|
|
1523
|
+
var isCollapsed = !!state.sidebarPinned && !!state.sidebarCollapsed;
|
|
1524
|
+
var isAnchored = isCollapsed || (!!state.sidebarPinned && !isMobile);
|
|
1508
1525
|
var collapsedCls = isCollapsed ? ' sidebar-collapsed' : '';
|
|
1509
1526
|
var sidebarCollapsedCls = isCollapsed ? ' collapsed' : '';
|
|
1510
1527
|
return '<div class="app-container">' +
|
|
1511
1528
|
'<div id="sessions-drawer-backdrop" class="drawer-backdrop' + drawerClass + '"></div>' +
|
|
1512
|
-
'<div class="main-layout' + (state.sessionsDrawerOpen ? ' sidebar-open' : '') + (
|
|
1513
|
-
'<aside id="sessions-drawer" class="sidebar' + drawerClass + (
|
|
1529
|
+
'<div class="main-layout' + (state.sessionsDrawerOpen ? ' sidebar-open' : '') + (isAnchored ? ' sidebar-pinned' : '') + collapsedCls + '">' +
|
|
1530
|
+
'<aside id="sessions-drawer" class="sidebar' + drawerClass + (isAnchored ? ' pinned' : '') + sidebarCollapsedCls + '">' +
|
|
1514
1531
|
'<div class="sidebar-header">' +
|
|
1515
1532
|
'<div class="sidebar-header-main">' +
|
|
1516
1533
|
'<div class="topbar-logo-icon">W</div>' +
|
|
@@ -1710,6 +1727,11 @@
|
|
|
1710
1727
|
'<ul class="todo-progress-list" id="todo-progress-list"></ul>' +
|
|
1711
1728
|
'</div>' +
|
|
1712
1729
|
'</div>' +
|
|
1730
|
+
// 排队条宿主:默认 display:none,updateQueueBar() 在 queuedMessages 非空时
|
|
1731
|
+
// 显形。结构上夹在 composer-top-row(todo 进度)和 input-composer(输入框 +
|
|
1732
|
+
// 工具栏)之间,位置正好"在输入框上方、对话框右下角"。所有内容由 updater
|
|
1733
|
+
// 注入;这里只保留稳定的外层骨架,便于 renderAppShell 全量重建后无缝复位。
|
|
1734
|
+
'<div id="queue-bar-host" class="queue-bar-host" hidden></div>' +
|
|
1713
1735
|
'<div class="input-composer">' +
|
|
1714
1736
|
'<button id="prompt-optimize-btn" class="prompt-optimize-btn" type="button" title="提示词优化(AI)" aria-label="提示词优化">' +
|
|
1715
1737
|
'<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">' +
|
|
@@ -1730,21 +1752,28 @@
|
|
|
1730
1752
|
// tabindex="-1": 把这些控件移出 iOS Safari 的表单导航链,
|
|
1731
1753
|
// 这样 textarea 聚焦时键盘上方就不会出现 ⌃ ⌄ ✓ 表单辅助栏。
|
|
1732
1754
|
'<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">' +
|
|
1733
|
-
// 三件套 (Mode / Model / Thinking) 同属"会话设置"
|
|
1734
|
-
//
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
'<span class="composer-
|
|
1745
|
-
'<span class="
|
|
1746
|
-
'<select id="chat-
|
|
1747
|
-
|
|
1755
|
+
// 三件套 (Mode / Model / Thinking) 同属"会话设置"层:扁平文字 + · 分隔。
|
|
1756
|
+
// 文字下叠一个透明 <select> 承载交互,桌面端弹原生下拉、移动端弹滚轮选择。
|
|
1757
|
+
// 显示文本用 raw id(如 default / claude-sonnet-4-5 / standard),不做翻译。
|
|
1758
|
+
'<span class="composer-text-group" role="group" aria-label="会话设置">' +
|
|
1759
|
+
'<span class="composer-text-pill" title="模式">' +
|
|
1760
|
+
'<span class="composer-text-label" id="chat-mode-label">' + escapeHtml(composerMode) + '</span>' +
|
|
1761
|
+
'<select id="chat-mode-select" class="composer-text-hidden-select" tabindex="-1" aria-label="模式">' +
|
|
1762
|
+
renderChatModeOptionsRaw(preferredTool, composerMode) +
|
|
1763
|
+
'</select>' +
|
|
1764
|
+
'</span>' +
|
|
1765
|
+
'<span class="composer-text-sep" aria-hidden="true">·</span>' +
|
|
1766
|
+
'<span class="composer-text-pill chat-model-text-pill" title="模型">' +
|
|
1767
|
+
'<span class="composer-text-label" id="chat-model-label">' + escapeHtml(getEffectiveModel(selectedSession) || "default") + '</span>' +
|
|
1768
|
+
'<select id="chat-model-select" class="composer-text-hidden-select" tabindex="-1" aria-label="模型">' +
|
|
1769
|
+
renderChatModelOptionsRaw(getEffectiveModel(selectedSession), selectedSession) +
|
|
1770
|
+
'</select>' +
|
|
1771
|
+
'</span>' +
|
|
1772
|
+
'<span class="composer-text-sep" aria-hidden="true">·</span>' +
|
|
1773
|
+
'<span class="composer-text-pill chat-thinking-text-pill" title="思考深度">' +
|
|
1774
|
+
'<span class="composer-text-label" id="chat-thinking-label">' + escapeHtml(getEffectiveThinking(selectedSession)) + '</span>' +
|
|
1775
|
+
'<select id="chat-thinking-select" class="composer-text-hidden-select" tabindex="-1" aria-label="思考深度">' +
|
|
1776
|
+
renderChatThinkingOptionsRaw(getEffectiveThinking(selectedSession)) +
|
|
1748
1777
|
'</select>' +
|
|
1749
1778
|
'</span>' +
|
|
1750
1779
|
'</span>' +
|
|
@@ -1759,9 +1788,7 @@
|
|
|
1759
1788
|
renderApprovalStatsBadge() +
|
|
1760
1789
|
'</div>' +
|
|
1761
1790
|
'<div class="input-composer-right">' +
|
|
1762
|
-
// queue-counter
|
|
1763
|
-
// 比小角标更显眼一点——加图标 + 强对比色,避免 v1.30.3 那一版用户没看见。
|
|
1764
|
-
'<span id="queue-counter" class="queue-counter hidden" title="正在排队的输入条数"><span class="queue-counter-dot"></span><span class="queue-counter-text">队列 0</span></span>' +
|
|
1791
|
+
// 排队提示从这里搬到 .queue-bar(输入框上方独立浮条),原 #queue-counter 已移除。
|
|
1765
1792
|
'<span class="input-hint' + (state.terminalInteractive ? ' terminal-interactive-hint' : state.currentView === "terminal" ? " hidden" : "") + '">' + (state.terminalInteractive ? '终端交互中 · Ctrl+C 中断 · Ctrl+L 清屏' : 'Enter 发送 · Shift+Enter 换行') + '</span>' +
|
|
1766
1793
|
renderInlineKeyboard() +
|
|
1767
1794
|
'<button id="stop-button" class="btn-circle btn-circle-stop' + (state.selectedId ? "" : " hidden") + '" title="停止">' +
|
|
@@ -3242,7 +3269,9 @@
|
|
|
3242
3269
|
}
|
|
3243
3270
|
|
|
3244
3271
|
function isSidebarNarrow() {
|
|
3245
|
-
|
|
3272
|
+
// 桌面: pinned + collapsed = 56px 窄条。
|
|
3273
|
+
// 手机: pinned + collapsed 同样允许窄条(pin 单独不在手机生效,但 collapsed 是窄条形态的标志)。
|
|
3274
|
+
return !!state.sidebarPinned && !!state.sidebarCollapsed;
|
|
3246
3275
|
}
|
|
3247
3276
|
|
|
3248
3277
|
function renderCollapsedSessionTiles() {
|
|
@@ -5320,7 +5349,8 @@
|
|
|
5320
5349
|
'</div>' +
|
|
5321
5350
|
'<div class="field-hint session-kind-hint-row">' +
|
|
5322
5351
|
'<span id="session-kind-description">' + escapeHtml(getSessionKindHint(sessionKind)) + '</span>' +
|
|
5323
|
-
renderWorktreeToggle
|
|
5352
|
+
// Worktree 模式入口暂时隐藏,保留 renderWorktreeToggle/state.sessionCreateWorktree 以便后续恢复
|
|
5353
|
+
// renderWorktreeToggle(worktreeEnabled) +
|
|
5324
5354
|
'</div>' +
|
|
5325
5355
|
'</div>' +
|
|
5326
5356
|
'<div class="field">' +
|
|
@@ -5435,6 +5465,36 @@
|
|
|
5435
5465
|
}
|
|
5436
5466
|
persistElementExpandState(el, "thinking");
|
|
5437
5467
|
};
|
|
5468
|
+
// Toggle function for subagent reply bubbles — cycles preview → expanded → collapsed.
|
|
5469
|
+
// 三态循环(preview 默认 ~5 行可滚 / expanded 大区可滚 / collapsed 完全收起)。
|
|
5470
|
+
window.__subagentReplyCycle = function(e, btn) {
|
|
5471
|
+
if (e) { e.preventDefault(); e.stopPropagation(); }
|
|
5472
|
+
var bubble = btn.closest(".subagent-reply");
|
|
5473
|
+
if (!bubble) return;
|
|
5474
|
+
var modes = ["preview", "expanded", "collapsed"];
|
|
5475
|
+
var current = bubble.getAttribute("data-collapse-mode") || "preview";
|
|
5476
|
+
var idx = modes.indexOf(current);
|
|
5477
|
+
if (idx < 0) idx = 0;
|
|
5478
|
+
var next = modes[(idx + 1) % modes.length];
|
|
5479
|
+
bubble.setAttribute("data-collapse-mode", next);
|
|
5480
|
+
var label = btn.querySelector(".subagent-reply-cycle-label");
|
|
5481
|
+
var icon = btn.querySelector(".subagent-reply-cycle-icon");
|
|
5482
|
+
if (label) {
|
|
5483
|
+
label.textContent = next === "preview" ? "展开"
|
|
5484
|
+
: next === "expanded" ? "收起"
|
|
5485
|
+
: "预览";
|
|
5486
|
+
}
|
|
5487
|
+
if (icon) {
|
|
5488
|
+
icon.textContent = next === "collapsed" ? "▸"
|
|
5489
|
+
: next === "expanded" ? "▴"
|
|
5490
|
+
: "▾";
|
|
5491
|
+
}
|
|
5492
|
+
btn.setAttribute("aria-label",
|
|
5493
|
+
next === "preview" ? "点击展开全部" :
|
|
5494
|
+
next === "expanded" ? "点击完全收起" :
|
|
5495
|
+
"点击切回预览"
|
|
5496
|
+
);
|
|
5497
|
+
};
|
|
5438
5498
|
// Toggle function for inline tool rows (Read, Glob, Grep, etc.)
|
|
5439
5499
|
window.__inlineToolToggle = function(el) {
|
|
5440
5500
|
var expanded = el.classList.toggle("inline-tool-open");
|
|
@@ -6011,7 +6071,9 @@
|
|
|
6011
6071
|
var modeSelect = document.getElementById("chat-mode-select");
|
|
6012
6072
|
if (modeSelect) modeSelect.addEventListener("change", function() {
|
|
6013
6073
|
state.chatMode = this.value;
|
|
6014
|
-
|
|
6074
|
+
var label = document.getElementById("chat-mode-label");
|
|
6075
|
+
if (label) label.textContent = this.value;
|
|
6076
|
+
showToast("新会话模式:" + this.value, "info");
|
|
6015
6077
|
});
|
|
6016
6078
|
var modelSelect = document.getElementById("chat-model-select");
|
|
6017
6079
|
if (modelSelect) modelSelect.addEventListener("change", function() {
|
|
@@ -6723,6 +6785,16 @@
|
|
|
6723
6785
|
initTerminal();
|
|
6724
6786
|
setupMobileKeyboardHandlers();
|
|
6725
6787
|
setupVisualViewportHandlers();
|
|
6788
|
+
|
|
6789
|
+
// 排队条:每次 shell 重渲后,重新挂事件代理 + 刷新内容。
|
|
6790
|
+
// document-level 的 ESC / 外点击 handler 只挂一次(state.__queueBarGlobalAttached 守门)。
|
|
6791
|
+
attachQueueBarDelegates();
|
|
6792
|
+
updateQueueBar();
|
|
6793
|
+
if (!state.__queueBarGlobalAttached) {
|
|
6794
|
+
state.__queueBarGlobalAttached = true;
|
|
6795
|
+
document.addEventListener("pointerdown", handleQueueBarOutsideClick, true);
|
|
6796
|
+
document.addEventListener("keydown", handleQueueBarKeydown, true);
|
|
6797
|
+
}
|
|
6726
6798
|
}
|
|
6727
6799
|
|
|
6728
6800
|
function saveWorkingDir(path) {
|
|
@@ -7016,6 +7088,16 @@
|
|
|
7016
7088
|
if (!state.terminal) return;
|
|
7017
7089
|
var viewport = getTerminalViewport();
|
|
7018
7090
|
if (!viewport) return;
|
|
7091
|
+
// 打"程序触发滚动"窗口:紧跟着的 scroll 事件是 wand 自己拽出来的,
|
|
7092
|
+
// scroll handler 在窗口内跳过 autoFollow 修改,避免"程序拽底 →
|
|
7093
|
+
// scroll 事件 → handler 看到在底 → autoFollow=true"的反馈环把
|
|
7094
|
+
// 用户刚 wheel 上滚的意图覆盖掉。smooth 模式 Chromium 滚动动画约
|
|
7095
|
+
// 300-500ms,瞬时滚动只需覆盖一次 rAF + 事件分发延迟。
|
|
7096
|
+
var windowMs = smooth ? 500 : 120;
|
|
7097
|
+
state.terminalProgrammaticScrollUntil = Math.max(
|
|
7098
|
+
state.terminalProgrammaticScrollUntil,
|
|
7099
|
+
Date.now() + windowMs
|
|
7100
|
+
);
|
|
7019
7101
|
if (smooth) {
|
|
7020
7102
|
viewport.scrollTo({ top: viewport.scrollHeight, behavior: "smooth" });
|
|
7021
7103
|
} else {
|
|
@@ -7422,7 +7504,36 @@
|
|
|
7422
7504
|
if (!state.wideParserState) state.wideParserState = createWideParserState();
|
|
7423
7505
|
var padded = widePadAnsi(data, state.wideParserState);
|
|
7424
7506
|
var framed = processSyncOutputFraming(padded);
|
|
7507
|
+
// wterm.write 内部用 5px 阈值判定"在底部",下一帧 _doRender 据此强制
|
|
7508
|
+
// scrollTop = scrollHeight。这与 wand 的 autoFollow("真正到底"才为
|
|
7509
|
+
// true,2px 阈值)独立,会把用户主动向上滚的几像素吞掉。覆写为 wand
|
|
7510
|
+
// 的 autoFollow 状态,让 autoFollow 成为唯一真相。
|
|
7511
|
+
//
|
|
7512
|
+
// 时序关键:必须在 terminal.write() 之前先覆写一次,否则 wterm 在 write
|
|
7513
|
+
// 内部解析 chunk 时可能同步触发 _doRender → 提前完成 scrollTop=scrollHeight,
|
|
7514
|
+
// write 之后再覆写就晚了一帧,用户上滚位置已被吞。write 之后再覆写一次
|
|
7515
|
+
// 兜底:wterm 在解析 newline / cursor move / scroll region 时可能把
|
|
7516
|
+
// _shouldScrollToBottom 改回 true。
|
|
7517
|
+
var follow = state.terminalAutoFollow !== false;
|
|
7518
|
+
if ("_shouldScrollToBottom" in terminal) {
|
|
7519
|
+
terminal._shouldScrollToBottom = follow;
|
|
7520
|
+
}
|
|
7425
7521
|
if (framed) terminal.write(framed);
|
|
7522
|
+
if ("_shouldScrollToBottom" in terminal) {
|
|
7523
|
+
terminal._shouldScrollToBottom = follow;
|
|
7524
|
+
}
|
|
7525
|
+
// wterm 按 follow=true 真的 scrollTop=scrollHeight 时会触发一次程序性的
|
|
7526
|
+
// scroll 事件 — 打窗口,让 scroll handler 不要误判为"用户滚回底部"。
|
|
7527
|
+
// **只在 follow=true 时打**:follow=false 时 wterm 不会拽底,没有程序事件
|
|
7528
|
+
// 要过滤;如果这里也打标,Claude 流式输出 chunk <120ms 一个会让窗口永
|
|
7529
|
+
// 不过期,scroll handler 永远 early return,用户哪怕滚回严格底部 autoFollow
|
|
7530
|
+
// 也回不到 true,再也走不出"上滚阅读"模式。
|
|
7531
|
+
if (follow) {
|
|
7532
|
+
state.terminalProgrammaticScrollUntil = Math.max(
|
|
7533
|
+
state.terminalProgrammaticScrollUntil,
|
|
7534
|
+
Date.now() + 120
|
|
7535
|
+
);
|
|
7536
|
+
}
|
|
7426
7537
|
// R6: 在 chunk 热路径上识别原地重绘序列(CSI nA/B/C/D/f/H/J/K),
|
|
7427
7538
|
// 节流安排一次 softResync 兜底。Claude 用相对光标位移重画菜单时,
|
|
7428
7539
|
// 如果 NEW-A 的 sync output buffer 因某种原因没拦截到完整帧(比如
|
|
@@ -7430,13 +7541,6 @@
|
|
|
7430
7541
|
// 错位。此 fallback 仅在真出现错位序列时触发,正常输出零开销。
|
|
7431
7542
|
// 与 R2 策略 A 配合:移除被动 5 处触发后,这是唯一的主动救场路径。
|
|
7432
7543
|
maybeScheduleResyncForChunk(data);
|
|
7433
|
-
// wterm.write 内部用 5px 阈值判定"在底部",下一帧 _doRender 据此强制
|
|
7434
|
-
// scrollTop = scrollHeight。这与 wand 的 autoFollow("真正到底"才为
|
|
7435
|
-
// true,2px 阈值)独立,会把用户主动向上滚的几像素吞掉。覆写为 wand
|
|
7436
|
-
// 的 autoFollow 状态,让 autoFollow 成为唯一真相。
|
|
7437
|
-
if ("_shouldScrollToBottom" in terminal) {
|
|
7438
|
-
terminal._shouldScrollToBottom = state.terminalAutoFollow !== false;
|
|
7439
|
-
}
|
|
7440
7544
|
}
|
|
7441
7545
|
|
|
7442
7546
|
function resetWideParserState() {
|
|
@@ -7854,6 +7958,16 @@
|
|
|
7854
7958
|
var viewport = getTerminalViewport();
|
|
7855
7959
|
if (viewport) {
|
|
7856
7960
|
state.terminalViewportScrollHandler = function() {
|
|
7961
|
+
// 程序触发的 scroll(wand 主动 scrollTo / wterm 内部
|
|
7962
|
+
// _doRender 因 _shouldScrollToBottom=true 拽 scrollTop=scrollHeight)
|
|
7963
|
+
// 也会进这里。如果不过滤,handler 会看到 isTerminalAtBottom()=true
|
|
7964
|
+
// 把 autoFollow 反转回 true,把用户刚上滚的意图吞掉,下一帧 chunk
|
|
7965
|
+
// 到达又被拽底,形成"上滚→拽底"反馈环。窗口长度由调用方按
|
|
7966
|
+
// 各自动画长度决定(瞬时 120ms / smooth 500ms)。
|
|
7967
|
+
if (Date.now() < state.terminalProgrammaticScrollUntil) {
|
|
7968
|
+
updateTerminalJumpToBottomButton();
|
|
7969
|
+
return;
|
|
7970
|
+
}
|
|
7857
7971
|
// 严格"真正到底"才恢复 autoFollow:避免 wheel 设 false 后被
|
|
7858
7972
|
// 紧接着的 scroll 事件因"接近底部 12px"而反转回 true。
|
|
7859
7973
|
if (isTerminalAtBottom()) {
|
|
@@ -8125,14 +8239,26 @@
|
|
|
8125
8239
|
|
|
8126
8240
|
function syncComposerModeSelect() {
|
|
8127
8241
|
var select = document.getElementById("chat-mode-select");
|
|
8128
|
-
if (!select) return;
|
|
8129
8242
|
state.chatMode = getSafeModeForTool("claude", state.chatMode);
|
|
8130
|
-
|
|
8131
|
-
|
|
8243
|
+
if (select) {
|
|
8244
|
+
select.innerHTML = renderChatModeOptionsRaw("claude", state.chatMode);
|
|
8245
|
+
select.value = state.chatMode;
|
|
8246
|
+
}
|
|
8247
|
+
var labelEl = document.getElementById("chat-mode-label");
|
|
8248
|
+
if (labelEl) labelEl.textContent = state.chatMode;
|
|
8132
8249
|
var modeHint = document.getElementById("mode-hint");
|
|
8133
8250
|
if (modeHint) modeHint.textContent = getModeHint(state.chatMode);
|
|
8134
8251
|
}
|
|
8135
8252
|
|
|
8253
|
+
// 三件套 raw 选项渲染:option 文本直接是 id(不带括号注释 / 不本地化)。
|
|
8254
|
+
function renderChatModeOptionsRaw(tool, selectedMode) {
|
|
8255
|
+
return getSupportedModes(tool).map(function(mode) {
|
|
8256
|
+
return '<option value="' + escapeHtml(mode) + '"' + (mode === selectedMode ? " selected" : "") + '>' +
|
|
8257
|
+
escapeHtml(mode) +
|
|
8258
|
+
'</option>';
|
|
8259
|
+
}).join("");
|
|
8260
|
+
}
|
|
8261
|
+
|
|
8136
8262
|
function getEffectiveModel(session) {
|
|
8137
8263
|
if (session && session.selectedModel) return session.selectedModel;
|
|
8138
8264
|
if (state.chatModel) return state.chatModel;
|
|
@@ -8160,13 +8286,29 @@
|
|
|
8160
8286
|
return html;
|
|
8161
8287
|
}
|
|
8162
8288
|
|
|
8289
|
+
// model 选项 raw 版:空值显示 "default",其它直接用 raw id(不带"(自定义)"等后缀)。
|
|
8290
|
+
function renderChatModelOptionsRaw(selected, session) {
|
|
8291
|
+
var models = getModelsForCurrentProvider(session);
|
|
8292
|
+
var html = '<option value="">default</option>';
|
|
8293
|
+
for (var i = 0; i < models.length; i++) {
|
|
8294
|
+
var m = models[i];
|
|
8295
|
+
html += '<option value="' + escapeHtml(m.id) + '"' + (m.id === selected ? " selected" : "") + '>' + escapeHtml(m.id) + '</option>';
|
|
8296
|
+
}
|
|
8297
|
+
if (selected && !models.some(function(m) { return m.id === selected; })) {
|
|
8298
|
+
html += '<option value="' + escapeHtml(selected) + '" selected>' + escapeHtml(selected) + '</option>';
|
|
8299
|
+
}
|
|
8300
|
+
return html;
|
|
8301
|
+
}
|
|
8302
|
+
|
|
8163
8303
|
function syncComposerModelSelect(session) {
|
|
8164
8304
|
var select = document.getElementById("chat-model-select");
|
|
8305
|
+
var effective = getEffectiveModel(session);
|
|
8165
8306
|
if (select) {
|
|
8166
|
-
|
|
8167
|
-
select.innerHTML = renderChatModelOptions(effective, session);
|
|
8307
|
+
select.innerHTML = renderChatModelOptionsRaw(effective, session);
|
|
8168
8308
|
select.value = effective;
|
|
8169
8309
|
}
|
|
8310
|
+
var labelEl = document.getElementById("chat-model-label");
|
|
8311
|
+
if (labelEl) labelEl.textContent = effective || "default";
|
|
8170
8312
|
// thinking 选择器与 model 选择器属于同一组"会话级设置",
|
|
8171
8313
|
// 任何刷新 model 的时机也应该同步刷新 thinking,避免漂移。
|
|
8172
8314
|
syncComposerThinkingSelect(session);
|
|
@@ -8206,14 +8348,26 @@
|
|
|
8206
8348
|
return html;
|
|
8207
8349
|
}
|
|
8208
8350
|
|
|
8351
|
+
// thinking 选项 raw 版:option 文本直接是 id(off / standard / deep / max)。
|
|
8352
|
+
function renderChatThinkingOptionsRaw(selected) {
|
|
8353
|
+
var v = selected || "off";
|
|
8354
|
+
var html = "";
|
|
8355
|
+
for (var i = 0; i < THINKING_LEVELS.length; i++) {
|
|
8356
|
+
var lvl = THINKING_LEVELS[i];
|
|
8357
|
+
html += '<option value="' + escapeHtml(lvl.id) + '"' + (lvl.id === v ? ' selected' : '') + '>' + escapeHtml(lvl.id) + '</option>';
|
|
8358
|
+
}
|
|
8359
|
+
return html;
|
|
8360
|
+
}
|
|
8361
|
+
|
|
8209
8362
|
function syncComposerThinkingSelect(session) {
|
|
8210
8363
|
var select = document.getElementById("chat-thinking-select");
|
|
8211
|
-
if (!select) return;
|
|
8212
8364
|
var effective = getEffectiveThinking(session);
|
|
8213
|
-
|
|
8214
|
-
|
|
8365
|
+
if (select) {
|
|
8366
|
+
select.innerHTML = renderChatThinkingOptionsRaw(effective);
|
|
8367
|
+
select.value = effective;
|
|
8368
|
+
}
|
|
8215
8369
|
var labelEl = document.getElementById("chat-thinking-label");
|
|
8216
|
-
if (labelEl) labelEl.textContent =
|
|
8370
|
+
if (labelEl) labelEl.textContent = effective;
|
|
8217
8371
|
}
|
|
8218
8372
|
|
|
8219
8373
|
function onChatThinkingChange(value) {
|
|
@@ -8224,7 +8378,7 @@
|
|
|
8224
8378
|
state.chatThinking = normalized;
|
|
8225
8379
|
try { localStorage.setItem("wand-thinking-effort", normalized); } catch (e) {}
|
|
8226
8380
|
var labelEl = document.getElementById("chat-thinking-label");
|
|
8227
|
-
if (labelEl) labelEl.textContent =
|
|
8381
|
+
if (labelEl) labelEl.textContent = normalized;
|
|
8228
8382
|
var session = getSelectedSession();
|
|
8229
8383
|
if (!session) return;
|
|
8230
8384
|
fetch("/api/sessions/" + encodeURIComponent(session.id) + "/thinking-effort", {
|
|
@@ -8473,6 +8627,8 @@
|
|
|
8473
8627
|
var normalized = (value || "").trim();
|
|
8474
8628
|
state.chatModel = normalized;
|
|
8475
8629
|
try { localStorage.setItem("wand-chat-model", normalized); } catch (e) {}
|
|
8630
|
+
var labelEl = document.getElementById("chat-model-label");
|
|
8631
|
+
if (labelEl) labelEl.textContent = normalized || "default";
|
|
8476
8632
|
var session = getSelectedSession();
|
|
8477
8633
|
if (!session) return;
|
|
8478
8634
|
fetch("/api/sessions/" + encodeURIComponent(session.id) + "/model", {
|
|
@@ -9088,14 +9244,16 @@
|
|
|
9088
9244
|
var drawer = document.getElementById("sessions-drawer");
|
|
9089
9245
|
var mainLayout = document.querySelector(".main-layout");
|
|
9090
9246
|
var pinBtn = document.getElementById("sidebar-pin-btn");
|
|
9091
|
-
|
|
9092
|
-
var
|
|
9247
|
+
// 与 renderAppShell 保持一致:手机端只允许窄条形态 anchored。
|
|
9248
|
+
var isMobile = isMobileLayout();
|
|
9249
|
+
var isCollapsed = !!state.sidebarPinned && !!state.sidebarCollapsed;
|
|
9250
|
+
var isAnchored = isCollapsed || (!!state.sidebarPinned && !isMobile);
|
|
9093
9251
|
if (drawer) {
|
|
9094
|
-
drawer.classList.toggle("pinned",
|
|
9252
|
+
drawer.classList.toggle("pinned", isAnchored);
|
|
9095
9253
|
drawer.classList.toggle("collapsed", isCollapsed);
|
|
9096
9254
|
}
|
|
9097
9255
|
if (mainLayout) {
|
|
9098
|
-
mainLayout.classList.toggle("sidebar-pinned",
|
|
9256
|
+
mainLayout.classList.toggle("sidebar-pinned", isAnchored);
|
|
9099
9257
|
mainLayout.classList.toggle("sidebar-collapsed", isCollapsed);
|
|
9100
9258
|
}
|
|
9101
9259
|
if (pinBtn) {
|
|
@@ -9208,12 +9366,11 @@
|
|
|
9208
9366
|
}
|
|
9209
9367
|
|
|
9210
9368
|
function toggleSidebarCollapsed() {
|
|
9211
|
-
|
|
9369
|
+
var isMobile = isMobileLayout();
|
|
9212
9370
|
// 在 drawer 模式(未 pin)下点 collapse 视为「先固定、再收起为窄条」——
|
|
9213
9371
|
// 用户直觉是「点了就该看到窄条」,过去这里 early return 让按钮看上去没反应。
|
|
9214
9372
|
if (!state.sidebarPinned) {
|
|
9215
9373
|
state.sidebarPinned = true;
|
|
9216
|
-
state.sessionsDrawerOpen = true;
|
|
9217
9374
|
try {
|
|
9218
9375
|
localStorage.setItem("wand-sidebar-pinned", "true");
|
|
9219
9376
|
} catch (e) {}
|
|
@@ -9222,6 +9379,22 @@
|
|
|
9222
9379
|
try {
|
|
9223
9380
|
localStorage.setItem("wand-sidebar-collapsed", String(state.sidebarCollapsed));
|
|
9224
9381
|
} catch (e) {}
|
|
9382
|
+
if (state.sidebarCollapsed) {
|
|
9383
|
+
// 进入窄条形态:sessionsDrawerOpen 设 false,避免手机上 .drawer-backdrop
|
|
9384
|
+
// 仍带 .open 类导致背景遮罩误显示(窄条已经常驻显示,不需要遮罩)。
|
|
9385
|
+
state.sessionsDrawerOpen = false;
|
|
9386
|
+
} else if (isMobile) {
|
|
9387
|
+
// 手机端展开窄条:不允许「pin 但不窄条」的 300px 全栏(太占地),
|
|
9388
|
+
// 改为回到 drawer 模式并自动打开抽屉,让用户看到完整会话列表。
|
|
9389
|
+
state.sidebarPinned = false;
|
|
9390
|
+
state.sessionsDrawerOpen = true;
|
|
9391
|
+
try {
|
|
9392
|
+
localStorage.setItem("wand-sidebar-pinned", "false");
|
|
9393
|
+
} catch (e) {}
|
|
9394
|
+
} else {
|
|
9395
|
+
// 桌面端展开窄条 → 300px 全栏固定,自动打开。
|
|
9396
|
+
state.sessionsDrawerOpen = true;
|
|
9397
|
+
}
|
|
9225
9398
|
render();
|
|
9226
9399
|
var mainLayout = document.querySelector(".main-layout");
|
|
9227
9400
|
if (mainLayout) {
|
|
@@ -12251,24 +12424,507 @@
|
|
|
12251
12424
|
}
|
|
12252
12425
|
|
|
12253
12426
|
function updateStructuredQueueCounter() {
|
|
12254
|
-
|
|
12255
|
-
|
|
12256
|
-
|
|
12257
|
-
|
|
12258
|
-
|
|
12259
|
-
|
|
12260
|
-
|
|
12261
|
-
|
|
12262
|
-
|
|
12263
|
-
|
|
12264
|
-
|
|
12427
|
+
// 旧 #queue-counter 已下线,所有"排队"提示由 .queue-bar(输入框上方独立浮条)承担。
|
|
12428
|
+
// 函数名先保留 —— 老的调用点(postStructuredInput / WS 事件等)都还在指向它。
|
|
12429
|
+
updateQueueBar();
|
|
12430
|
+
}
|
|
12431
|
+
|
|
12432
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
12433
|
+
// 排队条(.queue-bar)—— 输入框上方独立浮条,承担三个事情:
|
|
12434
|
+
// 1) 折叠态:● 排队 N + 队尾预览 + ⌃ chevron + ⚡ 立即 按钮
|
|
12435
|
+
// 2) 展开面板:列出所有排队消息,支持拖拽换序 / 单条删除 / 一键清空
|
|
12436
|
+
// 3) 立即按钮:中断当前回复,把队首作为新消息插队发出去(剩余队列保留)
|
|
12437
|
+
// 数据源:session.queuedMessages(由后端 WS 推送 + postStructuredInput 乐观更新)。
|
|
12438
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
12439
|
+
|
|
12440
|
+
var QUEUE_BAR_MAX = 10; // 后端硬上限
|
|
12441
|
+
|
|
12442
|
+
function queueBarTruncatePreview(text) {
|
|
12443
|
+
if (typeof text !== "string") return "";
|
|
12444
|
+
var s = text.replace(/\s+/g, " ").trim();
|
|
12445
|
+
if (s.length <= 48) return s;
|
|
12446
|
+
return s.slice(0, 46) + "…";
|
|
12447
|
+
}
|
|
12448
|
+
|
|
12449
|
+
function renderQueueBarSkeleton(count, latestPreview, inFlight, atCapacity, immediateLabel) {
|
|
12450
|
+
// 折叠条 + 展开面板的 HTML 一次性渲染好,靠 .queue-bar.expanded class 切换可见性。
|
|
12451
|
+
// 这样展开/收起不需要拼字符串,纯 class toggle,动画也好做。
|
|
12452
|
+
var dotClass = inFlight ? "queue-bar-dot queue-bar-dot-pulse" : "queue-bar-dot";
|
|
12453
|
+
var barClass = "queue-bar";
|
|
12454
|
+
if (state.queueBarExpanded) barClass += " expanded";
|
|
12455
|
+
if (atCapacity) barClass += " queue-bar-capacity";
|
|
12456
|
+
if (inFlight) barClass += " queue-bar-inflight";
|
|
12457
|
+
var html =
|
|
12458
|
+
'<div class="' + barClass + '" data-queue-bar="1">' +
|
|
12459
|
+
'<button type="button" class="queue-bar-toggle" data-action="toggle"' +
|
|
12460
|
+
' aria-expanded="' + (state.queueBarExpanded ? "true" : "false") + '"' +
|
|
12461
|
+
' title="点击查看 / 收起排队消息">' +
|
|
12462
|
+
'<span class="' + dotClass + '" aria-hidden="true"></span>' +
|
|
12463
|
+
'<span class="queue-bar-count">' + (atCapacity ? "队列已满 " : "排队 ") + count + '</span>' +
|
|
12464
|
+
'<span class="queue-bar-sep" aria-hidden="true">·</span>' +
|
|
12465
|
+
'<span class="queue-bar-preview">' + escapeHtml(latestPreview) + '</span>' +
|
|
12466
|
+
'<svg class="queue-bar-chevron" width="11" height="11" viewBox="0 0 24 24"' +
|
|
12467
|
+
' fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round"' +
|
|
12468
|
+
' stroke-linejoin="round" aria-hidden="true"><polyline points="6 15 12 9 18 15"/></svg>' +
|
|
12469
|
+
'</button>' +
|
|
12470
|
+
'<span class="queue-bar-divider" aria-hidden="true"></span>' +
|
|
12471
|
+
'<button type="button" class="queue-bar-promote" data-action="promote"' +
|
|
12472
|
+
' title="中断当前回复,立刻发送队首这条" aria-label="立即发送队首">' +
|
|
12473
|
+
'<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">' +
|
|
12474
|
+
'<path d="M13 2 L4 14 L11 14 L10 22 L20 9 L13 9 Z"/>' +
|
|
12475
|
+
'</svg>' +
|
|
12476
|
+
'<span class="queue-bar-promote-label">' + escapeHtml(immediateLabel) + '</span>' +
|
|
12477
|
+
'</button>' +
|
|
12478
|
+
'<div class="queue-bar-panel" data-queue-panel="1" role="region" aria-label="排队消息列表">' +
|
|
12479
|
+
'<div class="queue-bar-panel-header">' +
|
|
12480
|
+
'<span class="queue-bar-panel-title">📥 排队中 (' + count + ')</span>' +
|
|
12481
|
+
'<button type="button" class="queue-bar-clear" data-action="clear"' +
|
|
12482
|
+
(count === 0 ? " disabled" : "") + '>清空</button>' +
|
|
12483
|
+
'<button type="button" class="queue-bar-collapse" data-action="collapse" aria-label="收起">' +
|
|
12484
|
+
'收起' +
|
|
12485
|
+
'<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor"' +
|
|
12486
|
+
' stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
|
|
12487
|
+
'<polyline points="6 9 12 15 18 9"/></svg>' +
|
|
12488
|
+
'</button>' +
|
|
12489
|
+
'</div>' +
|
|
12490
|
+
'<ol class="queue-bar-list" data-queue-list="1"></ol>' +
|
|
12491
|
+
'</div>' +
|
|
12492
|
+
'</div>';
|
|
12493
|
+
return html;
|
|
12494
|
+
}
|
|
12495
|
+
|
|
12496
|
+
function renderQueueBarItems(listEl, items) {
|
|
12497
|
+
// ol 内容单独 render —— 拖拽 / 删除 / 展开会频繁动它,外层骨架不重建避免抖动。
|
|
12498
|
+
var single = items.length <= 1;
|
|
12499
|
+
var html = "";
|
|
12500
|
+
for (var i = 0; i < items.length; i++) {
|
|
12501
|
+
var raw = items[i] == null ? "" : String(items[i]);
|
|
12502
|
+
var expanded = !!state.queueBarItemExpanded[i];
|
|
12503
|
+
var itemClass = "queue-bar-item";
|
|
12504
|
+
if (expanded) itemClass += " expanded";
|
|
12505
|
+
if (single) itemClass += " queue-bar-item-single";
|
|
12506
|
+
html +=
|
|
12507
|
+
'<li class="' + itemClass + '" data-index="' + i + '">' +
|
|
12508
|
+
'<button type="button" class="queue-bar-item-drag" data-action="drag" aria-label="拖动调整顺序"' +
|
|
12509
|
+
' title="按住拖动调整顺序"' + (single ? " disabled" : "") + '>' +
|
|
12510
|
+
'<svg width="10" height="14" viewBox="0 0 10 14" fill="currentColor" aria-hidden="true">' +
|
|
12511
|
+
'<circle cx="2.2" cy="2.2" r="1.2"/><circle cx="7.8" cy="2.2" r="1.2"/>' +
|
|
12512
|
+
'<circle cx="2.2" cy="7" r="1.2"/><circle cx="7.8" cy="7" r="1.2"/>' +
|
|
12513
|
+
'<circle cx="2.2" cy="11.8" r="1.2"/><circle cx="7.8" cy="11.8" r="1.2"/>' +
|
|
12514
|
+
'</svg>' +
|
|
12515
|
+
'</button>' +
|
|
12516
|
+
'<span class="queue-bar-item-index">#' + (i + 1) + '</span>' +
|
|
12517
|
+
'<button type="button" class="queue-bar-item-text" data-action="expand-text"' +
|
|
12518
|
+
' aria-expanded="' + (expanded ? "true" : "false") + '"' +
|
|
12519
|
+
' title="点击展开 / 收起完整内容">' +
|
|
12520
|
+
escapeHtml(raw) +
|
|
12521
|
+
'</button>' +
|
|
12522
|
+
'<button type="button" class="queue-bar-item-delete" data-action="delete"' +
|
|
12523
|
+
' aria-label="删除这条排队消息" title="删除">' +
|
|
12524
|
+
'<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor"' +
|
|
12525
|
+
' stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
|
|
12526
|
+
'<line x1="6" y1="6" x2="18" y2="18"/><line x1="6" y1="18" x2="18" y2="6"/></svg>' +
|
|
12527
|
+
'</button>' +
|
|
12528
|
+
'</li>';
|
|
12529
|
+
}
|
|
12530
|
+
listEl.innerHTML = html;
|
|
12531
|
+
}
|
|
12532
|
+
|
|
12533
|
+
function updateQueueBar() {
|
|
12534
|
+
var host = document.getElementById("queue-bar-host");
|
|
12535
|
+
if (!host) return;
|
|
12536
|
+
var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
12537
|
+
var isStructured = session && session.sessionKind === "structured";
|
|
12538
|
+
var queue = isStructured ? getStructuredQueuedInputs(session) : [];
|
|
12539
|
+
queue = Array.isArray(queue) ? queue : [];
|
|
12540
|
+
|
|
12541
|
+
if (!isStructured || queue.length === 0) {
|
|
12542
|
+
// 队列空 / 非结构化会话:整条隐藏,并清掉展开/逐条展开的本地态。
|
|
12543
|
+
host.hidden = true;
|
|
12544
|
+
host.innerHTML = "";
|
|
12545
|
+
state.queueBarExpanded = false;
|
|
12546
|
+
state.queueBarItemExpanded = {};
|
|
12547
|
+
return;
|
|
12548
|
+
}
|
|
12549
|
+
|
|
12550
|
+
host.hidden = false;
|
|
12551
|
+
var inFlight = !!(session.structuredState && session.structuredState.inFlight && session.status === "running");
|
|
12552
|
+
var atCapacity = queue.length >= QUEUE_BAR_MAX;
|
|
12553
|
+
var latest = queueBarTruncatePreview(queue[queue.length - 1]);
|
|
12554
|
+
// inFlight=false 时按钮语义从"插队"退化为"立刻发";文案一并切换让用户不疑惑。
|
|
12555
|
+
var immediateLabel = inFlight ? "立即" : "发送";
|
|
12556
|
+
|
|
12557
|
+
// 拖拽进行中绝不重建骨架,否则 pointer capture 丢失、items 闪屏。
|
|
12558
|
+
// 只更新列表内容(且如果数量不变也跳过整段重排)。
|
|
12559
|
+
var existing = host.querySelector(".queue-bar");
|
|
12560
|
+
if (state.queueBarDrag && existing) {
|
|
12561
|
+
var listInDrag = existing.querySelector('[data-queue-list="1"]');
|
|
12562
|
+
if (listInDrag && listInDrag.children.length !== queue.length) {
|
|
12563
|
+
renderQueueBarItems(listInDrag, queue);
|
|
12265
12564
|
}
|
|
12266
|
-
|
|
12267
|
-
|
|
12268
|
-
|
|
12269
|
-
|
|
12565
|
+
return;
|
|
12566
|
+
}
|
|
12567
|
+
|
|
12568
|
+
host.innerHTML = renderQueueBarSkeleton(queue.length, latest, inFlight, atCapacity, immediateLabel);
|
|
12569
|
+
var listEl = host.querySelector('[data-queue-list="1"]');
|
|
12570
|
+
if (listEl) renderQueueBarItems(listEl, queue);
|
|
12571
|
+
}
|
|
12572
|
+
|
|
12573
|
+
// ── 折叠 / 展开 ──
|
|
12574
|
+
function setQueueBarExpanded(expanded) {
|
|
12575
|
+
var next = !!expanded;
|
|
12576
|
+
if (state.queueBarExpanded === next) return;
|
|
12577
|
+
state.queueBarExpanded = next;
|
|
12578
|
+
if (!next) state.queueBarItemExpanded = {};
|
|
12579
|
+
updateQueueBar();
|
|
12580
|
+
}
|
|
12581
|
+
function toggleQueueBar() { setQueueBarExpanded(!state.queueBarExpanded); }
|
|
12582
|
+
|
|
12583
|
+
function handleQueueBarOutsideClick(ev) {
|
|
12584
|
+
if (!state.queueBarExpanded) return;
|
|
12585
|
+
var host = document.getElementById("queue-bar-host");
|
|
12586
|
+
if (!host) return;
|
|
12587
|
+
if (host.contains(ev.target)) return;
|
|
12588
|
+
setQueueBarExpanded(false);
|
|
12589
|
+
}
|
|
12590
|
+
function handleQueueBarKeydown(ev) {
|
|
12591
|
+
if (!state.queueBarExpanded) return;
|
|
12592
|
+
if (ev.key === "Escape" || ev.key === "Esc") {
|
|
12593
|
+
setQueueBarExpanded(false);
|
|
12594
|
+
// 焦点回到 toggle 按钮,方便键盘党
|
|
12595
|
+
var toggle = document.querySelector(".queue-bar-toggle");
|
|
12596
|
+
if (toggle) toggle.focus();
|
|
12597
|
+
}
|
|
12598
|
+
}
|
|
12599
|
+
|
|
12600
|
+
// ── 单条删除 / 全部清空 / 队首插队 ──
|
|
12601
|
+
function rollbackQueueOptimistic(session, prevQueue) {
|
|
12602
|
+
updateSessionSnapshot({ id: session.id, queuedMessages: prevQueue });
|
|
12603
|
+
var refreshed = state.sessions.find(function(s) { return s.id === session.id; }) || session;
|
|
12604
|
+
state.currentMessages = buildMessagesForRender(refreshed, getPreferredMessages(refreshed, refreshed.output, false));
|
|
12605
|
+
renderChat(true);
|
|
12606
|
+
updateQueueBar();
|
|
12607
|
+
}
|
|
12608
|
+
|
|
12609
|
+
function queueBarDeleteItem(index) {
|
|
12610
|
+
var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
12611
|
+
if (!session) return;
|
|
12612
|
+
var queue = Array.isArray(session.queuedMessages) ? session.queuedMessages.slice() : [];
|
|
12613
|
+
if (index < 0 || index >= queue.length) return;
|
|
12614
|
+
var prev = queue.slice();
|
|
12615
|
+
var next = queue.slice(0, index).concat(queue.slice(index + 1));
|
|
12616
|
+
// 调整 queueBarItemExpanded 的下标偏移
|
|
12617
|
+
var nextExpanded = {};
|
|
12618
|
+
Object.keys(state.queueBarItemExpanded).forEach(function(k) {
|
|
12619
|
+
var i = Number(k);
|
|
12620
|
+
if (i === index) return;
|
|
12621
|
+
if (i > index) nextExpanded[i - 1] = state.queueBarItemExpanded[k];
|
|
12622
|
+
else nextExpanded[i] = state.queueBarItemExpanded[k];
|
|
12623
|
+
});
|
|
12624
|
+
state.queueBarItemExpanded = nextExpanded;
|
|
12625
|
+
updateSessionSnapshot({ id: session.id, queuedMessages: next });
|
|
12626
|
+
var refreshed = state.sessions.find(function(s) { return s.id === session.id; }) || session;
|
|
12627
|
+
state.currentMessages = buildMessagesForRender(refreshed, getPreferredMessages(refreshed, refreshed.output, false));
|
|
12628
|
+
renderChat(true);
|
|
12629
|
+
updateQueueBar();
|
|
12630
|
+
fetch("/api/structured-sessions/" + session.id + "/queued/" + index, {
|
|
12631
|
+
method: "DELETE",
|
|
12632
|
+
credentials: "same-origin",
|
|
12633
|
+
})
|
|
12634
|
+
.then(function(res) {
|
|
12635
|
+
if (!res.ok) {
|
|
12636
|
+
return res.json().catch(function() { return {}; }).then(function(p) {
|
|
12637
|
+
throw new Error((p && p.error) || "删除失败");
|
|
12638
|
+
});
|
|
12639
|
+
}
|
|
12640
|
+
})
|
|
12641
|
+
.catch(function(err) {
|
|
12642
|
+
rollbackQueueOptimistic(session, prev);
|
|
12643
|
+
showToast((err && err.message) || "删除排队消息失败。", "error");
|
|
12644
|
+
});
|
|
12645
|
+
}
|
|
12646
|
+
|
|
12647
|
+
function queueBarClearAll() {
|
|
12648
|
+
var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
12649
|
+
if (!session) return;
|
|
12650
|
+
var prev = Array.isArray(session.queuedMessages) ? session.queuedMessages.slice() : [];
|
|
12651
|
+
if (prev.length === 0) return;
|
|
12652
|
+
state.queueBarItemExpanded = {};
|
|
12653
|
+
updateSessionSnapshot({ id: session.id, queuedMessages: [] });
|
|
12654
|
+
var refreshed = state.sessions.find(function(s) { return s.id === session.id; }) || session;
|
|
12655
|
+
state.currentMessages = buildMessagesForRender(refreshed, getPreferredMessages(refreshed, refreshed.output, false));
|
|
12656
|
+
renderChat(true);
|
|
12657
|
+
updateQueueBar();
|
|
12658
|
+
fetch("/api/structured-sessions/" + session.id + "/queued", {
|
|
12659
|
+
method: "DELETE",
|
|
12660
|
+
credentials: "same-origin",
|
|
12661
|
+
})
|
|
12662
|
+
.then(function(res) {
|
|
12663
|
+
if (!res.ok) {
|
|
12664
|
+
return res.json().catch(function() { return {}; }).then(function(p) {
|
|
12665
|
+
throw new Error((p && p.error) || "清空失败");
|
|
12666
|
+
});
|
|
12667
|
+
}
|
|
12668
|
+
showToast("已清空 " + prev.length + " 条排队消息。", "info");
|
|
12669
|
+
})
|
|
12670
|
+
.catch(function(err) {
|
|
12671
|
+
rollbackQueueOptimistic(session, prev);
|
|
12672
|
+
showToast((err && err.message) || "清空排队消息失败。", "error");
|
|
12673
|
+
});
|
|
12674
|
+
}
|
|
12675
|
+
|
|
12676
|
+
function queueBarPromoteHead() {
|
|
12677
|
+
var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
12678
|
+
if (!session) return;
|
|
12679
|
+
var queue = Array.isArray(session.queuedMessages) ? session.queuedMessages.slice() : [];
|
|
12680
|
+
if (queue.length === 0) return;
|
|
12681
|
+
var head = queue[0];
|
|
12682
|
+
var rest = queue.slice(1);
|
|
12683
|
+
var prev = queue.slice();
|
|
12684
|
+
var inFlight = !!(session.structuredState && session.structuredState.inFlight && session.status === "running");
|
|
12685
|
+
|
|
12686
|
+
// 乐观:剥掉队首
|
|
12687
|
+
state.queueBarItemExpanded = (function() {
|
|
12688
|
+
var out = {};
|
|
12689
|
+
Object.keys(state.queueBarItemExpanded).forEach(function(k) {
|
|
12690
|
+
var i = Number(k);
|
|
12691
|
+
if (i === 0) return;
|
|
12692
|
+
out[i - 1] = state.queueBarItemExpanded[k];
|
|
12693
|
+
});
|
|
12694
|
+
return out;
|
|
12695
|
+
})();
|
|
12696
|
+
updateSessionSnapshot({ id: session.id, queuedMessages: rest });
|
|
12697
|
+
|
|
12698
|
+
// 收起面板,让用户视线回到 chat(新消息马上要进 user turn)
|
|
12699
|
+
setQueueBarExpanded(false);
|
|
12700
|
+
|
|
12701
|
+
var idempotencyKey = (typeof crypto !== "undefined" && crypto.randomUUID)
|
|
12702
|
+
? crypto.randomUUID()
|
|
12703
|
+
: (Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 10));
|
|
12704
|
+
|
|
12705
|
+
var body = { input: head, idempotencyKey: idempotencyKey };
|
|
12706
|
+
if (inFlight) {
|
|
12707
|
+
// 中断 + 保留剩余队列
|
|
12708
|
+
body.interrupt = true;
|
|
12709
|
+
body.preserveQueue = true;
|
|
12710
|
+
}
|
|
12711
|
+
// 给一个乐观 toast,让用户瞬间知道点击生效了
|
|
12712
|
+
showToast(inFlight ? "已请求中断当前回复,立即发送队首。" : "已立即发送队首消息。", "info");
|
|
12713
|
+
|
|
12714
|
+
fetch("/api/structured-sessions/" + session.id + "/messages", {
|
|
12715
|
+
method: "POST",
|
|
12716
|
+
headers: { "Content-Type": "application/json" },
|
|
12717
|
+
credentials: "same-origin",
|
|
12718
|
+
body: JSON.stringify(body),
|
|
12719
|
+
})
|
|
12720
|
+
.then(function(res) {
|
|
12721
|
+
if (!res.ok) {
|
|
12722
|
+
return res.json().catch(function() { return {}; }).then(function(p) {
|
|
12723
|
+
throw new Error((p && p.error) || "立即发送失败");
|
|
12724
|
+
});
|
|
12725
|
+
}
|
|
12726
|
+
return res.json();
|
|
12727
|
+
})
|
|
12728
|
+
.then(function(snapshot) {
|
|
12729
|
+
if (snapshot && snapshot.id) {
|
|
12730
|
+
updateSessionSnapshot(snapshot);
|
|
12731
|
+
if (snapshot.id === state.selectedId) {
|
|
12732
|
+
var refreshed = state.sessions.find(function(s) { return s.id === snapshot.id; }) || snapshot;
|
|
12733
|
+
state.currentMessages = buildMessagesForRender(refreshed, getPreferredMessages(refreshed, snapshot.output, false));
|
|
12734
|
+
renderChat(true);
|
|
12735
|
+
updateQueueBar();
|
|
12736
|
+
}
|
|
12270
12737
|
}
|
|
12738
|
+
})
|
|
12739
|
+
.catch(function(err) {
|
|
12740
|
+
rollbackQueueOptimistic(session, prev);
|
|
12741
|
+
showToast((err && err.message) || "立即发送失败。", "error");
|
|
12742
|
+
});
|
|
12743
|
+
}
|
|
12744
|
+
|
|
12745
|
+
// ── 拖拽排序(Pointer Events + 简化版 sort/animate)──
|
|
12746
|
+
function queueBarDragStart(ev, handleEl) {
|
|
12747
|
+
var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
12748
|
+
if (!session) return;
|
|
12749
|
+
var queue = Array.isArray(session.queuedMessages) ? session.queuedMessages.slice() : [];
|
|
12750
|
+
if (queue.length <= 1) return;
|
|
12751
|
+
var itemEl = handleEl.closest(".queue-bar-item");
|
|
12752
|
+
if (!itemEl) return;
|
|
12753
|
+
var listEl = itemEl.parentElement;
|
|
12754
|
+
if (!listEl) return;
|
|
12755
|
+
var origIndex = Number(itemEl.getAttribute("data-index"));
|
|
12756
|
+
var siblings = Array.prototype.slice.call(listEl.children);
|
|
12757
|
+
var rects = siblings.map(function(el) { return el.getBoundingClientRect(); });
|
|
12758
|
+
var rect0 = rects[origIndex];
|
|
12759
|
+
var itemHeight = rect0.height;
|
|
12760
|
+
var gap = 6; // 与 CSS .queue-bar-list 的 gap 保持一致
|
|
12761
|
+
|
|
12762
|
+
ev.preventDefault();
|
|
12763
|
+
try { handleEl.setPointerCapture(ev.pointerId); } catch (_e) {}
|
|
12764
|
+
if (navigator && navigator.vibrate) { try { navigator.vibrate(8); } catch (_e2) {} }
|
|
12765
|
+
|
|
12766
|
+
state.queueBarDrag = {
|
|
12767
|
+
pointerId: ev.pointerId,
|
|
12768
|
+
handleEl: handleEl,
|
|
12769
|
+
itemEl: itemEl,
|
|
12770
|
+
listEl: listEl,
|
|
12771
|
+
siblings: siblings,
|
|
12772
|
+
rects: rects,
|
|
12773
|
+
origIndex: origIndex,
|
|
12774
|
+
targetIndex: origIndex,
|
|
12775
|
+
startY: ev.clientY,
|
|
12776
|
+
itemHeight: itemHeight,
|
|
12777
|
+
gap: gap,
|
|
12778
|
+
queueSnapshot: queue,
|
|
12779
|
+
};
|
|
12780
|
+
|
|
12781
|
+
itemEl.classList.add("dragging");
|
|
12782
|
+
// 把所有兄弟先标记为"参与平滑动画"
|
|
12783
|
+
siblings.forEach(function(el) { if (el !== itemEl) el.classList.add("queue-bar-item-sliding"); });
|
|
12784
|
+
|
|
12785
|
+
var move = function(e) { queueBarDragMove(e); };
|
|
12786
|
+
var up = function(e) { queueBarDragEnd(e); };
|
|
12787
|
+
state.queueBarDrag.moveHandler = move;
|
|
12788
|
+
state.queueBarDrag.upHandler = up;
|
|
12789
|
+
handleEl.addEventListener("pointermove", move);
|
|
12790
|
+
handleEl.addEventListener("pointerup", up);
|
|
12791
|
+
handleEl.addEventListener("pointercancel", up);
|
|
12792
|
+
}
|
|
12793
|
+
|
|
12794
|
+
function queueBarDragMove(ev) {
|
|
12795
|
+
var d = state.queueBarDrag;
|
|
12796
|
+
if (!d || ev.pointerId !== d.pointerId) return;
|
|
12797
|
+
ev.preventDefault();
|
|
12798
|
+
var deltaY = ev.clientY - d.startY;
|
|
12799
|
+
d.itemEl.style.transform = "translateY(" + deltaY + "px)";
|
|
12800
|
+
|
|
12801
|
+
// 拖动中心 Y 决定目标插入位置
|
|
12802
|
+
var centerY = d.rects[d.origIndex].top + d.rects[d.origIndex].height / 2 + deltaY;
|
|
12803
|
+
var target = d.origIndex;
|
|
12804
|
+
for (var i = 0; i < d.rects.length; i++) {
|
|
12805
|
+
if (i === d.origIndex) continue;
|
|
12806
|
+
var midY = d.rects[i].top + d.rects[i].height / 2;
|
|
12807
|
+
if (i < d.origIndex && centerY < midY) { target = Math.min(target, i); }
|
|
12808
|
+
else if (i > d.origIndex && centerY > midY) { target = Math.max(target, i); }
|
|
12809
|
+
}
|
|
12810
|
+
if (target !== d.targetIndex) {
|
|
12811
|
+
d.targetIndex = target;
|
|
12812
|
+
// 重排兄弟元素的 translateY
|
|
12813
|
+
var shift = d.itemHeight + d.gap;
|
|
12814
|
+
d.siblings.forEach(function(el, idx) {
|
|
12815
|
+
if (idx === d.origIndex) return;
|
|
12816
|
+
var move = 0;
|
|
12817
|
+
if (d.origIndex < target && idx > d.origIndex && idx <= target) move = -shift;
|
|
12818
|
+
else if (d.origIndex > target && idx < d.origIndex && idx >= target) move = shift;
|
|
12819
|
+
el.style.transform = move ? "translateY(" + move + "px)" : "";
|
|
12820
|
+
});
|
|
12821
|
+
}
|
|
12822
|
+
}
|
|
12823
|
+
|
|
12824
|
+
function queueBarDragEnd(ev) {
|
|
12825
|
+
var d = state.queueBarDrag;
|
|
12826
|
+
if (!d || (ev && ev.pointerId !== d.pointerId)) return;
|
|
12827
|
+
try { d.handleEl.releasePointerCapture(d.pointerId); } catch (_e) {}
|
|
12828
|
+
d.handleEl.removeEventListener("pointermove", d.moveHandler);
|
|
12829
|
+
d.handleEl.removeEventListener("pointerup", d.upHandler);
|
|
12830
|
+
d.handleEl.removeEventListener("pointercancel", d.upHandler);
|
|
12831
|
+
|
|
12832
|
+
var origIndex = d.origIndex;
|
|
12833
|
+
var targetIndex = d.targetIndex;
|
|
12834
|
+
var queueSnapshot = d.queueSnapshot;
|
|
12835
|
+
|
|
12836
|
+
// 清掉 inline transform 让 CSS 自然回位
|
|
12837
|
+
d.siblings.forEach(function(el) {
|
|
12838
|
+
el.style.transform = "";
|
|
12839
|
+
el.classList.remove("queue-bar-item-sliding");
|
|
12840
|
+
});
|
|
12841
|
+
d.itemEl.classList.remove("dragging");
|
|
12842
|
+
|
|
12843
|
+
state.queueBarDrag = null;
|
|
12844
|
+
|
|
12845
|
+
if (origIndex === targetIndex) {
|
|
12846
|
+
// 没动,光擦一下重渲就行
|
|
12847
|
+
updateQueueBar();
|
|
12848
|
+
return;
|
|
12271
12849
|
}
|
|
12850
|
+
|
|
12851
|
+
// 计算 order: 原下标的新排列
|
|
12852
|
+
var order = [];
|
|
12853
|
+
for (var i = 0; i < queueSnapshot.length; i++) order.push(i);
|
|
12854
|
+
order.splice(origIndex, 1);
|
|
12855
|
+
order.splice(targetIndex, 0, origIndex);
|
|
12856
|
+
var nextQueue = order.map(function(i) { return queueSnapshot[i]; });
|
|
12857
|
+
|
|
12858
|
+
// 同步迁移 queueBarItemExpanded 下标
|
|
12859
|
+
var nextExpanded = {};
|
|
12860
|
+
Object.keys(state.queueBarItemExpanded).forEach(function(k) {
|
|
12861
|
+
var oldI = Number(k);
|
|
12862
|
+
var newI = order.indexOf(oldI);
|
|
12863
|
+
if (newI >= 0) nextExpanded[newI] = state.queueBarItemExpanded[k];
|
|
12864
|
+
});
|
|
12865
|
+
state.queueBarItemExpanded = nextExpanded;
|
|
12866
|
+
|
|
12867
|
+
var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
12868
|
+
if (!session) { updateQueueBar(); return; }
|
|
12869
|
+
updateSessionSnapshot({ id: session.id, queuedMessages: nextQueue });
|
|
12870
|
+
updateQueueBar();
|
|
12871
|
+
|
|
12872
|
+
fetch("/api/structured-sessions/" + session.id + "/queued", {
|
|
12873
|
+
method: "PATCH",
|
|
12874
|
+
headers: { "Content-Type": "application/json" },
|
|
12875
|
+
credentials: "same-origin",
|
|
12876
|
+
body: JSON.stringify({ order: order }),
|
|
12877
|
+
})
|
|
12878
|
+
.then(function(res) {
|
|
12879
|
+
if (!res.ok) {
|
|
12880
|
+
return res.json().catch(function() { return {}; }).then(function(p) {
|
|
12881
|
+
throw new Error((p && p.error) || "排序失败");
|
|
12882
|
+
});
|
|
12883
|
+
}
|
|
12884
|
+
})
|
|
12885
|
+
.catch(function(err) {
|
|
12886
|
+
rollbackQueueOptimistic(session, queueSnapshot);
|
|
12887
|
+
showToast((err && err.message) || "调整排队顺序失败。", "error");
|
|
12888
|
+
});
|
|
12889
|
+
}
|
|
12890
|
+
|
|
12891
|
+
// ── 事件代理:所有交互入口都从 #queue-bar-host 起手 ──
|
|
12892
|
+
function attachQueueBarDelegates() {
|
|
12893
|
+
var host = document.getElementById("queue-bar-host");
|
|
12894
|
+
if (!host || host.__queueDelegated) return;
|
|
12895
|
+
host.__queueDelegated = true;
|
|
12896
|
+
host.addEventListener("click", function(ev) {
|
|
12897
|
+
var actionEl = ev.target && ev.target.closest ? ev.target.closest("[data-action]") : null;
|
|
12898
|
+
if (!actionEl || !host.contains(actionEl)) return;
|
|
12899
|
+
var action = actionEl.getAttribute("data-action");
|
|
12900
|
+
if (action === "drag") return; // 拖拽由 pointerdown 处理,吞掉点击避免误触发
|
|
12901
|
+
ev.preventDefault();
|
|
12902
|
+
ev.stopPropagation();
|
|
12903
|
+
if (action === "toggle") { toggleQueueBar(); return; }
|
|
12904
|
+
if (action === "collapse") { setQueueBarExpanded(false); return; }
|
|
12905
|
+
if (action === "promote") { queueBarPromoteHead(); return; }
|
|
12906
|
+
if (action === "clear") { queueBarClearAll(); return; }
|
|
12907
|
+
if (action === "delete") {
|
|
12908
|
+
var itemEl = actionEl.closest(".queue-bar-item");
|
|
12909
|
+
if (itemEl) queueBarDeleteItem(Number(itemEl.getAttribute("data-index")));
|
|
12910
|
+
return;
|
|
12911
|
+
}
|
|
12912
|
+
if (action === "expand-text") {
|
|
12913
|
+
var item = actionEl.closest(".queue-bar-item");
|
|
12914
|
+
if (!item) return;
|
|
12915
|
+
var idx = Number(item.getAttribute("data-index"));
|
|
12916
|
+
state.queueBarItemExpanded[idx] = !state.queueBarItemExpanded[idx];
|
|
12917
|
+
item.classList.toggle("expanded", !!state.queueBarItemExpanded[idx]);
|
|
12918
|
+
actionEl.setAttribute("aria-expanded", state.queueBarItemExpanded[idx] ? "true" : "false");
|
|
12919
|
+
return;
|
|
12920
|
+
}
|
|
12921
|
+
});
|
|
12922
|
+
host.addEventListener("pointerdown", function(ev) {
|
|
12923
|
+
if (ev.button !== undefined && ev.button !== 0) return;
|
|
12924
|
+
var handle = ev.target && ev.target.closest ? ev.target.closest('[data-action="drag"]') : null;
|
|
12925
|
+
if (!handle || handle.disabled) return;
|
|
12926
|
+
queueBarDragStart(ev, handle);
|
|
12927
|
+
});
|
|
12272
12928
|
}
|
|
12273
12929
|
|
|
12274
12930
|
// 计算一条 ConversationTurn 里所有 content block 的"信息体积"——文字 / 思考 /
|
|
@@ -15904,6 +16560,9 @@
|
|
|
15904
16560
|
// (inFlight state may have changed without new message content)
|
|
15905
16561
|
var chatMessages = chatOutput.querySelector(".chat-messages");
|
|
15906
16562
|
if (chatMessages) renderStructuredStatusBar(chatMessages, selectedSession);
|
|
16563
|
+
// 同步刷一次进度条:inFlight 从 true→false 时(turn 结束)没有新消息,
|
|
16564
|
+
// updateTodoProgress 不被调到就会让"5/6"卡在底部一直不消失。
|
|
16565
|
+
updateTodoProgress(allMessages);
|
|
15907
16566
|
return;
|
|
15908
16567
|
}
|
|
15909
16568
|
var prevHash = state.lastRenderedHash;
|
|
@@ -16307,9 +16966,19 @@
|
|
|
16307
16966
|
});
|
|
16308
16967
|
|
|
16309
16968
|
function updateTodoProgress(messages) {
|
|
16969
|
+
// 只看"当前 turn"里的 TodoWrite——即最后一条 user 消息之后的那段。
|
|
16970
|
+
// 不限制范围的话,上一轮留下的进度条会在新一轮(哪怕新一轮根本没用
|
|
16971
|
+
// TodoWrite)里阴魂不散地重现。
|
|
16972
|
+
var startIdx = 0;
|
|
16973
|
+
for (var ui = messages.length - 1; ui >= 0; ui--) {
|
|
16974
|
+
if (messages[ui] && messages[ui].role === "user") {
|
|
16975
|
+
startIdx = ui + 1;
|
|
16976
|
+
break;
|
|
16977
|
+
}
|
|
16978
|
+
}
|
|
16979
|
+
|
|
16310
16980
|
var todos = null;
|
|
16311
|
-
|
|
16312
|
-
for (var i = messages.length - 1; i >= 0; i--) {
|
|
16981
|
+
for (var i = messages.length - 1; i >= startIdx; i--) {
|
|
16313
16982
|
var msg = messages[i];
|
|
16314
16983
|
if (!msg.content || !Array.isArray(msg.content)) continue;
|
|
16315
16984
|
for (var j = msg.content.length - 1; j >= 0; j--) {
|
|
@@ -16332,6 +17001,24 @@
|
|
|
16332
17001
|
return;
|
|
16333
17002
|
}
|
|
16334
17003
|
|
|
17004
|
+
// 当前 turn 已结束(结构化 inFlight=false 或 PTY 非 running)就把进度条
|
|
17005
|
+
// 收起来——模型经常忘了发最后一条"全 completed"的 TodoWrite,让用户
|
|
17006
|
+
// 对着 "5/6" 干瞪眼很别扭。allDone 那条分支保留,提前命中更快返回。
|
|
17007
|
+
var sel = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
17008
|
+
var turnDone = false;
|
|
17009
|
+
if (sel) {
|
|
17010
|
+
if (isStructuredSession(sel)) {
|
|
17011
|
+
turnDone = !(sel.structuredState && sel.structuredState.inFlight);
|
|
17012
|
+
} else {
|
|
17013
|
+
turnDone = sel.status !== "running";
|
|
17014
|
+
}
|
|
17015
|
+
}
|
|
17016
|
+
if (turnDone) {
|
|
17017
|
+
container.classList.add("hidden");
|
|
17018
|
+
if (bodyEl) bodyEl.classList.add("hidden");
|
|
17019
|
+
return;
|
|
17020
|
+
}
|
|
17021
|
+
|
|
16335
17022
|
container.classList.remove("hidden");
|
|
16336
17023
|
if (bodyEl) bodyEl.classList.remove("hidden");
|
|
16337
17024
|
|
|
@@ -17216,7 +17903,16 @@
|
|
|
17216
17903
|
return '<div class="subagent-reply pending"><span class="typing-indicator"><span></span><span></span><span></span></span></div>';
|
|
17217
17904
|
}
|
|
17218
17905
|
|
|
17219
|
-
|
|
17906
|
+
// 三态折叠:preview(默认 ~5 行预览,内部可滚)→ expanded(高一些上限,可滚)→
|
|
17907
|
+
// collapsed(完全收起,只剩工具条)→ preview。按钮一直可见在右下,状态写在
|
|
17908
|
+
// data-collapse-mode 上,配套 CSS 控制 max-height。
|
|
17909
|
+
return '<div class="subagent-reply collapsible" data-collapse-mode="preview">' +
|
|
17910
|
+
'<div class="subagent-reply-scroll">' + renderMarkdown(text) + '</div>' +
|
|
17911
|
+
'<button type="button" class="subagent-reply-cycle" onclick="__subagentReplyCycle(event, this)" title="展开 / 收起">' +
|
|
17912
|
+
'<span class="subagent-reply-cycle-label">展开</span>' +
|
|
17913
|
+
'<span class="subagent-reply-cycle-icon" aria-hidden="true">▾</span>' +
|
|
17914
|
+
'</button>' +
|
|
17915
|
+
'</div>';
|
|
17220
17916
|
}
|
|
17221
17917
|
var PIXEL_AVATAR = {
|
|
17222
17918
|
assistant: buildPixelSvg(buildCatGrid(GARFIELD_PALETTE)),
|
|
@@ -17574,7 +18270,9 @@
|
|
|
17574
18270
|
: '';
|
|
17575
18271
|
html += '<div class="chat-handoff" style="--agent-color:' + subPalette.primary + '">' +
|
|
17576
18272
|
'<span class="chat-handoff-arrow">↳</span> ' +
|
|
17577
|
-
escapeHtml(parentPersonaName) + ' 让 <strong>' + escapeHtml(subName) + '</strong>
|
|
18273
|
+
escapeHtml(parentPersonaName) + ' 让 <strong>' + escapeHtml(subName) + '</strong>' +
|
|
18274
|
+
'<span class="chat-handoff-tag" title="子代理 / subagent">subagent</span>' +
|
|
18275
|
+
'帮忙' + desc +
|
|
17578
18276
|
'</div>';
|
|
17579
18277
|
}
|
|
17580
18278
|
html += '<div class="chat-message-segment subagent" data-agent-id="' + escapeHtml(seg.subagent.taskId) + '" style="--agent-color:' + subPalette.primary + '">' +
|