# Agent Note: 提问 composer 的选项行是滚动内容，而非空间不足时的吸收方

Status: implemented

[English](2026-07-27-question-composer-rows-do-not-shrink.md) | 中文

## 问题

提问 composer 的卡片会按视口设上限（`max-height: min(60vh, 520px)`），并让选项列表自行滚动，这样在成批提问时标题和底部操作按钮始终可达。但当 composer 的容器变矮时（窗口较小，或视口偏矮且详情面板处于展开状态），选项行会互相叠在一起，也会叠到问题标题上。

缺陷不在这个高度上限，而在高度不足时由谁来吸收。`.options` 是一个 `flex-direction: column` 的盒子，其子元素默认取 `flex-shrink: 1`，因此空间不足时首先被压缩的是各个选项行，而不是让滚动容器产生溢出。一行会被压到它的 `min-height: 42px`，而 `.optionCopy` 仍保持文案折行后所需的更大固有高度（带描述的选项会占两行）。由于 `align-items: center`，文案于是以一个比自身更矮的盒子为基准居中，并向上下两个方向画到该行边框盒之外——向上盖住标题，向下盖住下一行。在实际发布的客户端上于 900x440 处实测：文案有 6.5px 落在行盒之外，视口高度降到 380px 时增至 10px，而 `.options` 报告的 `scrollHeight` 等于 `clientHeight`，因此始终不会给出滚动条。

只有文案会折行的选项行才能复现该问题。文案单行即可容纳的行，其内容与 42px 最小高度之间尚有余量，被压缩也看不出来——这正是既有 e2e fixture（测试前置数据）（选项为 `Blue`／`Green`，无描述）在任何尺寸下都渲染正常的原因。

## 决策

`.option` 与 `.custom` 声明 `flex-shrink: 0`。

在设有高度上限的卡片中，这些行是滚动内容；卡片的溢出归 `.options` 承担，它本就持有 `overflow-y: auto` 与 `min-height: 0`。把子元素固定住之后，高度不足会传导到那个滚动容器，而不再被行本身吸收，这正是该高度上限设计时想要的行为。另一种做法是允许行被压缩，但把文案约束在行内，那就必须在用户最需要阅读选项描述的尺寸上对其做裁剪或省略号处理。

`.header` 与 `.footer` 出于同样的原因，已在卡片层级带有 `flex-shrink: 0`；选项列表的子元素正是这条规则缺失的另一半。

## 曾考虑的替代方案

**在被压缩的行内裁剪文案或加省略号（对 `.option` 设 `overflow: hidden`）。** 这样一条声明就能消除重叠，且不必重新考虑布局。之所以否决：它把一个可见缺陷换成了一个无声缺陷——行仍保持 42px，而选项描述的第二行会在卡片最紧张的那些尺寸上直接消失。描述属于影响决策的内容，不是装饰。

**把 `align-items: center` 改为 `align-items: flex-start`。** 文案就只会向下生长，因此不再向上盖住标题。但这什么也没修好：被压缩的行依然会溢出到下一行上，而且这一改动会在所有尺寸下（包括常见尺寸）无声改变每个选项行的垂直对齐。

**移除卡片的 `max-height` 上限，使其永远不会被压缩。** 没有高度不足，就没有分配问题。之所以否决：正是这个上限保证成批提问时标题和底部操作按钮留在屏幕内；移除它会重新引入该上限本就为之存在的失败（composer 所处的容器是一个固定高度、`overflow: hidden` 的会话列，因此不设上限的卡片会连自己的提交按钮一起丢掉）。

**把折行文案限制为单行（对 `.description` 设 `white-space: nowrap` 加省略号）。** 行永远不会折行，因此被压缩时也永远不会溢出。否决理由与裁剪相同，此外它还为了修一个窄视口缺陷，而牺牲了空间充裕的宽视口渲染效果。

## 后果

- 被压缩的 composer 会滚动其选项列表，而不是让它互相重叠：在 900x380 处，该列表报告 `scrollHeight` 为 200、`clientHeight` 为 114，并给出滚动条；此前两者相等，不给滚动条。
- 选项行在任何视口尺寸下都保留完整的折行文案。不裁剪、不加省略号，宽视口下的渲染保持不变（该规则仅在 flex 盒子空间不足时才生效）。
- 由于高度不足不再被行部分吸收，卡片现在更早进入滚动状态。这正是该高度上限想要的行为，也意味着在此前只会无声画错列表的情形下，矮容器现在会显示滚动条。
- 该场景录制的问题，比它主要测试的那次往返所需的长度更长。这个代价是有意付出的：没有折行文案，该布局不变式无法被证伪，而为一条 CSS 规则再加一份 fixture 会更糟。

## 验证

Web e2e 的 composer 场景会在三个被压缩的容器高度（900x520／440／380）上，对活动的 composer 断言该运行时不变式：每个选项行的子元素都留在该行的边框盒之内。两道守卫防止该断言空洞地成立——必须至少有一行处于折行状态（这是唯一会溢出的形态），且 `.options` 必须确实处在滚动状态（证明容器确实触及了高度上限）。该场景录制的问题现在带有较长的选项描述，正是为此；没有折行文案，这条断言不可能失败。

在构建产物客户端上双向确认过：撤销 `flex-shrink: 0` 后该场景失败（`scrolls: false`，6.5px 溢出），恢复后通过。一次覆盖 340 种视口尺寸（420-1600 x 320-960）的独立几何遍历，从 86 种尺寸存在文案落在行盒之外，降到 0 种。

该断言仅在回放模式下执行：录制模式必须走到写入 fixture 那一步，而不是在布局检查处中断。另需注意，composer 以客户端模组包的形式发布，因此单跑 `pnpm run build:web` 不会带上对 `QuestionComposer.module.css` 的改动——必须执行包构建，浏览器测试通道才能看到它。

要复现这种空间不足，需要的是矮视口，而不是矮容器。高度上限为 `min(60vh, 520px)`，因此把会话列压到比卡片自身高度更矮，只会裁剪卡片，而不会让它空间不足——各行仍保持完整高度，也不会有任何溢出。凡是在 e2e 场景之外演示或测量该缺陷的手段，都必须改变视口。

`lib/` 陈旧会让浏览器测试通道对着一个比工作树更旧的客户端做断言，而中途失败的 `pnpm run build` 留下的正是这种状态：失败之前构建的那些包是新的，其余不是。在这种状态下刷新 golden，记录下来的是旧客户端的界面。抓取之前先确认构建以 0 退出；另需注意 `packages/` 下的未跟踪目录同样会被编译——来自另一个分支的遗留物可能以 diff 无法解释的原因让构建失败。
