docs(ui-deliverables): record CSS overflow policy

This commit is contained in:
imccyu 2026-09-01 01:50:27 +08:00
parent 3efd4b51e0
commit c11c3f98ad
9 changed files with 74 additions and 14 deletions

View file

@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.md
2026-07-31-web-workspace-file-links.md: ddc093e1ce5e646f6b96f1517b2a26c9e10fe423
2026-07-31-web-workspace-file-links.zh.md: f463de969bab7a47145abbbd46363b8807bcc673
2026-07-31-web-workspace-file-links.md: 944d7bef9b83eb34895c5f74e511600a713d786b
2026-07-31-web-workspace-file-links.zh.md: 71f0051204e0cb8b65995fc88a9edb230db8f5be

View file

@ -14,7 +14,7 @@ Two distinct defects sat behind that. The transcript never said what a turn had
## Decision
**A finished turn ends with the files it produced.** The row is its own plugin, `@deepseek-ai/dsh-client-ui-deliverables`, registered into the `conversation.chat.turnTail` hole the chat view renders between a closing message's body and its IconActions — ui-conversation owns the hole and the owner currency (nodes, closing seq, `openFile`), the plugin owns every policy. `producedForClosing` reads the paths off the mutation tools' own follow-along `locations` — a diff card, or a generic card whose `kind` is `edit` (the shape `str_replace_editor`'s insert presents) — so a turn's output is listed whether or not the closing message named it, and a new mutation tool joins by declaring what it does rather than by being added to a list. Reads, deletes, and failed calls contribute nothing; a path appears once per turn in first-seen order; accumulation resets on the turn boundary, so a turn that mutates and then ends without content text cannot spill into the next turn's row. The single-line lane measures its chips and localized remainder, then shows the largest prefix that fits (up to six) plus `+ N files`. One cordis.yml line composes the surface in or out; the unregistered hole renders nothing.
**A finished turn ends with the files it produced.** The row is its own plugin, `@deepseek-ai/dsh-client-ui-deliverables`, registered into the `conversation.chat.turnTail` hole the chat view renders between a closing message's body and its IconActions — ui-conversation owns the hole and the owner currency (nodes, closing seq, `openFile`), the plugin owns every policy. `producedForClosing` reads the paths off the mutation tools' own follow-along `locations` — a diff card, or a generic card whose `kind` is `edit` (the shape `str_replace_editor`'s insert presents) — so a turn's output is listed whether or not the closing message named it, and a new mutation tool joins by declaring what it does rather than by being added to a list. Reads, deletes, and failed calls contribute nothing; a path appears once per turn in first-seen order; accumulation resets on the turn boundary, so a turn that mutates and then ends without content text cannot spill into the next turn's row. CSS container-width bands select a prefix of up to six paths and its matching `+ N files` label, while flexbox shrinks and ellipsizes the visible basenames; the component performs no JavaScript layout observation ([decision](../simplification/2026-09-01-css-produced-file-layout.md)). One cordis.yml line composes the surface in or out; the unregistered hole renders nothing.
**The path link reads as a link.** Underlined at rest, not only on hover. This is the smaller half of the diff and the larger half of the fix.
@ -28,9 +28,9 @@ Two distinct defects sat behind that. The transcript never said what a turn had
- **Same-origin HTTP serving without isolation** — measurably unsafe, and recorded so nobody retries it: a document served beside `/api` drove `settings.describe` to a `200` with full data and `session.list` to 35 KB of every session's transcript, from a page that need not be agent-authored at all (a read row makes every file in a cloned repository openable).
- **`Content-Security-Policy: sandbox` over that same-origin serving** — closes the hole by taking the document's origin away, which measurably breaks the pages this feature exists to show: the reported artifact throws `SecurityError` on load, and because an uncaught exception aborts the rest of its `<script>`, every listener declared after that line — theme toggle, mobile menu, model tabs — never binds. Two of the four artifacts in the reporting user's workspace were dead pages under it, and they still rendered perfectly, so the breakage was invisible.
- **Linkifying paths in the assistant's closing message** — the shape a user asks for ("put the link at the end"), but it makes rendering depend on the model spelling a path recognizably. The tool calls already carry `locations` as structured fact, so the produced-files row consumes that instead.
- **Horizontal chip scrolling** — keeps every file in the DOM but makes the hidden tail undiscoverable, adds a nested horizontal gesture to the transcript, and provides no exact account of what is out of view. One measured line with a stable remainder preserves the answer's vertical rhythm and keeps the omission explicit.
- **Horizontal chip scrolling** — keeps every file in the DOM but makes the hidden tail undiscoverable, adds a nested horizontal gesture to the transcript, and provides no exact account of what is out of view. A non-scrolling CSS lane with a width-selected visible prefix preserves the answer's vertical rhythm and keeps the omission explicit.
- **An embedded WebView in the desktop shell** — the strongest isolation available, since the preview then runs in a container the product owns rather than in the user's browser. It belongs to the desktop shell's own design, not to this surface, and is recorded here as the direction a future preview capability should take.
## Consequences
Every existing file affordance changed at once: write, edit, read, and the generic single-file card all reach `openFile`, so the link fix and browser preference apply without per-row changes. The assembled Web test covers overflow geometry and a one-click Host handoff without launching a native application. A produced `file://` document cannot `fetch` its own siblings (while `<script src>`, `<img>`, and CSS `@import` work), the one capability HTTP serving had that this does not. Remote clients keep the chips but omit the folder action; the full path remains in each chip's `title`. Markdown still opens in the platform's `.md` application; in-product rendering is separate work.
Every existing file affordance changed at once: write, edit, read, and the generic single-file card all reach `openFile`, so the link fix and browser preference apply without per-row changes. The assembled Web test covers single-line CSS overflow and a one-click Host handoff without launching a native application. A produced `file://` document cannot `fetch` its own siblings (while `<script src>`, `<img>`, and CSS `@import` work), the one capability HTTP serving had that this does not. Remote clients keep the chips but omit the folder action; the full path remains in each chip's `title`. Markdown still opens in the platform's `.md` application; in-product rendering is separate work.

View file

@ -14,7 +14,7 @@ Status: implemented
## 决策
**完成的一轮以它产出的文件收尾。** 该行是独立插件 `@deepseek-ai/dsh-client-ui-deliverables`,注册进 chat 视图在收尾消息正文与其 IconActions 之间渲染的 `conversation.chat.turnTail` 空位——ui-conversation 拥有空位与 owner 通货(节点、收尾 seq、`openFile`),插件拥有全部策略。`producedForClosing` 从改写工具自身的跟随文件 `locations` 中读出路径——diff 卡片,或 `kind` 为 `edit` 的 generic 卡片(即 `str_replace_editor` 的 insert 所呈现的形状)——因此无论收尾消息是否点名,这一轮的产出都会被列出;新的改写工具靠声明自己做了什么加入,而不是靠被加进某张名单。read、删除与失败的调用不贡献任何条目;同一路径在一轮内按首见顺序只出现一次;累积在 turn 边界重置,因此一轮若先改写文件、随后没有正文内容就结束,不会溢进下一轮的行里。单行 lane 会测量 chip 和本地化剩余计数,再显示能放下的最大前缀(至多六个)及 `+ N 个文件`。cordis.yml 中的一行即可把该交互面组合进来或去掉;未注册的空位什么也不渲染。
**完成的一轮以它产出的文件收尾。** 该行是独立插件 `@deepseek-ai/dsh-client-ui-deliverables`,注册进 chat 视图在收尾消息正文与其 IconActions 之间渲染的 `conversation.chat.turnTail` 空位——ui-conversation 拥有空位与 owner 通货(节点、收尾 seq、`openFile`),插件拥有全部策略。`producedForClosing` 从改写工具自身的跟随文件 `locations` 中读出路径——diff 卡片,或 `kind` 为 `edit` 的 generic 卡片(即 `str_replace_editor` 的 insert 所呈现的形状)——因此无论收尾消息是否点名,这一轮的产出都会被列出;新的改写工具靠声明自己做了什么加入,而不是靠被加进某张名单。read、删除与失败的调用不贡献任何条目;同一路径在一轮内按首见顺序只出现一次;累积在 turn 边界重置,因此一轮若先改写文件、随后没有正文内容就结束,不会溢进下一轮的行里。CSS 容器宽度档位会选择前六条路径中的一个前缀及其匹配的 `+ N 个文件` 标签,flexbox 则收缩可见的 basename 并用 ellipsis 省略;组件不执行 JavaScript 布局观察([决策](../simplification/2026-09-01-css-produced-file-layout.zh.md))。cordis.yml 中的一行即可把该交互面组合进来或去掉;未注册的空位什么也不渲染。
**路径链接读得出是链接。** 静止状态下就带下划线,而不只在悬停时。这是本次改动中更小的那一半,却是修复中更大的那一半。
@ -28,9 +28,9 @@ Status: implemented
- **同源 HTTP 提供且不加隔离**——经实测不安全,记录在此以免有人重试:与 `/api` 并排提供的文档把 `settings.describe` 打到 `200` 并拿到完整数据,把 `session.list` 打到包含所有会话 transcript 的 35 KB,而这个页面根本不必由 agent 撰写(一条 read 行就让 clone 下来的仓库里任何文件变得可打开)。
- **在那套同源提供之上加 `Content-Security-Policy: sandbox`**——它以剥夺文档的源来堵住这个洞,而这经实测会破坏本功能存在的意义所在的那类页面:所报告的产物在加载时抛 `SecurityError`,又因为未捕获异常会中止其 `<script>` 的其余部分,该行之后声明的所有监听器——主题切换、移动端菜单、模型 tabs——统统不会绑定。报告者工作区里四份产物有两份在它之下是死页面,而且它们渲染得完美无缺,所以这种破坏是看不见的。
- **把路径在助手的收尾消息里链接化**——这是用户开口要的形状(「在结尾附上链接」),但它让渲染取决于模型是否把路径拼写得可识别。工具调用已经把 `locations` 作为结构化事实携带,产出文件行消费的正是它。
- **让文件 chip 横向滚动**——这样会把每个文件都留在 DOM 中,却使隐藏的尾部难以发现,在 transcript 内增加一层横向手势,也无法精确说明视口外还有什么。经过测量的一行和稳定的剩余计数既保留回答的纵向节奏,也明确呈现省略量。
- **让文件 chip 横向滚动**——这样会把每个文件都留在 DOM 中,却使隐藏的尾部难以发现,在 transcript 内增加一层横向手势,也无法精确说明视口外还有什么。不滚动的 CSS 单行与按宽度选择的可见前缀既保留回答的纵向节奏,也明确呈现省略量。
- **桌面端外壳中的内嵌 WebView**——可得到的最强隔离,因为那时预览跑在产品自己拥有的容器里,而不是用户的浏览器里。它属于桌面端外壳自身的设计,而非本交互面,记录在此作为未来预览能力应走的方向。
## 后果
现有的每一处文件交互都同时改变了:write、edit、read 与通用单文件卡片都汇到 `openFile`,因此链接修复与浏览器优先策略无需逐行改动。组装层 Web 测试覆盖溢出几何和单次点击的 Host 交接,且不会启动原生应用。产出的 `file://` 文档无法 `fetch` 同级文件(但 `<script src>`、`<img>` 和 CSS `@import` 可用),这是 HTTP 提供曾有、而此处没有的能力。远程客户端保留 chip,但省略文件夹操作;每个 chip 的 `title` 仍保留完整路径。Markdown 仍由平台的 `.md` 应用打开;产品内渲染属于另一项工作。
现有的每一处文件交互都同时改变了:write、edit、read 与通用单文件卡片都汇到 `openFile`,因此链接修复与浏览器优先策略无需逐行改动。组装层 Web 测试覆盖单行 CSS 溢出和单次点击的 Host 交接,且不会启动原生应用。产出的 `file://` 文档无法 `fetch` 同级文件(但 `<script src>`、`<img>` 和 CSS `@import` 可用),这是 HTTP 提供曾有、而此处没有的能力。远程客户端保留 chip,但省略文件夹操作;每个 chip 的 `title` 仍保留完整路径。Markdown 仍由平台的 `.md` 应用打开;产品内渲染属于另一项工作。

View file

@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-09-01-css-produced-file-layout.md
2026-09-01-css-produced-file-layout.md: fce7d70b21b05db8970fb249927c36f87abc6b23
2026-09-01-css-produced-file-layout.zh.md: bc8ad941dbe33af3acc629f0da50e79c4a0954da

View file

@ -0,0 +1,27 @@
# Agent Note: replace produced-file probes with CSS width bands
Status: implemented
English | [中文](2026-09-01-css-produced-file-layout.zh.md)
## Problem
The produced-files row duplicated every candidate chip in a hidden probe tree, synchronously read computed styles and element geometry in a layout effect, and repeated those reads whenever the row or a probe resized. That machinery existed only to choose how many labels fit on one line. Its forced layout work cost more than exact width-dependent chip counts were worth.
The produced-file discovery and Host-opening behavior remain owned by [opening a produced file from the web UI](../feature/2026-07-31-web-workspace-file-links.md). This decision changes only how that row handles limited horizontal space.
## Decision
The produced-files row is an inline-size query container. CSS width bands hide trailing chips from the six-file prefix and select the corresponding pre-rendered localized remainder label. The flex row performs the actual shrinking, and each basename uses CSS ellipsis while the selected remainder label keeps its intrinsic width. `ProducedFiles` has no resize observer, layout effect, layout state, duplicate chip probes, computed-style lookup, or element-geometry read.
The row keeps its chip maximum and gap as local CSS custom properties. Each width band corresponds to that budget and reveals one matching remainder label when paths are omitted, so `+ N files` remains accurate for the CSS-selected prefix. The same selected label controls whether the existing Host-gated **Show in folder** action is visible.
## Alternatives considered
- **Observe only the row and estimate from its width** — this removes content measurement but still creates one observer and width-driven React state per mounted result row even though CSS already receives the same width.
- **Always render six chips and let flexbox shrink them** — this removes all sizing logic but can reduce six filenames to nearly empty targets on a narrow conversation column. CSS width bands retain useful labels without runtime observation.
- **Wrap or horizontally scroll every chip** — wrapping changes the turn's vertical rhythm, while horizontal scrolling makes the tail hard to discover. The fixed cap keeps the row bounded without either interaction.
## Consequences
Mounting and resizing the row performs no JavaScript layout work. Because fixed width bands ignore actual text widths and localized remainder widths, they may show one more or fewer chip than exact measurement would; flex shrinking and clipping preserve the single-line layout, and the selected `+ N files` label still counts every omitted path. Rendered files preserve their full path in the accessible name and `title`. Each row carries at most six short remainder spans, of which CSS exposes zero or one. The assembled browser test verifies responsive omission, one-line layout, and the absence of horizontal overflow.

View file

@ -0,0 +1,27 @@
# Agent Note: 以 CSS 宽度档位替代产出文件探针
Status: implemented
[English](2026-09-01-css-produced-file-layout.md) | 中文
## 问题
产出文件行会在隐藏探针树中复制每个候选 chip,在 layout effect 中同步读取计算样式和元素几何,并在该行或任一探针改变尺寸时重复读取。这套机制只用于决定一行能容纳多少标签,其强制布局工作不值得用来换取随宽度精确变化的 chip 数量。
产出文件发现与 Host 打开行为仍由[从 web UI 打开产出的文件](../feature/2026-07-31-web-workspace-file-links.zh.md)所有。本决策只改变该行处理有限横向空间的方式。
## 决策
产出文件行是一个 inline-size 查询容器。CSS 宽度档位会隐藏六文件前缀尾部的 chip,并选择对应的预渲染本地化剩余计数标签。flex 行负责实际收缩,各 basename 由 CSS 以 ellipsis 省略,而选中的剩余计数标签保持自身宽度。`ProducedFiles` 不含 resize observer、layout effect、布局 state、重复 chip 探针、计算样式查询或元素几何读取。
该行以局部 CSS 自定义属性保存 chip 最大宽度与间距。每个宽度档位对应这份预算,并在省略路径时显示一个匹配的剩余计数标签,因此 `+ N 个文件` 对 CSS 选中的前缀保持准确。同一个选中标签控制现有 Host 能力约束下的**在文件夹中显示**动作是否可见。
## 考虑过的替代方案
- **只观察该行并根据其宽度估算**——这会删除内容测量,却仍会为每个已挂载结果行创建一个 observer 和宽度驱动的 React state,而 CSS 已经获得同一份宽度。
- **始终渲染六个 chip 并让 flexbox 收缩**——这会删除全部尺寸逻辑,却可能在狭窄会话列中把六个文件名压缩成近乎空白的点击目标。CSS 宽度档位无需运行时观察即可保留有用标签。
- **换行或横向滚动全部 chip**——换行会改变轮次的纵向节奏,横向滚动则使尾部难以发现。固定上限无需这两种交互即可约束该行。
## 后果
该行挂载与改变尺寸时不执行 JavaScript 布局工作。由于固定宽度档位忽略实际文本宽度与本地化剩余计数宽度,它可能比精确测量多显示或少显示一个 chip;flex 收缩与裁切会保持单行,选中的 `+ N 个文件` 标签仍准确统计全部未展示路径。已渲染文件在无障碍名称与 `title` 中保留完整路径。每行至多携带六个短剩余计数 span,其中 CSS 展示零个或一个。组装层浏览器测试验证响应式省略、单行布局与无横向溢出。

View file

@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/ui-deliverables/README.md
README.md: ac56ae782ac13664fdfb32c51ea07e2f8f9818d7
README.zh.md: 1aefe0d3c94f3d1c93c193426fd3d21b50359c75
README.md: b228106848549dcddcde84be07c47392dc92a9db
README.zh.md: 0a9d5207fe3c09b89ac34592f04c7eeaeed9b59a

View file

@ -25,11 +25,11 @@ This package renders the deliverables row a finished turn ends with — the file
<a id="use-this-package"></a>
## Use this package
Mount this plugin alongside `ui-conversation`; a finished turn then ends with the produced-files row between the closing message's body and its action footer. Each chip opens the file through the Host opener, with relative paths resolved against the session cwd; when the row first appears, it queries `session.canOpenWorkspacePath()`, and a **Show in folder** action opens the session workspace only when the page is loopback and that query succeeds with `true`.
Mount this plugin alongside `ui-conversation`; a finished turn then ends with the produced-files row between the closing message's body and its action footer. Each chip opens the file through the Host opener, with relative paths resolved against the session cwd; when the row first appears, it queries `session.canOpenWorkspacePath()`, and an omitted-file **Show in folder** action opens the session workspace only when the page is loopback and that query succeeds with `true`.
### The row
The row shows the largest leading prefix that fits — up to six chips, basename text with the full path as the title — reserving the exact localized `+ N files` width, so the remainder stays visible without wrapping or horizontal scrolling.
The row uses CSS container-width bands to show a responsive prefix of up to six file chips. Flexbox shrinks and ellipsizes basename text, while CSS selects the matching localized `+ N files` label for omitted paths; the full path remains available as the title, and the row performs no JavaScript layout observation or horizontal scrolling.
### Inline-code links

View file

@ -25,11 +25,11 @@ kind: "package-reference"
<a id="use-this-package"></a>
## 使用本包
与 `ui-conversation` 一起挂载本插件;已完成轮次随即以产出文件行收尾,位于收尾消息正文与其动作页脚之间。每个标签项经 Host 打开器打开文件,相对路径按会话 cwd 解析;该行首次显示时会查询 `session.canOpenWorkspacePath()`,只有页面为 loopback 且查询成功返回 `true` 时,**在文件夹中显示**动作才会打开会话工作区。
与 `ui-conversation` 一起挂载本插件;已完成轮次随即以产出文件行收尾,位于收尾消息正文与其动作页脚之间。每个标签项经 Host 打开器打开文件,相对路径按会话 cwd 解析;该行首次显示时会查询 `session.canOpenWorkspacePath()`,有文件被省略、页面为 loopback 且查询成功返回 `true` 时,**在文件夹中显示**动作才会打开会话工作区。
### 该行
该行展示能放下的最大前缀——至多六个标签项,文本为文件名、完整路径作为 `title`——并为本地化后的精确 `+ N 个文件` 宽度预留空间,因此剩余计数始终可见,既不换行也不横向滚动。
该行通过 CSS 容器宽度档位响应式展示至多六个文件标签项。Flexbox 负责收缩文件名并用 ellipsis 省略,CSS 为未展示路径选择匹配的本地化 `+ N 个文件` 标签;完整路径仍保留在 `title` 中,该行不执行 JavaScript 布局观察,也不提供横向滚动。
### 行内代码链接