DeepSeek Harness 消息输入框滚动一致性改造:两个文本层如何共享一个滚动视口
人工智能AI AgentAgent 框架DeepSeek【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址https://gitcode.com/gh_mirrors/de/deepseek-harness点击查看免费下载本篇技术笔记还原 DeepSeek Harness开源仓库 deepseek-harnessWeb 端对话 composer 输入框的一次关键 Bug 修复它原本用「透明textarea 绘制层 backdrop」叠加渲染草稿文本以支撑 claim-token 高亮、chips 与幽灵提示但两层各自持有一个滚动偏移在滚动手势下会发生 caret 与字形肉眼可见的错位。文章将完整展开两层文本架构的成因、两个阶段的缺陷、「单滚动视口」的最终决策、随之删除的两套前提、caret reveal 的细节规则、全部备选方案与测试证据并对照当前仓库源码给出可继续深入验证的入口。读完你不仅能复现和定位同类「双层文本不同步」问题还能理解为什么结构性约束一个盒子、一个偏移优于任何事件监听层面的修补。背景composer 为什么必须叠两层文本composer 把草稿文本画在两个上下堆叠的层里改动前的结构见 InputBar.tsx 的历史实现其插槽入口当前仍位于同文件底层textarea持有值value、选区selection与光标caret但自身的字形以color: transparent渲染用户看不见上层[data-input-backdrop]div 绘制所有可见字形同时承载 claim-token 高亮、chips引用/指令装饰和 ghost hint占位提示。这种拆分之所以必要是因为textarea 无法为自身文本的某个范围单独施加样式——它只能渲染一段统一的文本。而 claim 高亮、chips 这些需要“文字中间插入装饰”的能力只有让一个可自由布局的 DOM 层来投影文本才能实现。此外草稿框有14 行的高度上限CSS 中由--dsh-composer-text-max-height常量承载实测为 336px 14 × 24px 行高见 composer-draft-scroll.e2e.ts 的注释。一旦草稿超过上限必然有某个盒子需要滚动。于是问题来了两层、两个滚动偏移就注定在两个阶段分别失败。问题一静态backdrop 被裁剪而非滚动caret 与字形冻结分离最初backdrop 的定位是position: absolute; inset: 0; overflow: hidden——它是被裁剪的而不是被滚动的浏览器也没有任何机制把它的偏移与 textarea 的偏移关联起来。因此当草稿超过 14 行上限后caret 随 textarea 的内容继续移动字形却冻结在第 1 行backdrop 的裁剪框纹丝不动。低于上限时这个缺陷完全不可见因为两层都静止在偏移 0该缺陷与上限本身同龄一直藏在每个截图所记录的静止状态背后。它的修复方式是从scroll监听器把 textarea 的scrollTop镜像到 backdrop 上——这使两层在**静止at rest**时保持一致。这也是后来被取代的“镜像方案”的开端。问题二动态快速滑动时 caret 带着“惯性”飞出自己的文本用户随后报告从长草稿顶部快速滑动caret 像带惯性一样被甩出文本之外片刻后才落回来。镜像方案正是元凶。原因链如下wheel 手势在**合成器线程compositor thread**直接滚动 textarea绕过了主线程驱动scrollTop赋值的scroll事件之后才派发到主线程因此在事件派发前的若干帧里caret 位于新偏移而所有字形还停在旧偏移。作者在相同几何尺寸的独立 harness 上实测把偏移移动 200px并在任务结束前读取 caret 与其自身字形的距离——引擎同一任务内的分离距离一两帧后的稳定值Chromium203px3pxFirefox202px2pxWebKit203px3px即稳定后收敛到固定的行盒常数chromium/firefox/WebKit 各为 3/2/3而滚动瞬间的分离量与用户滚动速度成正比——每个引擎、每个手势都会复现。关键洞察是没有任何监听器能弥合这个间隙因为“间隙”本身就是监听器的定义——它运行于它所响应的事情之后。任何用 JavaScript 维持两个盒子相等的手段都会落后于“未经询问就移动其中一个盒子”的合成器整整一帧。决策一个滚动盒子容纳两个层最终方案一句话[data-input-scroll]成为 composer 唯一的滚动视口scrollport并承载 14 行上限。其内部结构为自动增长auto-grow的堆栈与整个草稿等高的——隐藏的镜像 divhidden mirror回到正常文档流、不再设上限用它撑起堆栈到全文高度backdrop 与 textarea 以绝对定位骑在这个高度上textarea 改为overflow: hidden自身不再有任何可滚动溢出因此它连“持有一个偏移”的可能性都被消除。这样浏览器在同一帧、同一个合成器上给两层施加同一个偏移。caret 与字形由构造by construction绑定而非维护by upkeep绑定没有代码要运行、没有事件要等待、没有任何状态可能滞后一帧。原 wheel 链式处理器被保留只是从 textarea 重定向到滚动视口并且它成为这个盒子上唯一的监听器——当前源码中的实现见 InputBar.tsx 内onWheel处理器滚动视口到达边界时才把增量转发给[data-conversation-scroll]宿主。唯一的引擎例外是 Safari其原生文本控件在“跨软换行阈值删除”时可能保留旧的换行布局需要单独的 Safari soft-wrap recovery对 textarea 与滚动视口依次做一像素高度失效并强制两次布局来恢复scrollHeight clientHeight不变量但不改变单滚动视口设计本身。随之删除的前提一backdrop 的“尾部换行哨兵”旧机制需要保证两个盒子的滚动范围相等为此 backdrop 末端挂着一个哨兵行。原因是textarea 在末尾换行后会为 caret保留一个行盒而white-space: pre-wrap会把文本节点的尾部换行折叠掉。于是“以换行结尾的草稿”会让 backdrop 比 textarea 短一行镜像的偏移被钳制在 caret 上方一行处。改用单滚动视口后backdrop 自身的高度范围不再决定任何东西镜像 div 为两层共同撑起堆栈两层从同一顶部开始内容先结束的那一层只在最后一行不画任何东西而已。作者特别提醒记住的是这个形状而非机制本身在两盒必须就高度达成一致的时代实测值是628 对 652。随之删除的前提二“换行宽度”前提旧架构中各层在各自盒内解析宽度一个占据布局空间的滚动条会对它们造成不同的宽度损失。前一份被取代的笔记记录过一个“开放且任何属性都无法修复”的分歧WebKit 只为overflow-y: auto的 textarea 预留滚动条 gutter 空间却不给它旁边的overflow: hidden层预留导致 textarea 按 768 布局、旁边层按 776 布局——在长草稿上相当于 2~5 个换行也就是“字形出现在错误的 caret 之下”且只在 WebKit 上可观测。单滚动视口下三层共享同一包含块宽度由构造一致改动后在 harness 上测量Playwright 内置的三套引擎chromium/firefox/WebKit中三层都报告同一个宽度新几何实测 1264/1264/1264。由我们自己执行的 caret reveal问题setSelectionRange 不触发滚动粘贴与剪切会抑制原生编辑——机器input machine持有草稿与撤销日志——然后通过setSelectionRange恢复 caret而setSelectionRange不会 reveal滚动到可见。实测在 chromium 与 WebKit 中粘贴一大段文本后视图停在原地caret 却位于所粘贴内容的末尾Firefox 碰巧只在旧几何中暴露过它。该缺陷早于本次改动存在单滚动视口之所以能修复它是因为这终于让 reveal 变成我们可以执行的事。实现用隐藏镜像测量 caret两条恢复路径粘贴/剪切共享一个 helper因为镜像与草稿同一度量、同一换行宽度所以在 caret 索引处折叠的 Range就能报告 caret 的位置——无需任何 caret API——然后滚动“使其进入视野”所需的最小距离这正是浏览器在打字时所做的事。特殊规则换行之后的 caret各引擎对一个“紧跟换行的 caret”意见不一致它坐在一行为空的行上尾部换行草稿正是如此结束的chromium 对折叠位置不返回任何 client rect一个全零盒子会把 reveal 引向错误方向firefox 报告上一行WebKit 报告正确的那行。因此 helper 改为测量 caret刚离开的那个换行符——它的盒子就是 caret 来源的那一行——再向下走一行。这样三套引擎都落在同一偏移652 中的 649caret 所在行位于 336px 盒子内的 315 处。而一个非折叠Range 覆盖该换行时即使草稿以连续换行结尾三套引擎也都返回真实矩形所以每个尾部空行都按同一条规则组合。把 reveal 交还给浏览器在单滚动视口下“reveal caret”成为唯一依赖浏览器而非我们自己的环节textarea 自身没有偏移它的 scroll-into-view 必须上溯到滚动视口。所有被测量的引擎都做到了在草稿末尾打字会把滚动视口带到 caretchromium/firefox/WebKit 分别为 625/626/628最大 628用ArrowUp把 caret 走回上方会滚回去滚走之后再打字也会返回。当前源码中与此对应的revealSelectionhelper 及其调用点unlock effect、迟到草稿 effect就在 InputBar.tsx 中与笔记描述的实现一一对应。备选方案与否决理由笔记完整记录了对以下 10 个方案的推演可作为同类问题的决策清单从scroll监听器镜像scrollTop到 backdrop——被取代的旧决策静止时正确但“无法在运动中正确”它是主线程对合成器线程事实的反应且依赖两个前提范围相等、换行宽度相等而每个前提都已经失败过一次。用transform: translateY(-scrollTop)平移 backdrop——同样滞后仍主线程、仍由同一事件驱动且只是掩盖范围分歧而不是让层一致一旦有人测量 backdrop 分歧就重新浮现。用滚动驱动动画animation-timeline: scroll()驱动 backdrop——确实能在合成器上运行耦合、在保留两盒的前提下消除滞后但 Safari 未实现、Firefox 近期才支持缺支持的引擎会保留原缺陷而回退路径正是被替换的机制本身因此否决。JavaScript 同时滚动两层textareaoverflow: hiddenwheel 处理器在同一个任务里给两层赋偏移——滚动手势期间不会分歧没有我们的参与就没有滚动但要用手写近似替代原生滚动动量、触控板橡皮筋、滚动条拖拽、键盘滚动且 caret-reveal 路径浏览器设置 textarea 自身偏移仍然异步落地。保留镜像的上限只把今天的结构包进一个 scroller——层仍是窗口尺寸而非草稿尺寸绝对定位子元素的inset: 0相对滚动视口的padding box解析而不是其可滚动溢出区两层会从本该垫在其下的内容上滚走。堆栈必须等于全文高度这个安排才有意义。给 backdropoverflow: auto让它自己滚——它就有了需要保持同步的自身偏移等于同一个问题外加一条盖在输入框上的滚动条。backdrop 是 textarea 的投影不是可独立导航的表面。丢掉 backdrop直接样式化 textarea 的文本——彻底移除分层和整类失步但不可实现textarea 只渲染一段统一文本claim-token 高亮、chips、ghost hint——backdrop 存在的全部理由——无法表达。用特性删除换一个有边界的缺陷不成比例。用contenteditablediv 渲染草稿——单元素单偏移、可样式化范围但会把 IME 组合、撤销/重做、选区语义、粘贴归一化全部推回给我们而输入机器已经拥有一个假设 textarea 值语义的撤销日志。三层都加scrollbar-gutter: stable——曾尝试并回退WebKit 只把它应用给overflow-y: auto而不给overflow: hidden8px 的缺口依然存在却让每个 chromium 用户损失 8px 文本列。如今层共享包含块没有东西需要预留该方案自然作废。scrollbar-width: none隐藏滚动条以均分宽度——被否决composer 故意在草稿越过上限后显示滚动条——.card绑定 l2 滚动条 token 正是为此——而且它是唯一提示“长草稿下面还有内容”的可见线索。后果清单笔记逐一记录了该决策带来的可验证后果caret 不可能离开自己的字形浏览器只滚动一个盒子textarea 放行与 backdrop 画字形之间的分离在任何偏移下都是固定行盒常数手势中途亦然。滚动条从 textarea 移到滚动视口视觉位置相同、向外挪了一个盒子.card的 l2 token 绑定依然继承下来。chips、claim-token 高亮、text-ref 标记在滚动时仍与字形对齐它们定位在 backdrop 内部、随其移动装饰遍历除了去掉哨兵外没有变化。Firefox 与 WebKit 的连带滚动点击一个草稿溢出上限的 composer 会顺带把对话记录滚到底——caret 的 scroll-into-view 会越过 composer 的滚动视口继续上溯到 transcript 的滚动视口textarea 比盒子短时永远不会发生chromium 不会。实测overscroll-behavior: contain与contain: paint都拦不住——没有任何 CSS 能终止 scroll-into-view 链。团队接受它它朝 composer 本就所在的方向底部滚动而替代方案是每套引擎上 caret 都明显脱离文本。Paging 与拖选不变新旧几何都实测过PageDown/PageUp本来就不移动 textarea 的 caret——chromium 滚一页、selectionStart原地不动只是滚动的盒子变了越过底边拖选仍会自动滚动到同一位置chromium 628/628、firefox 625/620、WebKit 170/170WebKit 较慢的自动滚动前后一样慢。composer 自己的focus()传preventScroll解锁/会话切换 effect 与工具栏按钮的 focus-keeping mousedown 都这么做使“没有人手势化发起的 focus”无法经由更高的 textarea 触发 reveal 链去挪动 transcript。压制这条链把 caret 交还给我们处理——composer DOM 跨会话复用切到更长的草稿会保留旧偏移而值交换把 caret 放到新草稿末尾实测三套引擎上 caret 落在偏移 0 的盒子下方 940pxeffect 于是在自己的滚动视口内 reveal 它落到 625/628旧几何经由浏览器到达 628。mousedown 路径不需要 revealcaret 没动过下一次击键会得到浏览器原生的 reveal。渲染后到达的非空草稿需要独立的 reveal-only effectConversationSession在自己的 mount effect 里播种持久化草稿而父组件 effect 运行在子组件之后所以首个 reveal 会测量到空的镜像而不会为随后出现的草稿再跑一次。第二个 effect 从不 focus普通空/非空转换发送清空、发送失败恢复不会从其他控件抢焦点。这一点在单元测试a persisted draft adopted after mount does not steal focus from another control中直接断言。Undo/redo 仍可在无 caret 恢复或 reveal 的情况下改变草稿机器重放上一个草稿DOM 选区停留在浏览器钳制的位置。这早于本次改动、也不被其改变——笔记点名它是因为“那两个会 reveal 的恢复”让这个遗漏看起来像刻意为之而 helper 就在旁边一旦被报告即可接上。结构耦合警告任何加在 backdrop 旁边的层必须放进滚动视口内、且与草稿同高否则会精确重演该缺陷。双层拆分对 chips/高亮是承重的因此耦合必须结构性、而非维护性。测试单元、端到端与双几何对照单元测试jsdom 可见的部分input-bar.client.spec.tsx 断言 jsdom 能看见的事实一个滚动盒子同时容纳 textarea 与 backdrop、backdrop 的文本就是草稿本身、以及迟到的持久化草稿会在不抢占其他控件焦点的情况下 reveal caret。由于 jsdom 对每个元素都报告scrollHeight clientHeight且从不滚动任何元素几何问题归属浏览器场景wheel 链式用例通过 stub 滚动视口的度量而非 textarea 的度量来驱动。端到端测试真实引擎上的几何composer-draft-scroll.e2e.ts 在 chromium 中对着构建后的客户端测量其余部分新工作区的空白 composer 中输入一个 40 行草稿零模型调用走失的流会以NO_ADAPTER大声失败。每个度量都在caret 自身的坐标系读取——textarea 把第 n 行放在哪、偏移是多少——对照 backdrop 文本上同一行的 DOM Range因为“这个差值”才是用户看到的东西。决定性用例是改变偏移、然后在任务结束前重读该差值——此时任何scroll监听器都不可能已运行单滚动视口下差值为 0镜像方案下则是完整 delta。测试还包含一个vacuity guard先断言草稿确实溢出了受限盒子否则后续断言全部平凡成立。其余独立用例覆盖14 行上限、三层同一换行宽度、wheel 手势、尾部换行草稿滚动到真正的末尾而非其上一行、以及 caret-reveal 路径——滚走之后再打字必须把滚动视口带回 caret这正是旧 textarea 自身滚动所处理的路径。粘贴路径单独端到端覆盖短草稿、caret 在末尾、一个携带真实剪贴板数据的paste事件——与 Cmd-V 交付的同一事件、走同一 handler——然后断言偏移与最后粘贴的一行。测试等待偏移而不是等待草稿溢出因为恢复restore在机器提交草稿之后一帧才落地没有 reveal 的构建会挂掉这次等待。双几何对照实现前的独立 harness旧与新两种几何不可能同时存在于应用中因此决策背后的对比在独立 harness 上先行测量同一任务内的 caret/字形分离旧203/202/203pxchromium/firefox/WebKit对新的3/2/3px换行宽度旧 WebKit768 对 776对新的三引擎1264/1264/1264新几何中 textarea 自身可滚动溢出为0——这使“第二个偏移”不是“恰好相等”而是“不可能存在”。构建注意composer 随client-module bundle发布因此单独运行pnpm run build:web不会拾取InputBar.tsx的改动——必须同时运行对应包的构建浏览器 lane 才能看到它对着过期的lib/跑场景等于在断言一棵比工作树更旧的客户端。从当前源码印证这套设计从当前仓库源码结构看单滚动视口的不变量在后续演进中被保留并进一步收紧InputBar.tsx 仍以[data-input-scroll]作为 composer唯一滚动盒内部grow堆栈承载编辑表面14 行上限由 InputBar.module.css 中.scroll { max-height: var(--dsh-composer-text-max-height); overflow-y: auto }落实.card负责把 l2 滚动条 token 继承到滚动盒。单元测试one editable surface rides the scrollport (no mirror or backdrop layers)明确断言[data-input-backdrop]与[data-input-mirror]已不存在——说明后续把“textarea backdrop 镜像”收敛为单一可编辑表面Lexical contenteditable一个盒子、一个偏移的结构原则延续下来两层时代遗留的几何耦合被彻底合并。e2e 的 golden 文件记录的是关系而非绝对坐标是否溢出、可见行数、表面自身可滚动溢出是否为零、首尾行是否在屏使 cap 或 reveal 行为的任何漂移都成为可评审的 diff而不是需要人脑重建的断言。Safari 侧的原生文本控件回缩恢复独立成册见 Safari textarea soft-wrap reflow。这条修复路径的完整决策记录含中文版归档于 2026-07-31-composer-text-layers-share-one-scrollport.md中文版。对需要在自家项目里实现“高亮/装饰层 原生编辑控件”叠加渲染的开发者而言最有迁移价值的结论是当两层必须同屏时让浏览器为它们提供同一个滚动盒而不是用事件去同步两个盒子——前者把一致性变成构造事实后者则永远欠合成器一帧。赞分享人工智能AI AgentAgent 框架DeepSeek【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址https://gitcode.com/gh_mirrors/de/deepseek-harness点击查看免费下载相关推荐DeepSeek Harness 会话列滚动修复Sticky Composer 与转录滚动视口的统一设计DeepSeek Harness 会话列滚动修复Sticky Composer 与转录滚动视口的统一设计 导读 DeepSeek HarnessEveryt人工智能AI AgentAgent 框架DeepSeek修复SDXL VAE fp16问题的最佳实践SDXL-VAE-FP16-Fix全面测评修复SDXL VAE fp16问题的最佳实践SDXL VAE FP16 Fix全面测评 如果您在使用SDXL模型进行AI图像生成时遇到了fp16精度下的NaN人工智能AI AgentAgent 框架DeepSeek一条命令装好 DeepSpeedWindows 原生安装与验证速成指南一条命令装好 DeepSpeedWindows 原生安装与验证速成指南 从 0.14.5 起DeepSpeed一个让分布式训练与推理变得更简单的深度学习优人工智能大模型深度学习分布式训练模型推理服务模型优化上一篇TeamPass角色权限管理终极指南如何配置精细化的访问控制下一篇突破视觉瓶颈VRM4U中VRM1与VRM0材质实例的渲染差异深度解析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考