deepseek-harness/.agents/notes/implemented/bug-fix/2026-08-11-preset-card-description-clamp.zh.md
Yichen Jiang d6d7953ae9 fix(web): flip a tooltip that the viewport has no room for
The bubble clamped horizontally and did nothing vertically, so a long
label under an anchor low on the page ran off the bottom. Preset cards
made that the common case: custom presets sit at the end of the roster
and carry the longest descriptions.

A `top` or `bottom` bubble now flips to the other side, and only into a
side that genuinely fits — an anchor with room on neither keeps the
requested placement rather than oscillating. Sliding the bubble
vertically instead would cover the text being read.
2026-08-11 14:26:18 +08:00

4.2 KiB
Raw Blame History

Agent Note: 预设卡片截断自身描述,而不是由描述决定整份名单的高度

Status: implemented

English | 中文

问题

preset 自行发布 description,长度不限,而设置分区把名单渲染为卡片网格。描述只有 min-height 没有上限,网格则以 grid-auto-rows: 1fr 排布行——该取值让每一个隐式行等高,而不只是承载高卡片的那一行。因此一条长描述决定了整份名单的高度:自定义组里放入一条 250 字的描述后,四张卡片全部量得 421px,短描述卡片被大片空白填满。

描述同时又是区分各个 preset 的字段,因此不能藏起来;卡片必须既给它设上限,又让全文仍然可达。

决定

描述截断为四行,其余内容通过共享的 Tooltip 呈现,且仅在元素确实溢出时才挂载(scrollHeight > clientHeight,并经 ResizeObserver 重新测量,因为设置面板宽度跟随窗口)。这与聊天统计行一致:它按同样的「先测量再挂载」规则截断为一行。

卡片高度仍是推导得出而非固定。描述有了上限之后,grid-auto-rows: 1fr 本身就让网格等高,而承载损坏原因或已展示目录的卡片仍能按自身内容定高——写死像素高度会把两者一并裁掉。

随之而来三个更小的决定:

  • .cardId 以 margin-top: auto 吃掉卡片的空余空间,描述不再拉伸。被 flex 拉伸的盒子会让截断高度与盒子高度不一致;让截断盒子只按内容定高,行为便不依赖这层交互。
  • 描述带有 title=""。空 title 表示该元素没有提示信息,查找就此停止,因此卡片主体的原生 tooltip 不会向上找到描述,被裁切的描述只回应一个气泡而不是两个。
  • Tooltip 新增可选的 maxWidth。它默认的半视口上限会把描述渲染成比所属设置弹窗还宽的一整块,溢出到背后的应用界面上。
  • Tooltip 同时在视口放不下时把 top 或 bottom 气泡翻到另一侧,此前它只做水平收敛。自定义 preset 位于名单末尾、又恰恰承载最长的描述,因此常见情形正是让一个高气泡挂在页面靠下的锚点之下。翻转只会移向确实放得下的一侧,两侧都放不下时保持请求的位置而不来回摆动;改为垂直滑动则会盖住正在阅读的文本。

形状检查未通过的名单行,徽记从 Broken(已损坏)改为 Failed to load(加载失败)。discovery 在组装文件缺失、读不出或格式错误时置位 broken——最常见的是用户刚编辑或删除的文件——因此断言损坏超出了观察到的事实,而徽记下方原样展示的原因本就点名了文件与修法。

备选方案

  • 写死卡片高度。 它直接表达了意图,却会裁掉两处高度本就可变的行:损坏预设的原因行和已展示的预设目录。
  • 用原生 title 属性承载完整描述。 无需测量也无需组件,代价是约一秒的延迟、操作系统的样式,以及在卡片大部分区域内顶替掉「设为默认」的提示。
  • 无条件挂载 tooltip。 省掉 ResizeObserver,代价是把鼠标停在短描述上时,弹出一个重复卡片已有内容的气泡。
  • hover 时展开截断。 它就地展示文本,同时让网格在指针下方发生位移。

后果

分区多了一个带测量的小组件,共享基元多了一个可选 prop。换来的是:任何卡片的高度都不再跟随名单中最长的那条描述;而且截断由 CSS 完成而非截短文本,完整描述始终留在无障碍树中。

title="" 的抑制作用由一条 DOM 断言钉住,而非通过观察原生 tooltip:浏览器 tooltip 画在页面之外,无法被捕获。若某个浏览器日后重新越过空 title 继续向上查找,退路是去掉卡片主体的 title——它的内容已经在主体的 aria-label 里。

测试

包内测试覆盖三种测量结果(被裁切、放得下、运行时没有 ResizeObserver)以及 tooltip 的宽度上限。web e2e golden 除 damaged.expected.md 按徽记文案重录外,其余原样回放通过。