- 装配只认 prompt_order.enabled;squash_system_messages 只合并相邻 system(新增 system 档,连接符按酒馆用单换行)
- 包装层:new_chat_prompt、示例对话(new_example_chat_prompt + <START> 切段 + name)、角色卡三块与 personality_format/scenario_format、{{user}}/{{char}} 取值、任务契约并进预设条目
- 工具协议:按 ToolBindings / 伪工具协议数据声明真 tools(presetToolPolicy=preset),工具描述用到的条目不再重复作为消息发出
- 正则:promptOnly 作用于对话历史(含 depth 过滤)、写入时脚本改写回复;{{//}} 注释按配平扫描
- tools/st-request-diff:真假上游抓包对比工具、三份预设的机制结论与实测报告
7.8 KiB
直聊(chat.html)思维链默认折叠,点击展开
Context
直聊页面的多模型回复卡(.chat-reply-card)把模型的思维链整段铺在正文上方,一张卡的可视高度被思维链吃掉,正文(真正要看的内容)被挤到下面。截图现场:盲测模式下的「模型 1」卡,头部下面整块浅色区域就是思维链,之后才是终端流水线正文。
目标:思维链收成一行折叠条,默认收起,点一下展开;流式期间模型只吐思考时自动展开,正文一开始到达就自动收起。
已定决策(用户)
- 流式阶段:自动展开,正文开始后自动收起(不是"始终保持折叠")。
- 折叠条文案:带字数,形如
思考过程 · 1234 字。 - 右上角「复制」:仍然包含思维链(
replyPlainText不动)。
现状(已确认)
web/chat.jsreplyBodyHtml(body)§902-908:思维链<div class="chat-bubble-thinking">+ 正文,永远展开。renderReplyCard()§893 调用它;paintStreamingReply()§1848-1857 每帧textEl.innerHTML = replyBodyHtml(body),任何展开状态都会被重渲染冲掉,所以展开状态必须存在模块级集合里,不能只留在 DOM。- 流式钩子:
onDelta收到event.field === "thinking"累加reply.thinking,否则累加reply.content(§1952-1956、§2112-2116)。 - 卡片内状态集合的既有惯例:
revealedCards、expandedCards都是Set<${turnId}:${windowId}>,key 用cardExpandKey(turnId, windowId)(§449);移除模型时按key.endsWith(`:${windowId}`)清理(§2479-2484)。 - 卡片点击是委托在 arena 上的单一大 handler(§2790-2880),
expand-card/reveal-card都在里面处理,reveal-card走applyCardReveal()就地改 DOM 不重渲染。 - 拖动滚动:
pointerdown里e.target.closest("button, a, textarea, input")才跳过(§1129),summary不在名单里 → 点折叠条会先进入拖动状态。
web/chat.css- §1155
.chat-bubble-thinking:浅底、muted、12px、margin: 0 0 8px。改完即无引用,直接替换。 - §815-836 多卡并排时卡片是固定高度、
.chat-reply-card-text内部滚动;单卡时高度 auto。
- §1155
web/agent-ui.js§93renderThinkingBlock()是主对话的同类实现(<details class="msg-thinking">,默认折叠),样式在web/styles.css§2948 —— 但直聊页不加载 styles.css,不能直接复用 class,只复用手法。- 无前端单测,
tests/全是src/的 vitest;验证靠手动跑页面。
Approach
-
状态:新增
const thinkingToggles = new Map();,key 同cardExpandKey,value 是用户手动选择(true展开 /false收起)。 -
展开判定(纯函数,渲染与流式刷新共用):
function thinkingOpenFor(key, body) { const manual = thinkingToggles.get(key); if (manual !== undefined) return manual; // 用户点过就听用户的 const hasThinking = Boolean(String(body.thinking ?? "").trim()); return hasThinking && !String(body.text ?? "").trim(); // 只有思考、还没正文 → 自动展开 }- 流式:只有
thinking→ 自动展开;content一有内容 → 自动收起(就是选择的方案 B)。 - 加载历史/结束后既有思考又有正文 → 默认收起。
- 用户在流式中点开过 → 一直保持展开(用户意图优先)。
- 流式:只有
-
渲染:
replyBodyHtml(body, key)产出<details class="chat-think" open?> <summary class="chat-think-summary">思考过程 · N 字</summary> <div class="chat-think-body">…renderMarkdownToHtml(thinking)…</div> </details>字数取
thinking.trim().length(中文字符数即字数,流式时随内容增长)。renderReplyCard和paintStreamingReply都传cardExpandKey(turnId, windowId)。 -
点击展开:在 arena 的委托 handler 里,
expand-card之前加一段——命中summary.chat-think-summary时e.preventDefault(),读details.open取反,写回thinkingToggles并直接改details.open(不整卡重渲染,不打断选中)。键盘 Enter/Space 在summary上也会派发 click,一并生效。 -
别被拖动吞掉:
pointerdown的排除名单加summary。 -
样式:把
.chat-bubble-thinking换成.chat-think一组(虚线边框/muted/12px,与.msg-thinking同气质但用 chat.css 的--st-*变量),summary保留原生三角、cursor: pointer、user-select: none、hover 提亮;.chat-arena.is-copy-off下summary仍保持cursor: pointer。展开后正文不再受 12px 之外的额外限制(不额外给 max-height,避免卡片内嵌套滚动条和现有 wheel 逻辑打架)。 -
清理:
removeWindow的清理循环里一并删thinkingToggles的:windowId项;retryReplies清掉该卡thinking时同步删掉对应 toggle,避免重试后残留"强制展开"。
Files to modify
web/chat.js— 状态集合、thinkingOpenFor、replyBodyHtml、renderReplyCard、paintStreamingReply、委托 handler、pointerdown排除、removeWindow/retryReplies清理web/chat.css—.chat-bubble-thinking→.chat-think/.chat-think-summary/.chat-think-body,copy-off 下的 cursor 覆盖
不动:replyPlainText(复制仍含思维链)、export.js、agent-ui.js(主对话早已折叠)。
边界 / 已知取舍
- 删掉唯一一轮回复或整段会话时
thinkingToggles可能留残留 key —— 与现有revealedCards一致,且 key 带 turnId 不会串到别的轮次。 - 多条并排时卡片是固定高度,展开思维链后内容在
.chat-reply-card-text内滚动,不重算collapseReplyTracks()(重算会把整行高度重新洗一遍,副作用更大)。「展开」按钮的可见性仍按折叠前的测量结果,可接受。 - 思维链全为空 → 不渲染折叠条(行为同现状)。
Steps
web/chat.js:加thinkingTogglesMap、thinkingOpenFor()、thinkingSummaryText()web/chat.js:replyBodyHtml(body, key)改成<details>结构,字数进 summaryweb/chat.js:renderReplyCard()与paintStreamingReply()传 keyweb/chat.js:委托 handler 处理summary.chat-think-summary(preventDefault + 写 map + 就地开合)web/chat.js:pointerdown排除summary;removeWindow/retryReplies清理 mapweb/chat.css:替换.chat-bubble-thinking为.chat-think*样式组- 手动回归:单卡 / 双卡、盲测、流式、复制、移除模型
Verification
- 语法自检:
node --check web/chat.js(浏览器 ESM,仅查语法;或npx esbuild web/chat.js --bundle=false --loader=js > $null)。 npm run web打开http://localhost:<port>/chat.html(端口见 server 启动日志):- 普通对话,模型有思维链 → 卡片顶部只有一行
思考过程 · N 字,正文直接从顶部开始;点开后思维链以 markdown 展开、再点收起。 - 打开浏览器 DevTools Network 慢速/用长思考模型,观察流式:正文未到时折叠条自动展开、文本在增长;第一段正文到达后自动收起;此时再手动点开应一直保持展开直到本轮结束、且不被后续流式刷新冲掉。
- 双模型并排 + 盲测(「模型 1」标签):折叠/展开两张卡互不影响;展开后卡片内部可滚动,「展开/收起」按钮行为不变。
- 右上角「复制」→ 粘贴检查内容仍含思维链 + 正文。
- 「移除」某模型、「重试」失败卡片后,展开状态不串卡。
- 关闭复制模式(copy-off)点折叠条仍能正常开合、不触发拖动。
- 普通对话,模型有思维链 → 卡片顶部只有一行
- 刷新页面重新加载历史会话(
thinking由服务端持久化)→ 仍是默认折叠。