@co0ontty/wand 1.30.0 → 1.30.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/structured-session-manager.js +55 -5
- package/dist/web-ui/content/scripts.js +443 -175
- package/dist/web-ui/content/styles.css +114 -11
- package/package.json +1 -1
|
@@ -117,6 +117,50 @@ function tagSubagentBlocks(blocks, parentToolUseId, registry) {
|
|
|
117
117
|
};
|
|
118
118
|
return blocks.map((block) => ({ ...block, __subagent: stamp }));
|
|
119
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* 给已被 captureTaskMeta 识别为 Task/Agent 的 tool_use block 本身也盖 __subagent 章。
|
|
122
|
+
* taskId 用自己的 block.id —— 与子消息的 parent_tool_use_id(也等于这个 id)保持一致,
|
|
123
|
+
* 前端 splitTurnBySubagent 按 taskId 分组时父 Task tool_use 和 SDK 转发的子消息能合并到同一段。
|
|
124
|
+
*/
|
|
125
|
+
function stampSelfTask(blocks, registry) {
|
|
126
|
+
return blocks.map((b) => {
|
|
127
|
+
if (b.type !== "tool_use")
|
|
128
|
+
return b;
|
|
129
|
+
if (b.__subagent)
|
|
130
|
+
return b; // 已盖章不重复(防止幂等问题)
|
|
131
|
+
const meta = registry.get(b.id);
|
|
132
|
+
if (!meta && b.name !== "Task" && b.name !== "Agent")
|
|
133
|
+
return b;
|
|
134
|
+
const stamp = {
|
|
135
|
+
taskId: b.id,
|
|
136
|
+
...(meta?.agentType ? { agentType: meta.agentType } : {}),
|
|
137
|
+
...(meta?.description ? { taskDescription: meta.description } : {}),
|
|
138
|
+
};
|
|
139
|
+
return { ...b, __subagent: stamp };
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* 当父 assistant 在 parentToolUseId === null 的 user turn 里收到 Task 工具的 tool_result 时,
|
|
144
|
+
* tagSubagentBlocks 不会被调用(它只在 parentToolUseId 非空时盖章)。这里按 tool_use_id
|
|
145
|
+
* 反查 registry,给这条 tool_result 单独盖章,让前端能把它归到同一个 subagent 段。
|
|
146
|
+
*/
|
|
147
|
+
function stampParentTaskResults(blocks, registry) {
|
|
148
|
+
return blocks.map((b) => {
|
|
149
|
+
if (b.type !== "tool_result")
|
|
150
|
+
return b;
|
|
151
|
+
if (b.__subagent)
|
|
152
|
+
return b;
|
|
153
|
+
const meta = registry.get(b.tool_use_id);
|
|
154
|
+
if (!meta)
|
|
155
|
+
return b;
|
|
156
|
+
const stamp = {
|
|
157
|
+
taskId: b.tool_use_id,
|
|
158
|
+
...(meta.agentType ? { agentType: meta.agentType } : {}),
|
|
159
|
+
...(meta.description ? { taskDescription: meta.description } : {}),
|
|
160
|
+
};
|
|
161
|
+
return { ...b, __subagent: stamp };
|
|
162
|
+
});
|
|
163
|
+
}
|
|
120
164
|
const STREAM_EMIT_DEBOUNCE_MS = 16;
|
|
121
165
|
/** Min interval between full saveSession() calls for an in-progress streaming turn.
|
|
122
166
|
* saveSession serializes the entire messages array, so doing it on every NDJSON
|
|
@@ -1555,7 +1599,7 @@ export class StructuredSessionManager {
|
|
|
1555
1599
|
captureTaskMeta(extracted.content, taskMetaRegistry);
|
|
1556
1600
|
}
|
|
1557
1601
|
const stamped = parentToolUseId === null
|
|
1558
|
-
? extracted.content
|
|
1602
|
+
? stampSelfTask(extracted.content, taskMetaRegistry)
|
|
1559
1603
|
: tagSubagentBlocks(extracted.content, parentToolUseId, taskMetaRegistry);
|
|
1560
1604
|
if (stamped.length > 0) {
|
|
1561
1605
|
upsertBlocks(msgId, stamped);
|
|
@@ -1599,7 +1643,7 @@ export class StructuredSessionManager {
|
|
|
1599
1643
|
? parsed.parent_tool_use_id
|
|
1600
1644
|
: null;
|
|
1601
1645
|
const stamped = parentToolUseId === null
|
|
1602
|
-
? collected
|
|
1646
|
+
? stampParentTaskResults(collected, taskMetaRegistry)
|
|
1603
1647
|
: tagSubagentBlocks(collected, parentToolUseId, taskMetaRegistry);
|
|
1604
1648
|
if (stamped.length > 0) {
|
|
1605
1649
|
upsertBlocks(`tool_result:${toolResultSeq++}`, stamped);
|
|
@@ -1983,7 +2027,13 @@ export class StructuredSessionManager {
|
|
|
1983
2027
|
streaming.push(block);
|
|
1984
2028
|
}
|
|
1985
2029
|
}
|
|
1986
|
-
|
|
2030
|
+
// 流式阶段就给 Task/Agent tool_use 本身盖章,防止"先显示工具卡片几秒再跳为
|
|
2031
|
+
// handoff 行"的闪烁。content_block_start 阶段就有 name=Task/Agent,
|
|
2032
|
+
// stampSelfTask 据此即可命中;agentType 字段藏在 input 里,delta 累计后再由
|
|
2033
|
+
// 后续 captureTaskMeta 回填 registry,下次 rebuild 自动补上更完整的 stamp。
|
|
2034
|
+
captureTaskMeta(streaming, taskMetaRegistry);
|
|
2035
|
+
const stampedStreaming = stampSelfTask(streaming, taskMetaRegistry);
|
|
2036
|
+
return [...finalizedBlocks, ...stampedStreaming];
|
|
1987
2037
|
};
|
|
1988
2038
|
const syncSnapshot = () => {
|
|
1989
2039
|
const current = this.sessions.get(sessionId);
|
|
@@ -2095,7 +2145,7 @@ export class StructuredSessionManager {
|
|
|
2095
2145
|
const parentToolUseId = assistantMsg.parent_tool_use_id ?? null;
|
|
2096
2146
|
if (parentToolUseId === null) {
|
|
2097
2147
|
captureTaskMeta(extracted.content, taskMetaRegistry);
|
|
2098
|
-
finalizedBlocks.push(...extracted.content);
|
|
2148
|
+
finalizedBlocks.push(...stampSelfTask(extracted.content, taskMetaRegistry));
|
|
2099
2149
|
}
|
|
2100
2150
|
else {
|
|
2101
2151
|
finalizedBlocks.push(...tagSubagentBlocks(extracted.content, parentToolUseId, taskMetaRegistry));
|
|
@@ -2150,7 +2200,7 @@ export class StructuredSessionManager {
|
|
|
2150
2200
|
}
|
|
2151
2201
|
}
|
|
2152
2202
|
if (parentToolUseId === null) {
|
|
2153
|
-
finalizedBlocks.push(...collected);
|
|
2203
|
+
finalizedBlocks.push(...stampParentTaskResults(collected, taskMetaRegistry));
|
|
2154
2204
|
}
|
|
2155
2205
|
else {
|
|
2156
2206
|
finalizedBlocks.push(...tagSubagentBlocks(collected, parentToolUseId, taskMetaRegistry));
|
|
@@ -238,10 +238,19 @@
|
|
|
238
238
|
chatUnreadCount: 0,
|
|
239
239
|
// state.currentMessages 中第一条未读消息的 index,-1 表示没有未读。
|
|
240
240
|
chatUnreadStartIndex: -1,
|
|
241
|
-
|
|
241
|
+
// 业界共识 150-180px:120px 在触控板/移动端惯性下边界来回弹。
|
|
242
|
+
chatScrollThreshold: 160,
|
|
242
243
|
chatIsProgrammaticScroll: false,
|
|
243
244
|
chatScrollElement: null,
|
|
244
245
|
chatScrollHandler: null,
|
|
246
|
+
chatScrollWheelHandler: null,
|
|
247
|
+
chatScrollTouchStartHandler: null,
|
|
248
|
+
chatScrollTouchMoveHandler: null,
|
|
249
|
+
chatTouchStartY: 0,
|
|
250
|
+
// 仅在"首次渲染当前会话视图"时才允许 fullRenderChat 强制贴底。
|
|
251
|
+
// resetChatRenderCache 会把它设回 false;fullRenderChat 第一次跑完就置 true。
|
|
252
|
+
// page-refresh / ws 重连不重置此标记,避免把用户拽到底部。
|
|
253
|
+
chatInitialRenderDone: false,
|
|
245
254
|
lastForegroundSyncAt: 0,
|
|
246
255
|
foregroundSyncTimer: null,
|
|
247
256
|
wsReconnectAttempts: 0,
|
|
@@ -597,8 +606,19 @@
|
|
|
597
606
|
updateChatUnreadBubble();
|
|
598
607
|
return;
|
|
599
608
|
}
|
|
600
|
-
if (state.chatScrollElement
|
|
601
|
-
|
|
609
|
+
if (state.chatScrollElement) {
|
|
610
|
+
if (state.chatScrollHandler) {
|
|
611
|
+
state.chatScrollElement.removeEventListener("scroll", state.chatScrollHandler);
|
|
612
|
+
}
|
|
613
|
+
if (state.chatScrollWheelHandler) {
|
|
614
|
+
state.chatScrollElement.removeEventListener("wheel", state.chatScrollWheelHandler);
|
|
615
|
+
}
|
|
616
|
+
if (state.chatScrollTouchStartHandler) {
|
|
617
|
+
state.chatScrollElement.removeEventListener("touchstart", state.chatScrollTouchStartHandler);
|
|
618
|
+
}
|
|
619
|
+
if (state.chatScrollTouchMoveHandler) {
|
|
620
|
+
state.chatScrollElement.removeEventListener("touchmove", state.chatScrollTouchMoveHandler);
|
|
621
|
+
}
|
|
602
622
|
}
|
|
603
623
|
state.chatScrollElement = chatMsgs;
|
|
604
624
|
state.chatScrollHandler = function() {
|
|
@@ -619,7 +639,35 @@
|
|
|
619
639
|
}
|
|
620
640
|
updateChatUnreadBubble();
|
|
621
641
|
};
|
|
642
|
+
// wheel/touch 提前下台:浏览器要等惯性产生位移才触发 scroll 事件,
|
|
643
|
+
// 这一帧空窗里如果有 streaming chunk 进来,会在 sticky=true 状态下
|
|
644
|
+
// 被强制贴底。监听用户开始上滚的瞬间立刻把 sticky 翻成 false,
|
|
645
|
+
// 避免那一帧的拽回。column-reverse 下 deltaY<0(滚轮上推)= 看历史。
|
|
646
|
+
state.chatScrollWheelHandler = function(e) {
|
|
647
|
+
if (state.chatIsProgrammaticScroll) return;
|
|
648
|
+
if (e.deltaY < 0) {
|
|
649
|
+
state.chatStickToBottom = false;
|
|
650
|
+
updateChatUnreadBubble();
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
state.chatScrollTouchStartHandler = function(e) {
|
|
654
|
+
if (!e.touches || e.touches.length === 0) return;
|
|
655
|
+
state.chatTouchStartY = e.touches[0].clientY;
|
|
656
|
+
};
|
|
657
|
+
state.chatScrollTouchMoveHandler = function(e) {
|
|
658
|
+
if (state.chatIsProgrammaticScroll) return;
|
|
659
|
+
if (!e.touches || e.touches.length === 0) return;
|
|
660
|
+
// column-reverse 下:手指向下拖(clientY 变大)= 内容向下走 = 看历史。
|
|
661
|
+
var deltaY = e.touches[0].clientY - state.chatTouchStartY;
|
|
662
|
+
if (deltaY > 4) {
|
|
663
|
+
state.chatStickToBottom = false;
|
|
664
|
+
updateChatUnreadBubble();
|
|
665
|
+
}
|
|
666
|
+
};
|
|
622
667
|
chatMsgs.addEventListener("scroll", state.chatScrollHandler, { passive: true });
|
|
668
|
+
chatMsgs.addEventListener("wheel", state.chatScrollWheelHandler, { passive: true });
|
|
669
|
+
chatMsgs.addEventListener("touchstart", state.chatScrollTouchStartHandler, { passive: true });
|
|
670
|
+
chatMsgs.addEventListener("touchmove", state.chatScrollTouchMoveHandler, { passive: true });
|
|
623
671
|
updateChatUnreadBubble();
|
|
624
672
|
}
|
|
625
673
|
|
|
@@ -951,23 +999,46 @@
|
|
|
951
999
|
});
|
|
952
1000
|
}
|
|
953
1001
|
|
|
954
|
-
|
|
1002
|
+
// options.preserveStickState=true:仅清渲染缓存,不动 sticky/未读
|
|
1003
|
+
// 状态。用于 page-refresh、ws 重连等"用户停留在当前会话,只是想刷新
|
|
1004
|
+
// DOM"的场景——不能把用户从历史位置拽回底部。
|
|
1005
|
+
// 默认(false):切会话 / 新建 / home 等真正"换上下文"路径用,全清。
|
|
1006
|
+
function resetChatRenderCache(options) {
|
|
1007
|
+
var opts = options || {};
|
|
955
1008
|
state.lastRenderedHash = 0;
|
|
956
1009
|
state.lastRenderedMsgCount = 0;
|
|
957
1010
|
state.lastRenderedEmpty = null;
|
|
958
1011
|
state.renderPending = false;
|
|
959
1012
|
state.chatRenderedCount = state.chatPageSize;
|
|
960
1013
|
state.askUserSelections = {};
|
|
961
|
-
if (state.chatScrollElement
|
|
962
|
-
|
|
1014
|
+
if (state.chatScrollElement) {
|
|
1015
|
+
if (state.chatScrollHandler) {
|
|
1016
|
+
state.chatScrollElement.removeEventListener("scroll", state.chatScrollHandler);
|
|
1017
|
+
}
|
|
1018
|
+
if (state.chatScrollWheelHandler) {
|
|
1019
|
+
state.chatScrollElement.removeEventListener("wheel", state.chatScrollWheelHandler);
|
|
1020
|
+
}
|
|
1021
|
+
if (state.chatScrollTouchStartHandler) {
|
|
1022
|
+
state.chatScrollElement.removeEventListener("touchstart", state.chatScrollTouchStartHandler);
|
|
1023
|
+
}
|
|
1024
|
+
if (state.chatScrollTouchMoveHandler) {
|
|
1025
|
+
state.chatScrollElement.removeEventListener("touchmove", state.chatScrollTouchMoveHandler);
|
|
1026
|
+
}
|
|
963
1027
|
}
|
|
964
1028
|
state.chatScrollElement = null;
|
|
965
1029
|
state.chatScrollHandler = null;
|
|
1030
|
+
state.chatScrollWheelHandler = null;
|
|
1031
|
+
state.chatScrollTouchStartHandler = null;
|
|
1032
|
+
state.chatScrollTouchMoveHandler = null;
|
|
966
1033
|
state.chatIsProgrammaticScroll = false;
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1034
|
+
if (!opts.preserveStickState) {
|
|
1035
|
+
// 切会话时未读状态归零、贴底重置——避免上一个会话残留的"未读气泡"。
|
|
1036
|
+
state.chatStickToBottom = true;
|
|
1037
|
+
state.chatUnreadCount = 0;
|
|
1038
|
+
state.chatUnreadStartIndex = -1;
|
|
1039
|
+
// 真正换会话时才允许首帧贴底;preserve 路径下保留旧 initial 状态。
|
|
1040
|
+
state.chatInitialRenderDone = false;
|
|
1041
|
+
}
|
|
971
1042
|
}
|
|
972
1043
|
|
|
973
1044
|
function getEffectiveCwd() {
|
|
@@ -1464,7 +1535,6 @@
|
|
|
1464
1535
|
? '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="10 6 16 12 10 18"/><line x1="20" y1="5" x2="20" y2="19"/></svg>'
|
|
1465
1536
|
: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="14 6 8 12 14 18"/><line x1="4" y1="5" x2="4" y2="19"/></svg>') +
|
|
1466
1537
|
'</button>' +
|
|
1467
|
-
'<button id="sidebar-close-fully-btn" class="btn btn-ghost btn-sm sidebar-close-fully" type="button" title="完全关闭侧栏" aria-label="完全关闭侧栏"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/></svg></button>' +
|
|
1468
1538
|
'<button id="close-drawer-button" class="btn btn-ghost btn-icon sidebar-close drawer-close-btn" type="button" aria-label="关闭菜单"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/></svg></button>' +
|
|
1469
1539
|
'</div>' +
|
|
1470
1540
|
'</div>' +
|
|
@@ -1676,6 +1746,11 @@
|
|
|
1676
1746
|
'<button id="stop-button" class="btn-circle btn-circle-stop' + (state.selectedId ? "" : " hidden") + '" title="停止">' +
|
|
1677
1747
|
'<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><rect x="3" y="3" width="10" height="10" rx="2"/></svg>' +
|
|
1678
1748
|
'</button>' +
|
|
1749
|
+
// 结构化模式且正在出 token 时显示:中断当前回复、立刻发送新输入。
|
|
1750
|
+
// 默认走 #send-input-button → 排队;想插队的人显式按这颗。
|
|
1751
|
+
'<button id="interrupt-send-button" class="btn-circle btn-circle-interrupt hidden" type="button" title="中断当前回复并立即发送" aria-label="立即发送">' +
|
|
1752
|
+
'<svg 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>' +
|
|
1753
|
+
'</button>' +
|
|
1679
1754
|
'<button id="send-input-button" class="btn-circle btn-circle-send" title="发送">' +
|
|
1680
1755
|
'<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>' +
|
|
1681
1756
|
'</button>' +
|
|
@@ -3094,29 +3169,44 @@
|
|
|
3094
3169
|
'</section>';
|
|
3095
3170
|
}
|
|
3096
3171
|
|
|
3172
|
+
// 「最近」分组的统一数据源:未归档 active sessions + 24h 内 Claude 历史,
|
|
3173
|
+
// 一起按"创建时间"倒序排(session 用 startedAt,history 用 timestamp)。
|
|
3174
|
+
// 展开侧栏的「最近」和折叠侧栏的窄条都基于这份列表渲染,序号严格对应。
|
|
3175
|
+
function getRecentEntries() {
|
|
3176
|
+
var cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
3177
|
+
var entries = [];
|
|
3178
|
+
state.sessions.forEach(function(s) {
|
|
3179
|
+
if (s.archived) return;
|
|
3180
|
+
var t = s.startedAt ? new Date(s.startedAt).getTime() : 0;
|
|
3181
|
+
entries.push({ kind: "session", ref: s, t: isFinite(t) ? t : 0 });
|
|
3182
|
+
});
|
|
3183
|
+
if (state.claudeHistoryLoaded) {
|
|
3184
|
+
getVisibleClaudeHistorySessions().forEach(function(h) {
|
|
3185
|
+
if (!h.timestamp) return;
|
|
3186
|
+
var t = new Date(h.timestamp).getTime();
|
|
3187
|
+
if (!isFinite(t) || t <= cutoff) return;
|
|
3188
|
+
entries.push({ kind: "history", ref: h, t: t });
|
|
3189
|
+
});
|
|
3190
|
+
}
|
|
3191
|
+
entries.sort(function(a, b) { return b.t - a.t; });
|
|
3192
|
+
return entries;
|
|
3193
|
+
}
|
|
3194
|
+
|
|
3097
3195
|
function renderSessions() {
|
|
3098
|
-
var activeSessions = state.sessions.filter(function(session) { return !session.archived; });
|
|
3099
3196
|
var archivedSessions = state.sessions.filter(function(session) { return session.archived; });
|
|
3100
3197
|
var groups = [];
|
|
3101
3198
|
groups.push(renderSessionManageBar());
|
|
3102
3199
|
|
|
3103
|
-
|
|
3104
|
-
var recentHistorySessions = [];
|
|
3105
|
-
if (state.claudeHistoryLoaded) {
|
|
3106
|
-
var cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
3107
|
-
recentHistorySessions = getVisibleClaudeHistorySessions().filter(function(s) {
|
|
3108
|
-
return s.timestamp && new Date(s.timestamp).getTime() > cutoff;
|
|
3109
|
-
});
|
|
3110
|
-
}
|
|
3200
|
+
var recentEntries = getRecentEntries();
|
|
3111
3201
|
|
|
3112
|
-
if (
|
|
3113
|
-
groups.push(renderRecentGroup(
|
|
3202
|
+
if (recentEntries.length > 0) {
|
|
3203
|
+
groups.push(renderRecentGroup(recentEntries));
|
|
3114
3204
|
}
|
|
3115
3205
|
if (archivedSessions.length > 0) {
|
|
3116
3206
|
groups.push(renderArchivedGroup(archivedSessions));
|
|
3117
3207
|
}
|
|
3118
3208
|
groups.push(renderClaudeHistorySection());
|
|
3119
|
-
if (
|
|
3209
|
+
if (recentEntries.length === 0 && archivedSessions.length === 0) {
|
|
3120
3210
|
return renderSessionManageBar() + '<div class="empty-state"><strong>还没有会话记录</strong><br>点击上方「新对话」开始你的第一次对话。</div>' + renderClaudeHistorySection();
|
|
3121
3211
|
}
|
|
3122
3212
|
return groups.join("");
|
|
@@ -3127,36 +3217,23 @@
|
|
|
3127
3217
|
}
|
|
3128
3218
|
|
|
3129
3219
|
function renderCollapsedSessionTiles() {
|
|
3130
|
-
var
|
|
3131
|
-
|
|
3132
|
-
var ta = a.startedAt ? new Date(a.startedAt).getTime() : 0;
|
|
3133
|
-
var tb = b.startedAt ? new Date(b.startedAt).getTime() : 0;
|
|
3134
|
-
return ta - tb;
|
|
3135
|
-
});
|
|
3136
|
-
var recentHistorySessions = [];
|
|
3137
|
-
if (state.claudeHistoryLoaded) {
|
|
3138
|
-
var cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
3139
|
-
recentHistorySessions = getVisibleClaudeHistorySessions().filter(function(s) {
|
|
3140
|
-
return s.timestamp && new Date(s.timestamp).getTime() > cutoff;
|
|
3141
|
-
});
|
|
3142
|
-
}
|
|
3143
|
-
if (activeSessions.length === 0 && recentHistorySessions.length === 0) {
|
|
3220
|
+
var entries = getRecentEntries();
|
|
3221
|
+
if (entries.length === 0) {
|
|
3144
3222
|
return '<div class="sidebar-collapsed-empty" title="无会话">—</div>';
|
|
3145
3223
|
}
|
|
3146
|
-
var tiles =
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
var
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
});
|
|
3224
|
+
var tiles = entries.map(function(e, i) {
|
|
3225
|
+
var idx = i + 1;
|
|
3226
|
+
if (e.kind === "session") {
|
|
3227
|
+
var s = e.ref;
|
|
3228
|
+
var activeCls = s.id === state.selectedId ? " active" : "";
|
|
3229
|
+
var title = s.summary || s.command || ("会话 " + idx);
|
|
3230
|
+
return '<button class="sidebar-collapsed-tile' + activeCls + '" type="button" data-collapsed-session-id="' + escapeHtml(s.id) + '" title="' + escapeHtml(title) + '">' + idx + '</button>';
|
|
3231
|
+
}
|
|
3232
|
+
var h = e.ref;
|
|
3233
|
+
var preview = h.firstUserMessage || "(空会话)";
|
|
3234
|
+
var hTitle = preview + " · " + formatHistoryTime(h.timestamp);
|
|
3235
|
+
return '<button class="sidebar-collapsed-tile history" type="button" data-collapsed-history-id="' + escapeHtml(h.claudeSessionId) + '" data-cwd="' + escapeHtml(h.cwd || "") + '" title="' + escapeHtml(hTitle) + '">' + idx + '</button>';
|
|
3236
|
+
}).join("");
|
|
3160
3237
|
return '<div class="sidebar-collapsed-tiles">' + tiles + '</div>';
|
|
3161
3238
|
}
|
|
3162
3239
|
|
|
@@ -3223,11 +3300,14 @@
|
|
|
3223
3300
|
return '<section class="session-group">' + header + items + '</section>';
|
|
3224
3301
|
}
|
|
3225
3302
|
|
|
3226
|
-
function renderRecentGroup(
|
|
3303
|
+
function renderRecentGroup(entries) {
|
|
3227
3304
|
var html = '<section class="session-group">' +
|
|
3228
3305
|
'<div class="session-group-title">最近</div>';
|
|
3229
|
-
html +=
|
|
3230
|
-
|
|
3306
|
+
html += entries.map(function(e) {
|
|
3307
|
+
return e.kind === "session"
|
|
3308
|
+
? renderSessionItem(e.ref, "sessions")
|
|
3309
|
+
: renderClaudeHistoryItem(e.ref, "history");
|
|
3310
|
+
}).join("");
|
|
3231
3311
|
html += '</section>';
|
|
3232
3312
|
return html;
|
|
3233
3313
|
}
|
|
@@ -5627,8 +5707,6 @@
|
|
|
5627
5707
|
if (pinBtn) pinBtn.addEventListener("click", toggleSidebarPin);
|
|
5628
5708
|
var collapseBtn = document.getElementById("sidebar-collapse-btn");
|
|
5629
5709
|
if (collapseBtn) collapseBtn.addEventListener("click", toggleSidebarCollapsed);
|
|
5630
|
-
var closeFullyBtn = document.getElementById("sidebar-close-fully-btn");
|
|
5631
|
-
if (closeFullyBtn) closeFullyBtn.addEventListener("click", closeSidebarCompletely);
|
|
5632
5710
|
var sidebarMoreBtn = document.getElementById("sidebar-more-btn");
|
|
5633
5711
|
var sidebarOverflow = document.getElementById("sidebar-overflow-menu");
|
|
5634
5712
|
if (sidebarMoreBtn && sidebarOverflow) {
|
|
@@ -5894,6 +5972,11 @@
|
|
|
5894
5972
|
closeSessionsDrawer();
|
|
5895
5973
|
sendOrStart();
|
|
5896
5974
|
});
|
|
5975
|
+
var interruptSendBtn = document.getElementById("interrupt-send-button");
|
|
5976
|
+
if (interruptSendBtn) interruptSendBtn.addEventListener("click", function() {
|
|
5977
|
+
closeSessionsDrawer();
|
|
5978
|
+
sendOrStart({ interrupt: true });
|
|
5979
|
+
});
|
|
5897
5980
|
var stopBtn = document.getElementById("stop-button");
|
|
5898
5981
|
if (stopBtn) stopBtn.addEventListener("click", stopSession);
|
|
5899
5982
|
var modeSelect = document.getElementById("chat-mode-select");
|
|
@@ -6142,7 +6225,8 @@
|
|
|
6142
6225
|
return;
|
|
6143
6226
|
}
|
|
6144
6227
|
softResyncTerminal();
|
|
6145
|
-
|
|
6228
|
+
// 用户停留在当前会话,只是想刷一下 DOM——保留其阅读位置和 sticky 状态。
|
|
6229
|
+
resetChatRenderCache({ preserveStickState: true });
|
|
6146
6230
|
scheduleChatRender(true);
|
|
6147
6231
|
});
|
|
6148
6232
|
var jumpBottomBtn = document.getElementById("terminal-jump-bottom");
|
|
@@ -7909,6 +7993,11 @@
|
|
|
7909
7993
|
if (canAutoResumeSession(session)) return "";
|
|
7910
7994
|
return "会话已结束";
|
|
7911
7995
|
}
|
|
7996
|
+
// 结构化会话在出 token 时,输入框仍然可用——告诉用户默认行为是排队,
|
|
7997
|
+
// 想插队请按右侧的 » 按钮。短语保持单行不换行。
|
|
7998
|
+
if (isStructuredSession(session) && session.structuredState && session.structuredState.inFlight) {
|
|
7999
|
+
return "回复中…继续输入将排队(» 立即发送)";
|
|
8000
|
+
}
|
|
7912
8001
|
return "";
|
|
7913
8002
|
}
|
|
7914
8003
|
|
|
@@ -8322,6 +8411,7 @@
|
|
|
8322
8411
|
var hasSession = !!state.selectedId;
|
|
8323
8412
|
var terminalContainer = document.getElementById("output");
|
|
8324
8413
|
var chatContainer = document.getElementById("chat-output");
|
|
8414
|
+
var blankChat = document.getElementById("blank-chat");
|
|
8325
8415
|
var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
8326
8416
|
var structured = isStructuredSession(selectedSession);
|
|
8327
8417
|
var showTerminal = hasSession && !structured && state.currentView === "terminal";
|
|
@@ -8340,6 +8430,14 @@
|
|
|
8340
8430
|
chatContainer.classList.toggle("active", showChat);
|
|
8341
8431
|
chatContainer.classList.toggle("hidden", !showChat);
|
|
8342
8432
|
}
|
|
8433
|
+
// blank-chat 的可见性由 applyCurrentView 收口:updateShellChrome 在
|
|
8434
|
+
// selectedSession 缺失(启动期 selectedId 已恢复但 /api/sessions 未回 /
|
|
8435
|
+
// activateSession 在 updateSessionSnapshot 之前调 switchToSessionView 等
|
|
8436
|
+
// 瞬态)时会走 else 分支把 blank-chat 显示出来,但紧接着调到这里,应
|
|
8437
|
+
// 以 hasSession 为准重新隐藏,避免与 terminal/chat 同屏并存。
|
|
8438
|
+
if (blankChat) {
|
|
8439
|
+
blankChat.classList.toggle("hidden", hasSession);
|
|
8440
|
+
}
|
|
8343
8441
|
if (chatContainer && showChat) {
|
|
8344
8442
|
ensureChatMessagesContainer(chatContainer);
|
|
8345
8443
|
}
|
|
@@ -8604,7 +8702,8 @@
|
|
|
8604
8702
|
} else if (state.selectedId) {
|
|
8605
8703
|
var sel = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
8606
8704
|
if (isStructuredSession(sel)) {
|
|
8607
|
-
|
|
8705
|
+
// ws 重连后的同会话刷新——保留用户阅读位置。
|
|
8706
|
+
resetChatRenderCache({ preserveStickState: true });
|
|
8608
8707
|
scheduleChatRender(true);
|
|
8609
8708
|
}
|
|
8610
8709
|
}
|
|
@@ -8974,29 +9073,6 @@
|
|
|
8974
9073
|
hideCollapsedTileBubble();
|
|
8975
9074
|
}
|
|
8976
9075
|
|
|
8977
|
-
function closeSidebarCompletely() {
|
|
8978
|
-
if (isMobileLayout()) return;
|
|
8979
|
-
state.sidebarPinned = false;
|
|
8980
|
-
state.sidebarCollapsed = false;
|
|
8981
|
-
state.sessionsDrawerOpen = false;
|
|
8982
|
-
try {
|
|
8983
|
-
localStorage.setItem("wand-sidebar-pinned", "false");
|
|
8984
|
-
localStorage.setItem("wand-sidebar-collapsed", "false");
|
|
8985
|
-
} catch (e) {}
|
|
8986
|
-
render();
|
|
8987
|
-
var mainLayout = document.querySelector(".main-layout");
|
|
8988
|
-
if (mainLayout) {
|
|
8989
|
-
var onEnd = function(e) {
|
|
8990
|
-
if (e.propertyName === "padding-left") {
|
|
8991
|
-
mainLayout.removeEventListener("transitionend", onEnd);
|
|
8992
|
-
scheduleTerminalResize(true);
|
|
8993
|
-
}
|
|
8994
|
-
};
|
|
8995
|
-
mainLayout.addEventListener("transitionend", onEnd);
|
|
8996
|
-
}
|
|
8997
|
-
setTimeout(function() { scheduleTerminalResize(true); }, 350);
|
|
8998
|
-
}
|
|
8999
|
-
|
|
9000
9076
|
function toggleSidebarCollapsed() {
|
|
9001
9077
|
if (isMobileLayout()) return;
|
|
9002
9078
|
// 在 drawer 模式(未 pin)下点 collapse 视为「先固定、再收起为窄条」——
|
|
@@ -10931,7 +11007,10 @@
|
|
|
10931
11007
|
return;
|
|
10932
11008
|
}
|
|
10933
11009
|
event.preventDefault();
|
|
10934
|
-
|
|
11010
|
+
// Cmd/Ctrl+Enter → 立即发送(中断当前回复)。仅对正在 inFlight 的
|
|
11011
|
+
// 结构化会话生效;其它情况下退化为普通发送,避免无谓的中断信号。
|
|
11012
|
+
var interruptShortcut = !!(event.metaKey || event.ctrlKey);
|
|
11013
|
+
sendInputFromBox(interruptShortcut ? { interrupt: true } : undefined);
|
|
10935
11014
|
return;
|
|
10936
11015
|
}
|
|
10937
11016
|
|
|
@@ -11640,7 +11719,8 @@
|
|
|
11640
11719
|
});
|
|
11641
11720
|
}
|
|
11642
11721
|
|
|
11643
|
-
function sendOrStart() {
|
|
11722
|
+
function sendOrStart(opts) {
|
|
11723
|
+
opts = opts || {};
|
|
11644
11724
|
// Support welcome input as well as the main input box
|
|
11645
11725
|
var welcomeInput = document.getElementById("welcome-input");
|
|
11646
11726
|
var inputBox = document.getElementById("input-box");
|
|
@@ -11651,7 +11731,7 @@
|
|
|
11651
11731
|
// If we have a selected ID, try to send input to it
|
|
11652
11732
|
if (state.selectedId) {
|
|
11653
11733
|
if (value) {
|
|
11654
|
-
sendInputFromBox();
|
|
11734
|
+
sendInputFromBox(opts);
|
|
11655
11735
|
}
|
|
11656
11736
|
return;
|
|
11657
11737
|
}
|
|
@@ -11750,7 +11830,9 @@
|
|
|
11750
11830
|
}
|
|
11751
11831
|
|
|
11752
11832
|
|
|
11753
|
-
function sendInputFromBox() {
|
|
11833
|
+
function sendInputFromBox(opts) {
|
|
11834
|
+
opts = opts || {};
|
|
11835
|
+
var interruptFlag = !!opts.interrupt;
|
|
11754
11836
|
if (state.terminalInteractive) {
|
|
11755
11837
|
showToast("终端交互模式开启时,请直接在终端中输入。", "info");
|
|
11756
11838
|
return Promise.resolve();
|
|
@@ -11789,7 +11871,7 @@
|
|
|
11789
11871
|
if (todoEl) todoEl.classList.add("hidden");
|
|
11790
11872
|
|
|
11791
11873
|
if (isStructuredSession(selectedSession)) {
|
|
11792
|
-
return postStructuredInput(finalValue, inputBox, selectedSession);
|
|
11874
|
+
return postStructuredInput(finalValue, inputBox, selectedSession, { interrupt: interruptFlag });
|
|
11793
11875
|
}
|
|
11794
11876
|
|
|
11795
11877
|
var submitChunks = getTerminalSubmitChunks(selectedSession, finalValue);
|
|
@@ -11837,8 +11919,13 @@
|
|
|
11837
11919
|
// 防止同一会话并发提交(快速双击 / 重复触发)
|
|
11838
11920
|
var _structuredSubmittingSessions = {};
|
|
11839
11921
|
|
|
11840
|
-
function postStructuredInput(input, inputBox, session) {
|
|
11841
|
-
|
|
11922
|
+
function postStructuredInput(input, inputBox, session, opts) {
|
|
11923
|
+
opts = opts || {};
|
|
11924
|
+
// 用户显式点击"立即发送"才会传 interrupt:true。普通 Enter / 点发送
|
|
11925
|
+
// 在上一条还在流式时默认走 queue —— 后端 sendMessage(...) 会把它
|
|
11926
|
+
// 追加到 queuedMessages,等当前 turn 结束自动 flush。
|
|
11927
|
+
var requestedInterrupt = !!opts.interrupt;
|
|
11928
|
+
console.log("[WAND] postStructuredInput selectedId:", state.selectedId, "input:", input && input.substring(0, 50), "requestedInterrupt:", requestedInterrupt, "session:", session && { id: session.id, sessionKind: session.sessionKind, runner: session.runner, status: session.status, inFlight: session.structuredState && session.structuredState.inFlight });
|
|
11842
11929
|
if (!state.selectedId || !input) return Promise.resolve();
|
|
11843
11930
|
if (!session) {
|
|
11844
11931
|
showToast("会话不存在,请重新选择或新建会话。", "error");
|
|
@@ -11850,25 +11937,48 @@
|
|
|
11850
11937
|
return Promise.resolve();
|
|
11851
11938
|
}
|
|
11852
11939
|
|
|
11853
|
-
var
|
|
11854
|
-
|
|
11855
|
-
var
|
|
11940
|
+
var sessionInFlight = !!(session.structuredState && session.structuredState.inFlight && session.status === "running");
|
|
11941
|
+
var isInterrupting = sessionInFlight && requestedInterrupt;
|
|
11942
|
+
var isQueueing = sessionInFlight && !requestedInterrupt;
|
|
11943
|
+
|
|
11856
11944
|
var userMsgs = stripRenderOnlyStructuredMessages(Array.isArray(session.messages) ? session.messages.slice() : []);
|
|
11857
|
-
|
|
11858
|
-
|
|
11859
|
-
|
|
11860
|
-
|
|
11861
|
-
|
|
11862
|
-
|
|
11863
|
-
|
|
11864
|
-
|
|
11865
|
-
|
|
11866
|
-
|
|
11867
|
-
|
|
11868
|
-
|
|
11869
|
-
|
|
11870
|
-
|
|
11871
|
-
|
|
11945
|
+
var optimisticPatch;
|
|
11946
|
+
|
|
11947
|
+
if (isQueueing) {
|
|
11948
|
+
// Queue 模式:不要乐观 push user turn —— buildMessagesForRender 会把
|
|
11949
|
+
// queuedMessages 渲成 __queued 占位(带"排队中"徽章),再 push 一份
|
|
11950
|
+
// 真 user turn 会被去重逻辑遮蔽掉,徽章就丢了。inFlight / status 维持。
|
|
11951
|
+
var nextQueue = Array.isArray(session.queuedMessages) ? session.queuedMessages.slice() : [];
|
|
11952
|
+
nextQueue.push(input);
|
|
11953
|
+
optimisticPatch = {
|
|
11954
|
+
id: session.id,
|
|
11955
|
+
queuedMessages: nextQueue,
|
|
11956
|
+
};
|
|
11957
|
+
updateSessionSnapshot(optimisticPatch);
|
|
11958
|
+
var queueRefreshed = state.sessions.find(function(s) { return s.id === session.id; }) || session;
|
|
11959
|
+
state.currentMessages = buildMessagesForRender(queueRefreshed, getPreferredMessages(queueRefreshed, queueRefreshed.output, false));
|
|
11960
|
+
updateInputHint("已加入排队,等待当前回复完成…");
|
|
11961
|
+
renderChat(true);
|
|
11962
|
+
updateStructuredQueueCounter();
|
|
11963
|
+
} else {
|
|
11964
|
+
// 普通发送 / interrupt 发送:照旧乐观推 user turn + inFlight=true
|
|
11965
|
+
var userTurn = { role: "user", content: [{ type: "text", text: input }] };
|
|
11966
|
+
userMsgs.push(userTurn);
|
|
11967
|
+
var optimisticStructuredState = Object.assign({}, session.structuredState || {}, { inFlight: true });
|
|
11968
|
+
updateSessionSnapshot({
|
|
11969
|
+
id: session.id,
|
|
11970
|
+
status: "running",
|
|
11971
|
+
messages: userMsgs,
|
|
11972
|
+
structuredState: optimisticStructuredState,
|
|
11973
|
+
});
|
|
11974
|
+
state.currentMessages = buildMessagesForRender(Object.assign({}, session, {
|
|
11975
|
+
status: "running",
|
|
11976
|
+
messages: userMsgs,
|
|
11977
|
+
structuredState: optimisticStructuredState,
|
|
11978
|
+
}), userMsgs);
|
|
11979
|
+
updateInputHint(isInterrupting ? "已中断,正在处理新消息…" : "思考中…");
|
|
11980
|
+
renderChat(true);
|
|
11981
|
+
}
|
|
11872
11982
|
|
|
11873
11983
|
if (inputBox) {
|
|
11874
11984
|
inputBox.value = "";
|
|
@@ -11924,8 +12034,12 @@
|
|
|
11924
12034
|
var refreshedSession = state.sessions.find(function(s) { return s.id === snapshot.id; }) || snapshot;
|
|
11925
12035
|
state.currentMessages = buildMessagesForRender(refreshedSession, getPreferredMessages(refreshedSession, snapshot.output, false));
|
|
11926
12036
|
renderChat(true);
|
|
12037
|
+
updateStructuredQueueCounter();
|
|
11927
12038
|
if (isInterrupting) {
|
|
11928
12039
|
showToast("已中断上一条回复,正在处理新消息…", "info");
|
|
12040
|
+
} else if (isQueueing) {
|
|
12041
|
+
var qLen = Array.isArray(refreshedSession.queuedMessages) ? refreshedSession.queuedMessages.length : 0;
|
|
12042
|
+
showToast(qLen > 1 ? ("已加入排队(共 " + qLen + " 条等待)") : "已加入排队,等待当前回复完成。", "info");
|
|
11929
12043
|
}
|
|
11930
12044
|
}
|
|
11931
12045
|
}
|
|
@@ -11943,20 +12057,36 @@
|
|
|
11943
12057
|
return;
|
|
11944
12058
|
}
|
|
11945
12059
|
|
|
11946
|
-
|
|
11947
|
-
|
|
11948
|
-
|
|
11949
|
-
|
|
11950
|
-
|
|
11951
|
-
|
|
11952
|
-
|
|
11953
|
-
|
|
11954
|
-
|
|
11955
|
-
|
|
11956
|
-
|
|
11957
|
-
|
|
11958
|
-
|
|
11959
|
-
|
|
12060
|
+
if (isQueueing) {
|
|
12061
|
+
// Queue 模式回滚:把刚 push 的那条 queuedMessages 撤掉。inFlight / messages
|
|
12062
|
+
// 都没动过,不必复位,否则会把后端真实的 inFlight=true 误改成 false。
|
|
12063
|
+
var prevQueue = Array.isArray(session.queuedMessages) ? session.queuedMessages.slice() : [];
|
|
12064
|
+
updateSessionSnapshot({
|
|
12065
|
+
id: session.id,
|
|
12066
|
+
queuedMessages: prevQueue,
|
|
12067
|
+
});
|
|
12068
|
+
if (session.id === state.selectedId) {
|
|
12069
|
+
var rolledQueueSession = state.sessions.find(function(s) { return s.id === session.id; }) || session;
|
|
12070
|
+
state.currentMessages = buildMessagesForRender(rolledQueueSession, getPreferredMessages(rolledQueueSession, rolledQueueSession.output, false));
|
|
12071
|
+
renderChat(true);
|
|
12072
|
+
updateStructuredQueueCounter();
|
|
12073
|
+
}
|
|
12074
|
+
} else {
|
|
12075
|
+
// 回滚乐观更新:恢复发送前的 messages(去掉刚加的 userTurn)和 inFlight 状态
|
|
12076
|
+
var rollbackMsgs = userMsgs.slice(0, -1);
|
|
12077
|
+
updateSessionSnapshot({
|
|
12078
|
+
id: session.id,
|
|
12079
|
+
status: session.status,
|
|
12080
|
+
messages: rollbackMsgs,
|
|
12081
|
+
structuredState: Object.assign({}, session.structuredState || {}, { inFlight: false }),
|
|
12082
|
+
});
|
|
12083
|
+
if (session.id === state.selectedId) {
|
|
12084
|
+
state.currentMessages = buildMessagesForRender(
|
|
12085
|
+
Object.assign({}, session, { messages: rollbackMsgs, structuredState: Object.assign({}, session.structuredState || {}, { inFlight: false }) }),
|
|
12086
|
+
rollbackMsgs
|
|
12087
|
+
);
|
|
12088
|
+
renderChat(true);
|
|
12089
|
+
}
|
|
11960
12090
|
}
|
|
11961
12091
|
var message = (error && error.message) || "";
|
|
11962
12092
|
var isTransientAbort =
|
|
@@ -12564,11 +12694,19 @@
|
|
|
12564
12694
|
composer.classList.toggle("is-terminal-passthrough", !!state.terminalInteractive);
|
|
12565
12695
|
}
|
|
12566
12696
|
var sendBtn = document.getElementById("send-input-button");
|
|
12697
|
+
var structuredInFlight = structured && isRunning;
|
|
12567
12698
|
if (sendBtn) {
|
|
12568
12699
|
sendBtn.disabled = !structured && !!selectedSession && !isRunning && !canResumeOnSend;
|
|
12569
12700
|
sendBtn.setAttribute("title", structured
|
|
12570
|
-
? "发送"
|
|
12701
|
+
? (structuredInFlight ? "排队发送(当前回复结束后处理)" : "发送")
|
|
12571
12702
|
: (isCodex ? (isRunning ? "发送给 Codex" : "Codex 会话已结束") : (!selectedSession || isRunning || canResumeOnSend ? "发送" : "会话已结束")));
|
|
12703
|
+
sendBtn.classList.toggle("queue-mode", structuredInFlight);
|
|
12704
|
+
}
|
|
12705
|
+
var interruptBtn = document.getElementById("interrupt-send-button");
|
|
12706
|
+
if (interruptBtn) {
|
|
12707
|
+
// 仅结构化 + inFlight 时显示。pty 会话有自己的 Ctrl+C / stop 按钮,
|
|
12708
|
+
// 用不上这套语义。
|
|
12709
|
+
interruptBtn.classList.toggle("hidden", !structuredInFlight);
|
|
12572
12710
|
}
|
|
12573
12711
|
var container = document.getElementById("output");
|
|
12574
12712
|
if (container) container.classList.toggle("interactive", !structured && state.terminalInteractive);
|
|
@@ -13287,7 +13425,10 @@
|
|
|
13287
13425
|
// fill the expanded space, and the scroll position needs resetting.
|
|
13288
13426
|
if (isTouchDevice()) {
|
|
13289
13427
|
ensureTerminalFit("keyboard-blur", { forceReplay: true });
|
|
13290
|
-
|
|
13428
|
+
// "keyboard" 而非 "force":用户原本在终端历史里翻看时,键盘
|
|
13429
|
+
// 收起不该把视图拽走。maybeScrollTerminalToBottom 会按
|
|
13430
|
+
// terminalAutoFollow 决定——贴底者继续贴底,离底者保位。
|
|
13431
|
+
maybeScrollTerminalToBottom("keyboard");
|
|
13291
13432
|
}
|
|
13292
13433
|
}, 100);
|
|
13293
13434
|
}
|
|
@@ -14081,7 +14222,9 @@
|
|
|
14081
14222
|
syncAppViewportHeight();
|
|
14082
14223
|
}
|
|
14083
14224
|
ensureTerminalFit("keyboard-close", { forceReplay: true });
|
|
14084
|
-
|
|
14225
|
+
// 同 handleInputBoxBlur:尊重 terminalAutoFollow,避免把上滚
|
|
14226
|
+
// 阅读历史的用户在键盘关闭瞬间拽回底部。
|
|
14227
|
+
maybeScrollTerminalToBottom("keyboard");
|
|
14085
14228
|
}, 200);
|
|
14086
14229
|
}
|
|
14087
14230
|
|
|
@@ -15497,6 +15640,9 @@
|
|
|
15497
15640
|
}
|
|
15498
15641
|
|
|
15499
15642
|
var allMessages = state.currentMessages;
|
|
15643
|
+
// 预扫一遍全量 messages,构建 task id → subagent meta 的 map,
|
|
15644
|
+
// 供老消息 tool_result(没有 __subagent 盖章)按 tool_use_id 反查兜底。
|
|
15645
|
+
var legacyTaskMap = collectLegacyTaskIdMap(allMessages);
|
|
15500
15646
|
|
|
15501
15647
|
if (allMessages.length === 0) {
|
|
15502
15648
|
if (state.lastRenderedEmpty !== "empty") {
|
|
@@ -15573,10 +15719,11 @@
|
|
|
15573
15719
|
|
|
15574
15720
|
// 在动 DOM 之前先看用户是不是贴在底部——这决定后面我们要不要让视图
|
|
15575
15721
|
// "继续粘在底部"。column-reverse 下 scrollTop 接近 0 = 视觉底部。
|
|
15576
|
-
//
|
|
15577
|
-
//
|
|
15722
|
+
// 注意:state.chatStickToBottom 的维护**完全交给 scroll handler**
|
|
15723
|
+
// (bindChatScrollListener + wheel/touch 提前下台),这里不再做
|
|
15724
|
+
// "近底即锁回 true"的自愈,避免 resize / 键盘动画 / 锚点回填瞬间
|
|
15725
|
+
// 把已经上滚阅读的用户误判回贴底状态。
|
|
15578
15726
|
var renderWasAtBottom = isChatNearBottom(chatMessages);
|
|
15579
|
-
if (renderWasAtBottom) state.chatStickToBottom = true;
|
|
15580
15727
|
|
|
15581
15728
|
// 把 .system-info 卡片从计数里剔除——它由 extractPtySystemInfo 在
|
|
15582
15729
|
// fullRenderChat 里穿插注入,不存在于 messages 数组中,混进 existingCount
|
|
@@ -15620,7 +15767,7 @@
|
|
|
15620
15767
|
}
|
|
15621
15768
|
|
|
15622
15769
|
// Render message
|
|
15623
|
-
html += renderChatMessage(msg, roundUsageByIndex[originalIndex] || null, originalIndex);
|
|
15770
|
+
html += renderChatMessage(msg, roundUsageByIndex[originalIndex] || null, originalIndex, legacyTaskMap);
|
|
15624
15771
|
}
|
|
15625
15772
|
|
|
15626
15773
|
// Add sentinel for loading older messages (DOM end = visual top in column-reverse)
|
|
@@ -15634,9 +15781,12 @@
|
|
|
15634
15781
|
// 的 data-msg-index 和它到容器顶部的偏移。重写完成后找到同一 data-msg-index
|
|
15635
15782
|
// 的新节点,把它放回原来的偏移——这是 column-reverse 下保住用户视线的
|
|
15636
15783
|
// 标准锚点法。没有锚点时(首次渲染、空 → 非空)才走 scrollTop=0 兜底。
|
|
15784
|
+
// 改用 existingCount 而非 prevMsgCount:page-refresh 等 preserveStickState
|
|
15785
|
+
// 路径下 prevMsgCount 被重置为 0,但 DOM 里仍有节点可作锚点,必须保住
|
|
15786
|
+
// 用户的阅读位置。
|
|
15637
15787
|
var anchorMsgIndex = -1;
|
|
15638
15788
|
var anchorOffset = 0;
|
|
15639
|
-
if (
|
|
15789
|
+
if (existingCount > 0 && !renderWasAtBottom) {
|
|
15640
15790
|
var containerTop = chatMessages.getBoundingClientRect().top;
|
|
15641
15791
|
var preEls = chatMessages.querySelectorAll(".chat-message:not(.system-info)");
|
|
15642
15792
|
for (var pi = 0; pi < preEls.length; pi++) {
|
|
@@ -15666,13 +15816,19 @@
|
|
|
15666
15816
|
})();
|
|
15667
15817
|
// 会话切换 / 首次渲染后,浏览器会把旧的 scrollTop 钳制到新内容
|
|
15668
15818
|
// 的最大值——column-reverse 下这意味着视觉上跳到最上面(最旧消息),
|
|
15669
|
-
// 也就是用户反馈的"退出再回来时被重定向到最上面"
|
|
15670
|
-
//
|
|
15671
|
-
//
|
|
15672
|
-
|
|
15819
|
+
// 也就是用户反馈的"退出再回来时被重定向到最上面"。
|
|
15820
|
+
// 关键:只在该会话视图的**首次**渲染(chatInitialRenderDone=false)
|
|
15821
|
+
// 才执行这个强制贴底;之后即便 prevMsgCount===0(page-refresh /
|
|
15822
|
+
// ws 重连等保留 sticky 的 reset 路径),也尊重 chatStickToBottom,
|
|
15823
|
+
// 不再把上滚的用户拽回去。
|
|
15824
|
+
if (prevMsgCount === 0 && !state.chatInitialRenderDone) {
|
|
15673
15825
|
chatMessages.scrollTop = 0;
|
|
15674
15826
|
state.chatStickToBottom = true;
|
|
15675
15827
|
clearChatUnread({ removeDivider: true });
|
|
15828
|
+
state.chatInitialRenderDone = true;
|
|
15829
|
+
} else if (prevMsgCount === 0 && state.chatStickToBottom) {
|
|
15830
|
+
// 非首次但缓存重置后的 re-render——仅在用户原本贴底时回贴。
|
|
15831
|
+
chatMessages.scrollTop = 0;
|
|
15676
15832
|
} else if (renderWasAtBottom) {
|
|
15677
15833
|
// 同一会话内的全量重渲染:用户原本贴底就保持贴底,浏览器在 innerHTML
|
|
15678
15834
|
// 重置后可能把 scrollTop 钳到一个奇怪的值,这里显式拉回 0。
|
|
@@ -15805,7 +15961,7 @@
|
|
|
15805
15961
|
for (var i = 0; i < newMessages.length; i++) {
|
|
15806
15962
|
var div = document.createElement("div");
|
|
15807
15963
|
var nmOrigIdx = visibleOffset + existingCount + (newMessages.length - 1 - i);
|
|
15808
|
-
div.innerHTML = renderChatMessage(newMessages[i], roundUsageByIndex[nmOrigIdx] || null, nmOrigIdx);
|
|
15964
|
+
div.innerHTML = renderChatMessage(newMessages[i], roundUsageByIndex[nmOrigIdx] || null, nmOrigIdx, legacyTaskMap);
|
|
15809
15965
|
var el = div.firstElementChild;
|
|
15810
15966
|
if (el) {
|
|
15811
15967
|
el.classList.add("animate-in");
|
|
@@ -15863,7 +16019,7 @@
|
|
|
15863
16019
|
var currentEl = existingEls[mi];
|
|
15864
16020
|
var tmpWrap = document.createElement("div");
|
|
15865
16021
|
var srOrigIdx = visibleOffset + reversedMessages.length - 1 - mi;
|
|
15866
|
-
tmpWrap.innerHTML = renderChatMessage(reversedMessages[mi], roundUsageByIndex[srOrigIdx] || null, srOrigIdx);
|
|
16022
|
+
tmpWrap.innerHTML = renderChatMessage(reversedMessages[mi], roundUsageByIndex[srOrigIdx] || null, srOrigIdx, legacyTaskMap);
|
|
15867
16023
|
var replacementEl = tmpWrap.firstElementChild;
|
|
15868
16024
|
if (!replacementEl) continue;
|
|
15869
16025
|
if (currentEl.innerHTML !== replacementEl.innerHTML || currentEl.className !== replacementEl.className) {
|
|
@@ -16844,6 +17000,27 @@
|
|
|
16844
17000
|
'<span class="avatar-name">' + escapeHtml(name) + '</span>' +
|
|
16845
17001
|
'</div>';
|
|
16846
17002
|
}
|
|
17003
|
+
|
|
17004
|
+
// subagent tool_result → 独立 reply 气泡(markdown 渲染)。出错时显示红色错误体,
|
|
17005
|
+
// 没文本时显示打字指示器(subagent 还在跑)。
|
|
17006
|
+
function renderSubagentReplyBubble(block, role) {
|
|
17007
|
+
if (!block || block.type !== "tool_result") return "";
|
|
17008
|
+
var text = extractToolResultText(block.content);
|
|
17009
|
+
var isError = block.is_error === true;
|
|
17010
|
+
|
|
17011
|
+
if (isError) {
|
|
17012
|
+
return '<div class="subagent-reply error">' +
|
|
17013
|
+
'<span class="subagent-reply-icon">✗</span>' +
|
|
17014
|
+
'<div class="subagent-reply-body">' + (text ? renderMarkdown(text) : escapeHtml("(无输出)")) + '</div>' +
|
|
17015
|
+
'</div>';
|
|
17016
|
+
}
|
|
17017
|
+
|
|
17018
|
+
if (!text || !String(text).trim()) {
|
|
17019
|
+
return '<div class="subagent-reply pending"><span class="typing-indicator"><span></span><span></span><span></span></span></div>';
|
|
17020
|
+
}
|
|
17021
|
+
|
|
17022
|
+
return '<div class="subagent-reply">' + renderMarkdown(text) + '</div>';
|
|
17023
|
+
}
|
|
16847
17024
|
var PIXEL_AVATAR = {
|
|
16848
17025
|
assistant: buildPixelSvg(buildCatGrid(GARFIELD_PALETTE)),
|
|
16849
17026
|
user: buildPixelSvg(buildCatGrid(SHORTHAIR_PALETTE)),
|
|
@@ -16897,16 +17074,20 @@
|
|
|
16897
17074
|
'</div>';
|
|
16898
17075
|
}
|
|
16899
17076
|
|
|
16900
|
-
function renderChatMessage(msg, roundUsage, messageIndex) {
|
|
17077
|
+
function renderChatMessage(msg, roundUsage, messageIndex, legacyTaskMap) {
|
|
16901
17078
|
// Thinking card (deep thought) — from PTY parsing
|
|
16902
17079
|
if (msg.role === "thinking") {
|
|
17080
|
+
// 空 / 全空白的 thinking 没有任何信息量,渲染出来只是一条带"展开"的紫色窄条,
|
|
17081
|
+
// 展开了也看不到内容——直接跳过。
|
|
17082
|
+
var ptyThinkingText = typeof msg.content === "string" ? msg.content : "";
|
|
17083
|
+
if (!ptyThinkingText.trim()) return "";
|
|
16903
17084
|
var thinkingKey = buildExpandKey("thinking", [getMessageKey(msg, messageIndex), "pty"]);
|
|
16904
17085
|
var thinkingPersisted = getPersistedExpandState(thinkingKey);
|
|
16905
17086
|
var thinkingExpanded = thinkingPersisted === null ? getCardDefault("thinking") : thinkingPersisted;
|
|
16906
17087
|
return '<div class="chat-message thinking">' +
|
|
16907
|
-
'<div class="thinking-inline thinking-pty ' + (thinkingExpanded ? 'expanded' : 'collapsed') + '" data-expand-kind="thinking" data-expand-key="' + escapeHtml(thinkingKey) + '" data-thinking="" onclick="__thinkingToggle(this)">' +
|
|
17088
|
+
'<div class="thinking-inline thinking-pty ' + (thinkingExpanded ? 'expanded' : 'collapsed') + '" data-expand-kind="thinking" data-expand-key="' + escapeHtml(thinkingKey) + '" data-thinking="' + escapeHtml(ptyThinkingText) + '" onclick="__thinkingToggle(this)">' +
|
|
16908
17089
|
'<span class="thinking-inline-icon">⦿</span>' +
|
|
16909
|
-
'<span class="thinking-inline-preview">' + escapeHtml(
|
|
17090
|
+
'<span class="thinking-inline-preview">' + escapeHtml(ptyThinkingText) + '</span>' +
|
|
16910
17091
|
'<span class="thinking-inline-action">' + (thinkingExpanded ? '收起' : '展开') + '</span>' +
|
|
16911
17092
|
'</div>' +
|
|
16912
17093
|
'</div>';
|
|
@@ -16924,7 +17105,7 @@
|
|
|
16924
17105
|
|
|
16925
17106
|
// Structured content blocks (from JSON chat mode)
|
|
16926
17107
|
if (Array.isArray(msg.content)) {
|
|
16927
|
-
return renderStructuredMessage(msg, roundUsage, messageIndex);
|
|
17108
|
+
return renderStructuredMessage(msg, roundUsage, messageIndex, legacyTaskMap);
|
|
16928
17109
|
}
|
|
16929
17110
|
|
|
16930
17111
|
// Legacy string content (from PTY parsing)
|
|
@@ -16980,6 +17161,8 @@
|
|
|
16980
17161
|
}
|
|
16981
17162
|
|
|
16982
17163
|
// ── 连续同类工具调用分组 ──
|
|
17164
|
+
// 注意:禁止把 Task/Agent 加入 GROUPABLE_TOOLS——它们由 renderContentBlock 入口屏蔽返空,
|
|
17165
|
+
// 加入分组会导致空 group 包裹一堆空字符串,留下视觉空盒子。
|
|
16983
17166
|
var GROUPABLE_TOOLS = { Read: 1, Glob: 1, Grep: 1, WebFetch: 1, WebSearch: 1, TodoRead: 1 };
|
|
16984
17167
|
|
|
16985
17168
|
function groupConsecutiveTools(content) {
|
|
@@ -17076,16 +17259,61 @@
|
|
|
17076
17259
|
persistElementExpandState(el, "tool-group");
|
|
17077
17260
|
};
|
|
17078
17261
|
|
|
17262
|
+
// 老消息(SQLite 历史 turn)后端还没盖章。前端给 name === "Task"/"Agent"
|
|
17263
|
+
// 或 input.subagent_type 非空的 tool_use 虚拟盖章,让现有 multi-agent 渲染路径吃到。
|
|
17264
|
+
function deriveLegacySubagent(block) {
|
|
17265
|
+
if (!block || block.type !== "tool_use") return null;
|
|
17266
|
+
var input = block.input || {};
|
|
17267
|
+
var agentType = typeof input.subagent_type === "string" ? input.subagent_type : null;
|
|
17268
|
+
if (!agentType && block.name !== "Task" && block.name !== "Agent") return null;
|
|
17269
|
+
return {
|
|
17270
|
+
taskId: block.id,
|
|
17271
|
+
agentType: agentType || undefined,
|
|
17272
|
+
taskDescription: typeof input.description === "string" ? input.description : undefined,
|
|
17273
|
+
};
|
|
17274
|
+
}
|
|
17275
|
+
|
|
17276
|
+
// tool_result 老消息靠 tool_use_id 关联 task。由外层预扫一遍 messages,构建 task id → meta 的 map。
|
|
17277
|
+
function collectLegacyTaskIdMap(allMessages) {
|
|
17278
|
+
var map = new Map();
|
|
17279
|
+
if (!Array.isArray(allMessages)) return map;
|
|
17280
|
+
for (var i = 0; i < allMessages.length; i++) {
|
|
17281
|
+
var m = allMessages[i];
|
|
17282
|
+
if (!m || m.role !== "assistant" || !Array.isArray(m.content)) continue;
|
|
17283
|
+
for (var j = 0; j < m.content.length; j++) {
|
|
17284
|
+
var b = m.content[j];
|
|
17285
|
+
if (!b || b.type !== "tool_use") continue;
|
|
17286
|
+
var derived = b.__subagent || deriveLegacySubagent(b);
|
|
17287
|
+
if (derived) map.set(b.id, derived);
|
|
17288
|
+
}
|
|
17289
|
+
}
|
|
17290
|
+
return map;
|
|
17291
|
+
}
|
|
17292
|
+
|
|
17079
17293
|
// 把一条 assistant turn 按相邻 block 的 __subagent.taskId 切成段。
|
|
17080
17294
|
// 输出每段附带原数组中的 firstIndex,方便渲染时 expand key 用全局 index
|
|
17081
17295
|
// 避免不同段冲突。
|
|
17082
|
-
|
|
17296
|
+
// legacyTaskMap:老消息没有 __subagent 盖章时,按 tool_use_id 反查兜底。
|
|
17297
|
+
function splitTurnBySubagent(blocks, legacyTaskMap) {
|
|
17083
17298
|
var segs = [];
|
|
17084
17299
|
if (!Array.isArray(blocks) || !blocks.length) return segs;
|
|
17085
17300
|
var current = null;
|
|
17086
17301
|
for (var i = 0; i < blocks.length; i++) {
|
|
17087
17302
|
var b = blocks[i];
|
|
17303
|
+
// __processing 占位 block(流式中的 typing indicator)没有 __subagent 盖章,
|
|
17304
|
+
// 强制延续上一段(若已有),避免"父-Task-占位-子内容"反复切段导致 DOM 抖动。
|
|
17305
|
+
// 边界:若占位是第一个 block(current 仍为 null),走正常路径开 parent 段。
|
|
17306
|
+
var isPlaceholder = b && b.type === "text" && b.__processing === true;
|
|
17307
|
+
if (isPlaceholder && current) {
|
|
17308
|
+
current.blocks.push(b);
|
|
17309
|
+
continue;
|
|
17310
|
+
}
|
|
17088
17311
|
var sub = b && b.__subagent ? b.__subagent : null;
|
|
17312
|
+
if (!sub) sub = deriveLegacySubagent(b);
|
|
17313
|
+
// 老消息 tool_result 兜底:用 tool_use_id 反查 map
|
|
17314
|
+
if (!sub && b && b.type === "tool_result" && legacyTaskMap && legacyTaskMap.has(b.tool_use_id)) {
|
|
17315
|
+
sub = legacyTaskMap.get(b.tool_use_id);
|
|
17316
|
+
}
|
|
17089
17317
|
var key = sub ? sub.taskId : null;
|
|
17090
17318
|
if (!current || current.key !== key) {
|
|
17091
17319
|
current = { key: key, subagent: sub, blocks: [], firstIndex: i };
|
|
@@ -17124,11 +17352,55 @@
|
|
|
17124
17352
|
return html;
|
|
17125
17353
|
}
|
|
17126
17354
|
|
|
17127
|
-
|
|
17355
|
+
// 抽出 multi-agent 渲染共用块。assistant turn 与 user turn 都可能含 subagent 段
|
|
17356
|
+
// (user turn 的 Task tool_result 由后端反查盖章),但 user turn 不再输出 handoff
|
|
17357
|
+
// 行,避免与 assistant turn 的 handoff 重复。
|
|
17358
|
+
// TODO:嵌套 subagent(子 subagent 在外层 subagent 段内再切)时,
|
|
17359
|
+
// parentPersonaName 应是外层 subagent.name 而不是固定父 persona;目前先不处理。
|
|
17360
|
+
function buildMultiAgentHtml(segments, role, parentPersonaName, toolResults, messageKey, options) {
|
|
17361
|
+
var opts = options || {};
|
|
17362
|
+
var showHandoff = opts.showHandoff !== false; // 默认 true;user turn 传 false
|
|
17363
|
+
var html = "";
|
|
17364
|
+
var lastSubId = null;
|
|
17365
|
+
for (var si = 0; si < segments.length; si++) {
|
|
17366
|
+
var seg = segments[si];
|
|
17367
|
+
var segHtml = buildSegmentBlocksHtml(seg.blocks, seg.firstIndex, role, toolResults, messageKey);
|
|
17368
|
+
// 段内所有 block 都被短路返空(典型场景:父段只剩一个空 thinking)时,
|
|
17369
|
+
// 跳过整段。否则会渲染出"只有头像没内容"的空气泡。
|
|
17370
|
+
if (!segHtml || !segHtml.trim()) continue;
|
|
17371
|
+
if (seg.subagent) {
|
|
17372
|
+
var subPalette = getSubagentPalette(seg.subagent);
|
|
17373
|
+
if (showHandoff && lastSubId !== seg.subagent.taskId) {
|
|
17374
|
+
var subName = getSubagentDisplayName(seg.subagent);
|
|
17375
|
+
var desc = seg.subagent.taskDescription
|
|
17376
|
+
? ':<span class="chat-handoff-desc">' + escapeHtml(seg.subagent.taskDescription) + '</span>'
|
|
17377
|
+
: '';
|
|
17378
|
+
html += '<div class="chat-handoff" style="--agent-color:' + subPalette.primary + '">' +
|
|
17379
|
+
'<span class="chat-handoff-arrow">↳</span> ' +
|
|
17380
|
+
escapeHtml(parentPersonaName) + ' 让 <strong>' + escapeHtml(subName) + '</strong> 帮忙' + desc +
|
|
17381
|
+
'</div>';
|
|
17382
|
+
}
|
|
17383
|
+
html += '<div class="chat-message-segment subagent" data-agent-id="' + escapeHtml(seg.subagent.taskId) + '" style="--agent-color:' + subPalette.primary + '">' +
|
|
17384
|
+
subagentAvatarHtml(seg.subagent) +
|
|
17385
|
+
'<div class="chat-message-content">' + segHtml + '</div>' +
|
|
17386
|
+
'</div>';
|
|
17387
|
+
lastSubId = seg.subagent.taskId;
|
|
17388
|
+
} else {
|
|
17389
|
+
html += '<div class="chat-message-segment parent">' +
|
|
17390
|
+
chatAvatar(role) +
|
|
17391
|
+
'<div class="chat-message-content">' + segHtml + '</div>' +
|
|
17392
|
+
'</div>';
|
|
17393
|
+
lastSubId = null;
|
|
17394
|
+
}
|
|
17395
|
+
}
|
|
17396
|
+
return html;
|
|
17397
|
+
}
|
|
17398
|
+
|
|
17399
|
+
function renderStructuredMessage(msg, roundUsage, messageIndex, legacyTaskMap) {
|
|
17128
17400
|
var role = msg.role;
|
|
17129
17401
|
var messageKey = getMessageKey(msg, messageIndex);
|
|
17130
17402
|
|
|
17131
|
-
// 排队中的用户消息标记(subagent 不会出现在 user role
|
|
17403
|
+
// 排队中的用户消息标记(subagent 不会出现在 user role 的 user input 中)
|
|
17132
17404
|
var isQueued = role === "user" && msg.content && msg.content.some(function(b) { return b.__queued; });
|
|
17133
17405
|
|
|
17134
17406
|
if (!msg.content || msg.content.length === 0) {
|
|
@@ -17147,12 +17419,22 @@
|
|
|
17147
17419
|
}
|
|
17148
17420
|
|
|
17149
17421
|
var toolResults = buildToolResultMap(msg.content);
|
|
17422
|
+
var parentPersona = getStructuredChatPersona("assistant");
|
|
17150
17423
|
|
|
17151
|
-
// user role
|
|
17424
|
+
// user role:可能含 Task tool_result(subagent 反查盖章过的)。检测一下,
|
|
17425
|
+
// 有 subagent 段就走 multi-agent 渲染(不输出 handoff,避免重复)。
|
|
17152
17426
|
if (role !== "assistant") {
|
|
17153
|
-
var
|
|
17427
|
+
var userSegments = splitTurnBySubagent(msg.content, legacyTaskMap);
|
|
17428
|
+
var userHasSub = userSegments.some(function(s) { return s.subagent; });
|
|
17154
17429
|
var queuedClass = isQueued ? " queued" : "";
|
|
17155
17430
|
var queuedBadge = isQueued ? '<span class="queued-badge">排队中</span>' : "";
|
|
17431
|
+
if (userHasSub) {
|
|
17432
|
+
var userMultiHtml = buildMultiAgentHtml(userSegments, role, parentPersona.name, toolResults, messageKey, { showHandoff: false });
|
|
17433
|
+
return '<div class="chat-message ' + role + queuedClass + ' multi-agent" data-message-key="' + escapeHtml(messageKey) + '">' +
|
|
17434
|
+
userMultiHtml + queuedBadge +
|
|
17435
|
+
'</div>';
|
|
17436
|
+
}
|
|
17437
|
+
var userHtml = buildSegmentBlocksHtml(msg.content, 0, role, toolResults, messageKey);
|
|
17156
17438
|
return '<div class="chat-message ' + role + queuedClass + '" data-message-key="' + escapeHtml(messageKey) + '">' +
|
|
17157
17439
|
chatAvatar(role) +
|
|
17158
17440
|
'<div class="chat-message-content">' + userHtml + queuedBadge + '</div>' +
|
|
@@ -17160,7 +17442,7 @@
|
|
|
17160
17442
|
}
|
|
17161
17443
|
|
|
17162
17444
|
// assistant:检测是否有 subagent 段,没有就走单段渲染(兼容老消息 / 无 subagent 的 turn)
|
|
17163
|
-
var segments = splitTurnBySubagent(msg.content);
|
|
17445
|
+
var segments = splitTurnBySubagent(msg.content, legacyTaskMap);
|
|
17164
17446
|
var hasSubagent = segments.some(function(s) { return s.subagent; });
|
|
17165
17447
|
|
|
17166
17448
|
if (!hasSubagent) {
|
|
@@ -17174,43 +17456,26 @@
|
|
|
17174
17456
|
// 多段:父 assistant 段 + 各 subagent 段。同一根 .chat-message 容器,
|
|
17175
17457
|
// 内部多个 .chat-message-segment 子段,每段自带头像;切到新 subagent 时
|
|
17176
17458
|
// 插入一行 handoff 提示("勤劳初二 ↳ 让 侦探猫 帮忙")。
|
|
17177
|
-
var parentPersona = getStructuredChatPersona("assistant");
|
|
17178
17459
|
var multiHtml = '<div class="chat-message ' + role + ' multi-agent" data-message-key="' + escapeHtml(messageKey) + '">';
|
|
17179
|
-
|
|
17180
|
-
for (var si = 0; si < segments.length; si++) {
|
|
17181
|
-
var seg = segments[si];
|
|
17182
|
-
var segHtml = buildSegmentBlocksHtml(seg.blocks, seg.firstIndex, role, toolResults, messageKey);
|
|
17183
|
-
if (seg.subagent) {
|
|
17184
|
-
var subPalette = getSubagentPalette(seg.subagent);
|
|
17185
|
-
if (lastSubId !== seg.subagent.taskId) {
|
|
17186
|
-
var subName = getSubagentDisplayName(seg.subagent);
|
|
17187
|
-
var desc = seg.subagent.taskDescription
|
|
17188
|
-
? ':<span class="chat-handoff-desc">' + escapeHtml(seg.subagent.taskDescription) + '</span>'
|
|
17189
|
-
: '';
|
|
17190
|
-
multiHtml += '<div class="chat-handoff" style="--agent-color:' + subPalette.primary + '">' +
|
|
17191
|
-
'<span class="chat-handoff-arrow">↳</span> ' +
|
|
17192
|
-
escapeHtml(parentPersona.name) + ' 让 <strong>' + escapeHtml(subName) + '</strong> 帮忙' + desc +
|
|
17193
|
-
'</div>';
|
|
17194
|
-
}
|
|
17195
|
-
multiHtml += '<div class="chat-message-segment subagent" data-agent-id="' + escapeHtml(seg.subagent.taskId) + '" style="--agent-color:' + subPalette.primary + '">' +
|
|
17196
|
-
subagentAvatarHtml(seg.subagent) +
|
|
17197
|
-
'<div class="chat-message-content">' + segHtml + '</div>' +
|
|
17198
|
-
'</div>';
|
|
17199
|
-
lastSubId = seg.subagent.taskId;
|
|
17200
|
-
} else {
|
|
17201
|
-
multiHtml += '<div class="chat-message-segment parent">' +
|
|
17202
|
-
chatAvatar("assistant") +
|
|
17203
|
-
'<div class="chat-message-content">' + segHtml + '</div>' +
|
|
17204
|
-
'</div>';
|
|
17205
|
-
lastSubId = null;
|
|
17206
|
-
}
|
|
17207
|
-
}
|
|
17460
|
+
multiHtml += buildMultiAgentHtml(segments, role, parentPersona.name, toolResults, messageKey, { showHandoff: true });
|
|
17208
17461
|
multiHtml += '</div>';
|
|
17209
17462
|
return multiHtml;
|
|
17210
17463
|
}
|
|
17211
17464
|
function renderContentBlock(block, role, toolResults, index, messageKey) {
|
|
17212
17465
|
if (!block || !block.type) return "";
|
|
17213
17466
|
|
|
17467
|
+
// Task/Agent tool_use 自带 __subagent.taskId === block.id(后端自盖章):
|
|
17468
|
+
// 不画工具卡片,由 handoff 行表达"派遣"语义。
|
|
17469
|
+
if (block.type === "tool_use" && block.__subagent && block.__subagent.taskId === block.id) {
|
|
17470
|
+
return "";
|
|
17471
|
+
}
|
|
17472
|
+
// 只有父 Task 的 tool_result(taskId === tool_use_id)走 reply bubble;
|
|
17473
|
+
// 子 agent 内部工具的 tool_result(taskId === parent_tool_use_id ≠ tool_use_id)
|
|
17474
|
+
// 走普通工具卡片,不能误判为 reply bubble。
|
|
17475
|
+
if (block.type === "tool_result" && block.__subagent && block.__subagent.taskId === block.tool_use_id) {
|
|
17476
|
+
return renderSubagentReplyBubble(block, role);
|
|
17477
|
+
}
|
|
17478
|
+
|
|
17214
17479
|
switch (block.type) {
|
|
17215
17480
|
case "text":
|
|
17216
17481
|
if (role === "assistant" && block.__processing) {
|
|
@@ -17220,7 +17485,6 @@
|
|
|
17220
17485
|
|
|
17221
17486
|
case "thinking":
|
|
17222
17487
|
var thinkingText = block.thinking || "";
|
|
17223
|
-
var preview = thinkingText.length > 60 ? thinkingText.slice(0, 57) + "…" : thinkingText;
|
|
17224
17488
|
var isStreaming = block.thinking === undefined && block.type === "thinking";
|
|
17225
17489
|
if (isStreaming) {
|
|
17226
17490
|
return '<div class="thinking-inline thinking-streaming" data-thinking="">' +
|
|
@@ -17230,6 +17494,10 @@
|
|
|
17230
17494
|
'</div>' +
|
|
17231
17495
|
'</div>';
|
|
17232
17496
|
}
|
|
17497
|
+
// 非流式分支:thinking 字段是空字符串时,UI 上只会出现一条带"展开"
|
|
17498
|
+
// 的紫色窄条,展开了也是空——直接不渲染,避免视觉噪音。
|
|
17499
|
+
if (!thinkingText.trim()) return "";
|
|
17500
|
+
var preview = thinkingText.length > 60 ? thinkingText.slice(0, 57) + "…" : thinkingText;
|
|
17233
17501
|
var thinkingKey = buildExpandKey("thinking", [messageKey, index]);
|
|
17234
17502
|
var thinkingPersisted = getPersistedExpandState(thinkingKey);
|
|
17235
17503
|
var thinkingExpanded = thinkingPersisted === null ? getCardDefault("thinking") : thinkingPersisted;
|
|
@@ -515,7 +515,30 @@
|
|
|
515
515
|
transition: width 0.3s var(--ease-out-expo), transform 0.35s var(--ease-out-expo), box-shadow 0.35s ease, opacity 0.25s ease;
|
|
516
516
|
}
|
|
517
517
|
|
|
518
|
-
|
|
518
|
+
/* 多塞一层 .sidebar-header-actions 把 specificity 抬到 0,4,0,
|
|
519
|
+
压过文件后段 .sidebar-header-actions .btn-ghost.btn-sm
|
|
520
|
+
与 .drawer-close-btn 的 display: inline-flex 重置规则。 */
|
|
521
|
+
.sidebar.pinned .sidebar-header-actions .sidebar-close {
|
|
522
|
+
display: none;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/* 窄条(collapsed)状态:明确不显示 X 关闭按钮。
|
|
526
|
+
虽然 collapsed 当前必然蕴含 pinned(见 isSidebarNarrow),
|
|
527
|
+
上一条 .sidebar.pinned 规则已经能把它藏住,但单独写一条更直观,
|
|
528
|
+
也防止以后允许 drawer 模式下 collapse 时漏掉。
|
|
529
|
+
展开(drawer 模式)下才显示关闭按钮供用户收起侧栏。 */
|
|
530
|
+
.sidebar.collapsed .sidebar-header-actions .sidebar-close {
|
|
531
|
+
display: none;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/* 与上方对称:drawer 模式(未 pin)下,X 关闭按钮已能把侧栏关掉,
|
|
535
|
+
再放一个「收起为窄条」按钮会和 X 视觉上重复,且它的行为
|
|
536
|
+
(自动 pin + collapse)也容易让人误以为是关闭。
|
|
537
|
+
只在 pinned 模式保留这颗,drawer 模式下隐藏。
|
|
538
|
+
注意:必须用 .sidebar-header-actions 多嵌一层把 specificity 提到 0,4,0,
|
|
539
|
+
否则会和文件后段 .sidebar-header-actions .btn-ghost.btn-sm (0,3,0) 打平
|
|
540
|
+
而因后定义胜出,导致 drawer 模式下窄条按钮和 X 同时显示。 */
|
|
541
|
+
.sidebar:not(.pinned) .sidebar-header-actions .sidebar-collapse-toggle {
|
|
519
542
|
display: none;
|
|
520
543
|
}
|
|
521
544
|
|
|
@@ -677,15 +700,6 @@
|
|
|
677
700
|
.sidebar-collapse-toggle.collapsed {
|
|
678
701
|
color: var(--primary);
|
|
679
702
|
}
|
|
680
|
-
/* 完全关闭侧栏 — 仅在 pinned 展开态可见(drawer 自带 X;窄条态保持极简) */
|
|
681
|
-
.sidebar .sidebar-header-actions .btn.sidebar-close-fully {
|
|
682
|
-
display: none;
|
|
683
|
-
flex-shrink: 0;
|
|
684
|
-
}
|
|
685
|
-
.sidebar.pinned:not(.collapsed) .sidebar-header-actions .btn.sidebar-close-fully {
|
|
686
|
-
display: inline-flex;
|
|
687
|
-
}
|
|
688
|
-
|
|
689
703
|
/* ===== 图钉按钮 ===== */
|
|
690
704
|
.sidebar-pin-toggle {
|
|
691
705
|
flex-shrink: 0;
|
|
@@ -4117,12 +4131,19 @@
|
|
|
4117
4131
|
gap: 6px;
|
|
4118
4132
|
max-width: 95%;
|
|
4119
4133
|
}
|
|
4134
|
+
/* user turn 默认 align-self: flex-end(右对齐);多段渲染时强制
|
|
4135
|
+
容器内左对齐,否则 subagent 段会被推到右边,border-left 色条断裂。 */
|
|
4136
|
+
.chat-message.user.multi-agent {
|
|
4137
|
+
align-self: flex-start;
|
|
4138
|
+
align-items: flex-start;
|
|
4139
|
+
}
|
|
4120
4140
|
.chat-message.multi-agent .chat-message-segment {
|
|
4121
4141
|
display: flex;
|
|
4122
4142
|
flex-direction: column;
|
|
4143
|
+
align-self: flex-start;
|
|
4123
4144
|
}
|
|
4124
4145
|
.chat-message.multi-agent .chat-message-segment.subagent {
|
|
4125
|
-
padding-left:
|
|
4146
|
+
padding-left: 10px;
|
|
4126
4147
|
margin-left: 6px;
|
|
4127
4148
|
border-left: 2px solid var(--agent-color, var(--border-subtle));
|
|
4128
4149
|
border-radius: 0 4px 4px 0;
|
|
@@ -4165,6 +4186,56 @@
|
|
|
4165
4186
|
font-style: italic;
|
|
4166
4187
|
}
|
|
4167
4188
|
|
|
4189
|
+
/* subagent 回复气泡(群聊角色完成任务后的发言) */
|
|
4190
|
+
.subagent-reply {
|
|
4191
|
+
background: color-mix(in srgb, var(--agent-color, var(--accent)) 6%, var(--bg-surface));
|
|
4192
|
+
border: 1px solid color-mix(in srgb, var(--agent-color, var(--accent)) 20%, transparent);
|
|
4193
|
+
border-radius: 10px;
|
|
4194
|
+
padding: 10px 14px;
|
|
4195
|
+
margin: 4px 0;
|
|
4196
|
+
line-height: 1.55;
|
|
4197
|
+
font-size: 0.875rem;
|
|
4198
|
+
color: var(--text-primary);
|
|
4199
|
+
word-break: break-word;
|
|
4200
|
+
}
|
|
4201
|
+
|
|
4202
|
+
.subagent-reply > :first-child { margin-top: 0; }
|
|
4203
|
+
.subagent-reply > :last-child { margin-bottom: 0; }
|
|
4204
|
+
|
|
4205
|
+
.subagent-reply.error {
|
|
4206
|
+
background: color-mix(in srgb, var(--danger, #e2574c) 8%, var(--bg-surface));
|
|
4207
|
+
border-color: color-mix(in srgb, var(--danger, #e2574c) 35%, transparent);
|
|
4208
|
+
}
|
|
4209
|
+
|
|
4210
|
+
.subagent-reply.pending {
|
|
4211
|
+
opacity: 0.7;
|
|
4212
|
+
padding: 14px 16px;
|
|
4213
|
+
}
|
|
4214
|
+
|
|
4215
|
+
.subagent-reply-icon {
|
|
4216
|
+
display: inline-block;
|
|
4217
|
+
color: var(--danger, #e2574c);
|
|
4218
|
+
margin-right: 6px;
|
|
4219
|
+
font-weight: 700;
|
|
4220
|
+
}
|
|
4221
|
+
|
|
4222
|
+
.subagent-reply-body {
|
|
4223
|
+
display: inline;
|
|
4224
|
+
}
|
|
4225
|
+
|
|
4226
|
+
.subagent-reply pre,
|
|
4227
|
+
.subagent-reply code {
|
|
4228
|
+
font-size: 0.82rem;
|
|
4229
|
+
}
|
|
4230
|
+
|
|
4231
|
+
.subagent-reply pre {
|
|
4232
|
+
background: rgba(0, 0, 0, 0.04);
|
|
4233
|
+
padding: 10px 12px;
|
|
4234
|
+
border-radius: 6px;
|
|
4235
|
+
overflow-x: auto;
|
|
4236
|
+
margin: 8px 0;
|
|
4237
|
+
}
|
|
4238
|
+
|
|
4168
4239
|
/* ===== 消息使用量信息 ===== */
|
|
4169
4240
|
.message-usage {
|
|
4170
4241
|
margin-top: 8px;
|
|
@@ -6845,6 +6916,38 @@
|
|
|
6845
6916
|
transform: scale(0.95);
|
|
6846
6917
|
}
|
|
6847
6918
|
|
|
6919
|
+
/* "立即发送":结构化会话流式输出时显示,按下会中断当前回复立即处理新输入。
|
|
6920
|
+
视觉上比 send 弱、比 stop 弱,避免被误点;和 send 之间留一点空气。 */
|
|
6921
|
+
.btn-circle-interrupt {
|
|
6922
|
+
background: rgba(160, 110, 60, 0.10);
|
|
6923
|
+
color: var(--accent);
|
|
6924
|
+
margin-right: 2px;
|
|
6925
|
+
border: 1px dashed rgba(197, 101, 61, 0.35);
|
|
6926
|
+
}
|
|
6927
|
+
.btn-circle-interrupt:hover {
|
|
6928
|
+
background: var(--accent);
|
|
6929
|
+
color: white;
|
|
6930
|
+
border-style: solid;
|
|
6931
|
+
transform: scale(1.08);
|
|
6932
|
+
}
|
|
6933
|
+
.btn-circle-interrupt:active {
|
|
6934
|
+
transform: scale(0.95);
|
|
6935
|
+
}
|
|
6936
|
+
.btn-circle-interrupt.hidden {
|
|
6937
|
+
display: none;
|
|
6938
|
+
}
|
|
6939
|
+
|
|
6940
|
+
/* send 按钮在「排队模式」下退到次要色,让相邻的"立即发送"成为视觉重点。
|
|
6941
|
+
不动尺寸 / 圆形,避免 layout shift。 */
|
|
6942
|
+
.btn-circle-send.queue-mode {
|
|
6943
|
+
background: linear-gradient(180deg, rgba(197, 101, 61, 0.55) 0%, rgba(168, 82, 47, 0.55) 100%);
|
|
6944
|
+
box-shadow: 0 1px 4px rgba(197, 101, 61, 0.15);
|
|
6945
|
+
}
|
|
6946
|
+
.btn-circle-send.queue-mode:hover {
|
|
6947
|
+
background: linear-gradient(180deg, var(--accent) 0%, #a8522f 100%);
|
|
6948
|
+
box-shadow: 0 3px 10px rgba(197, 101, 61, 0.35);
|
|
6949
|
+
}
|
|
6950
|
+
|
|
6848
6951
|
/* Prompt optimize button — top-right of the input composer */
|
|
6849
6952
|
.prompt-optimize-btn {
|
|
6850
6953
|
position: absolute;
|